实例化与原型链变更

2020/05/22 Javascript 共 3513 字,约 11 分钟

从一个问题引入,如何实现new实例化

构造函数实例化的过程: 1、创建对象。创建一个新对象 2、绑定原型。将构造函数的作用域赋值给新对象,也即将新对象的__proto__指向构造函数的prototype属性 3、添加方法。将构造函数的this指向该对象,通过this将属性和方法添加至这个对象 4、返回对象。返回this指向的对象,也即实例

手写还原实例化过程

我们通过函数描述上面实例化的过程:

extra:

  • 这里我们是一种比较旧的思路实现new,这样做主要是为了方便理解实例化的本质过程,其实我们也可以通过Object.create(constructor.prototype)的方式直接复制一个原型出来

优质实现,只需要将创建对象绑定原型使用Object.create合并为一步:

// 创建对象 & 绑定原型
let obj = Object.create(constructor.prototype)

需要注意:

  • fn.prototype上是存在constructor构造器的,这也是构造调用的特点
    • 手动进行原型绑定时,需要注意复原这种关系

手动绑定原型

function Foo(name) {
  this.name = name
}
Foo.prototype.myName = function () {
  return this.name
}
function Bar(name, label) {
  Foo.call(this, name)
  this.label = label
}
// 需要注意,这里原型链被改写,Bar.prototype.constructor丢失
// 已经被替换为了Foo.prototype.constructor
Bar.prototype = Object.create(Foo.prototype)
console.log('it should be [true]:', Bar.prototype.constructor === Foo)
Bar.prototype.myLabel = function () {
  return this.label
}
var a = new Bar('a', 'obj.a')
console.log('it should be [a]: ', a.myName());
console.log('it should be [obj.a]: ', a.myLabel());

需要注意:

  • Object.create(Foo.prototype)实际上使用Foo.prototype创建了一个新的原型对象
    • 因为Foo为函数,则存在Foo.prototype.constructor === Foo
    • 则赋值给Bar时,原型发生替换,constructor被一同替换掉了
  • 所以,如果只是单纯的获取原型方法,而不是类似于new构造调用的继承,我们可以使用如下语句将constructor变更回来
    • Bar.prototype.constructor = Bar

总结

当你在普通函数调用前面加上new关键字以后,就会把这个函数调用变成一个“构造函数”调用。这个行为的本质实际上是new操作符对于普通函数的劫持。

一般地:

  • 在JS中,函数为一等公民,只有函数才有prototype属性,才可以作为构造函数进行实例构造
    • 需要额外注意,lambda函数没有prototype属性,不可以作为构造函数
  • 所以,所有实例都有__proto__属性,用于指向构造函数的prototype
    • 需要注意,函数本身也存在__proto__属性,可以理解为非标属性__proto__的标准化统一

延伸

proto

__proto__是一个被无奈标准化的非标属性,看起来是一个属性,实际上更像一个getter/setter,大致实现如下:

Object.defineProperty(Object.prototype, '__proto__', {
  get() {
    return Object.getPrototypeOf(this)
  },
  set(proto) {
    Object.setPrototypeOf(this, proto)
    return proto
  }
})

Object.create

Object.create方法创建一个新对象,使用参数来提供新创建的对象的__proto__

Object.create的polyfill实现如下:

if (!Object.create) {
  Object.create = function(proto) {
    function F() {}
    F.prototype = proto
    return new F()
  }
}

Object.create的价值在于:

  • 创建一个新的对象,并关联到prototype
  • 不存在new构造调用可能发生副作用的场景 详见下方【原型链变更方式与问题对比】

原型链变更方式与问题对比

粗鄙原型链修改

Bar.prototype = Foo.prototype

为什么说粗鄙呢?因为这是一种很糟糕的实现,我们知道prototype本身是一个引用,上面的方式只是通过修改引用指针的方式实现,这样会有比较严重的问题:

  • Foo.prototype发生改变时,会通过指针引用的方式影响到Bar.prototype
  • 科学的方式应该是创建一个关联到prototype的新对象,这也是使用new操作符进行构造调用的意义

new构造调用

Bar.prototype = new Foo()

这种方式虽然通过创建一个新对象,然后关联到prototype的方式,避免了原型指针的污染影响,但是也存在了一些可能导致副作用的问题:

  • 问题的根本原因源于构造调用本身,我们使用new操作符进行构造调用时,会执行构造函数本身,此时假如Foo函数包含了一些含有副作用的内容,如:
    • 修改一些局部或者全局的状态,污染全局或者局部作用域
    • this添加一些属性,容易发生属性屏蔽
    • 等等
  • 我们期望的原型修改需要有两个条件:
    • 创建新的关联对象,关联到prototype对象,不通过指针污染原型
    • 只关注原型本身,不引入其他任何可能发生副作用的场景

比较靠谱的实现Object.create

Bar.prototype = Object.create(Foo.prototype)

通过Object.create的方式,我们实现了:

  • 创建一个新的对象,并关联到prototype
  • 不引入new构造调用可能发生副作用的不稳定因素
  • 最终,我们通过新旧原型替换的方式,实现了原型链变更

但是这样有一个小问题,原型链发生了替换,带来了轻微的性能损失(抛弃的对象需要进行GC),那有没有直接修改原型的方式?

原型修改setPrototypeOf

Object.setPrototypeOf(Bar.prototype, Foo.prototype)

这种实现,性能最优,但是却并没有Object.create精短且利于理解,虽然JS是可变原型机制,但我们仍希望通过一种 immutable的方式进行原型链更新,这也更利于管理我们的心智模型


[1] 备注【1】:原型属性屏蔽

Search

    Table of Contents