1.原型是什么
funetion Person(){
}
const p1 = new Person()
Person.prototype //Object
Person.prototype === p1.__proto__ //true
Person.prototype.__proto__ === Object.prototype //true
Array.prototype //[]
Array.prototype === []. _proto //true
Array.prototype.__proto__ === Object.prototype //true
Object.prototype //Object {}
String.prototype //String (length; 0, [[Primitivevalue]l:"")
Function.prototype //function (){}
复制代码
每个对象实例都有一个_proto_属性,每个构造函数都有一个prototype属性
需要注意,_proto_属性并不是标准,目前没有兼容所有浏览器
设constructor为某构造函数,obj为由该构造函数生成的对象实例,则有:
constructor.prototype === obj._proto_
constructor.prototype._proto_ === Object.prototype
Object.prototype._proto_ === null
复制代码
结论:
- 每个构造函数的prototype指向的对象都是不同的,独一无二的
- 构造函数的prototype属性和对象实例的_proto_指向统一的原型对象(但是注意实例与构造函数的原型有直接的关系,与构造函数本身没有)
- 所有对象的原型链顶端都指向Object.prototype这个对象,而后者的原型对象指向了null。
- 任意构造函数的prototype(或者说任意对象实例的_proto_)都是0bject的一个对象实例
2.对象属性获取
设obj为某一个对象实例,执行console.log(obj.foo)
,发生了什么:
获取(或者说引用)对象实例的某个属性时,会先检查该对象实例本身是否直接包含该属性,如果有就使用它(即get操作);没有则会通历其原型链,如果最终仍然找不到,则返回undefined
3.对象属性设置
设obj为某一个对象实例,执行obj.foo = 'bar'
,发生了什么?
-
如果obj对象中直接包含了名为foo的属性,则会修改已有的直接属性值
-
如果obj对象没有直接包含名为foo的属性,就会历其原型链,如果仍然找不到名为foo的属性,则foo会被添加到obj的直接包含属性中
-
接上一条,如果遍历其原型链时存在对应的foo属性,并且该属性可写(writable:true),则foo会被添加到obj的直接包含属性中。即此时原型链上对应的属性被屏蔽
-
接上一条,如果遍历其原型链时存在对应的foo属性,并且该属性只读(writable:false),则不会修改该属性或者创建该对象实例的直接属性,总之什么都不会发生(在非严格模式下会抛出错误)
-
接上一条,如果遍历其原型链时存在对应的foo属性,并且该属性是一个setter,那就一定会调用这个setter。此时不会添加为obj的直接包含属性
4.迷惑的constructor属性
function Person(){
}
const p1 - new Person()
Person.prototype.constructor === Person //true
p1.constructor === Person //true
复制代码
上面代码中,我们发现构造函数的prototype默认有一个constructor属性,而创建的对象实例默认也有一个constructor属性。它们都统一的指向了构造函数本身,这“似乎”表明这个属性指向“创建这个对象的函数”或者“对象实例由…创建”
实际上,对象实例本身并没有constructor 属性,而且这个属性井不表示由…构造”
首先我们需要消除一个容易存在的误区:
所有的函数声明(无论它的声明首字母是否大写)都是普通函数, 函数本身不是构造函数, 当且仅当你使用new时,函数调用会变成”构造函数调用”
根据上面的例子,我们来揭开constructor属性的真面目:
- Person.prototype的constructor属性只是Person函数在声明时的默认属性,它默认指向Person本身。
- 当通过new Person生成的对象实例所能访问的constructor属性其实是依据原型链访问到了Person.prototype.constructor.
- 如果我们修改了Person.prototype的指向,其对应的constructor不会保留原有的值,即不再是Person.
见下方示例:
function Person() {}
const p1 - new Person()
p1.constructor === Person //true
//修改构造函数的原型对象
Person.prototype - new Array()
//新建一个实例
const p2 = new Person()
//cantructor指向改变了
p2.constructor === Person //false
p2.constructor === Array//true
复制代码
代码解析:
- 先通过new Person()创建一个对象实例,访问
p1.constructor
,其本身设有该属性,所以按照原型链访问到Person.prototype
(即p1.__proto__
),由于Person在声明时使得Person.prototype
默认有了该属性,所以访问到了。其值指向了Person。 - 接着我们修改
Person.prototype
为一个Array构造函数的象实例,然后再通讨new Person生成一个对象实例p2。此时访问p2.constructor
。同理,p2本身无该属性,然后顺着原型链访问到了Person.prototype
,此时它已被修改,使得原有的constructor值已丢失。所以会继续按照原型链访问Person.prototype.__proto__
,即new Array().__proto__
,也就是Array.prototype
。而与Person同理,Array.prototype.constructor
默认指向的是构造函数Array。所以访问p2.constructor就指向了Array
如果construtor属性表示由…构造,那么Person的对象实例的constructor属性应该永远指向Person,显然实际并不是这样的。
5.原型继承
通过原型链的知识,我们知道new出来的实例对象可以访问到原型对象中的属性或方法。有时候根据需要我们需要更改原型对象的默认指向,然后就能拿到该对象的属性和方法。这就是所谓的原型继承
。
设Bar和Foo是两个不同的构造函数,我们想让Bar”原型继承”Foo,此时更改原型对象主要存在以下几种方法:
(1) Bar.prototype === Foo.prototype
这种方法只是单纯修改Bar.prototype指向由默认原型对象变成了Foo.prototype。它的问题在于如果修改其中一个原型对象。就会影响另一个原型对象,所以这并不是我们想要的
(2)Bar.prototype === new Foo()
此方法与(2)相比,解决了原型对象引用共享的问题,但是如果函数Foo存在一些副作用(如修改状态,给this添加属性等),同样会影响到Bar的实例对象,此法也不可取
(3)Bar.prototype === 0bject.create(Foo.prototype)
object.create(obj)
会凭空创建并返回一个以目标参数对象为原型对象的新对象,这样就解决了(1)和(2)的问题。
它的唯一缺点在于该方法是抛弃了默认的原型对象,创建了新的对象来代替它。有一定轻微性能损失。
(4)Object.setPrototypeof(Bar.prototype,Foo.prototype)
此方法为ES6新增,相比(3),它的好处在于是直接修改原型的对象的引用,不会存在因抛弃对象存在的垃圾回收的性能问题