浅谈JavaScript原型链机制

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),它的好处在于是直接修改原型的对象的引用,不会存在因抛弃对象存在的垃圾回收的性能问题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值