从一个问题引入,如何实现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
属性,不可以作为构造函数
- 需要额外注意,lambda函数没有
- 所以,所有实例都有
__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】:原型属性屏蔽