在开始阅读这篇文章之前,你可以对比下面这两段代码的输出结果是否一致(假设 myNew
是你自己实现的 new 操作):
function F(){}
F.prototype = null
const obj1 = new F()
const obj2 = myNew(F)
console.log(Object.getPrototypeOf(obj1))
console.log(Object.getPrototypeOf(obj2))
如果不一样,那么可能说明你的 myNew
方法和标准的 new
操作之间存在着些许出入,这篇文章也许能够让你的方法更加完善 / 严谨。
从 Object.create
说起
最近在刷一些手写实现原生方法的面试题,偶然看到了有一个 Object.create()
方法的实现是这么写的:
Object.myCreate = function (proto, propertyObject = undefined) {
if (propertyObject === null) {
// 这里没有判断propertyObject是否是原始包装对象
throw 'TypeError'
} else {
function Fn () {}
Fn.prototype = proto
const obj = new Fn()
if (propertyObject !== undefined) {
Object.defineProperties(obj, propertyObject)
}
if (proto === null) {
// 创建一个没有原型对象的对象,Object.create(null)
obj.__proto__ = null
}
return obj
}
}
而这个方法在规范里的实现是这样的:
简单地说,它会接受两个参数,第一个参数作为调用后返回对象的 __proto__
,第二个参数负责配置该对象的相关属性。而这里的第一个参数,可以是对象也可以是 null
。
基本上,上面代码的实现没有什么问题,但是我突然产生了一个疑问:当第一个参数是 null
的时候,Fn.prototype = proto
已经把构造函数的原型对象设置为 null
了,为什么后面还要在判断第一个参数为 null
之后设置 obj.__proto__ = null
呢?这两个语句的作用难道不是一样的吗?毕竟 Fn.prototype
和 obj.__proto__
都是指向同一个原型对象呀!
于是我将代码中的 if (proto === null)
判断去掉,并分别测试了 Object.create()
方法和 Object.myCreate()
方法:
可以看到,第二个打印是符合预期的,返回对象的 __proto__
确实指向传入的参数 null
;但第一个打印却和预想的不一样,展开打印对象后会发现,它其实是 Object.prototype
。这是怎么回事呢?难道说代码中执行 Fn.prototype = proto
的时候,实际上实例的 __proto__
并没有跟着改变?
于是继续测试:
这里可以看到:用 null
重写构造函数的原型后,通过 new 构造函数创建的实例的 __proto__
并没有跟着变成 null
,而是指向了 Object.prototype
。
调用构造函数的时候做了什么?
这时候,我们可能会想到,通过 new 调用构造函数的时候,内部可能做了一些处理,导致最终返回的实例对象的 __proto__
和我们预期的不一致。既然如此,我们通过规范看一下调用构造函数的时候,具体做了什么事:
这里我们只需要关注第六步和第七步。这两步会检查构造函数的原型对象的类型,如果是一个对象,则会将其作为实例的 __proto__
;如果不是对象,则会将 Object.prototype
作为实例的 __proto__
。这就能解释为什么用 null
重写构造函数的原型后,实例的 __proto__
没有跟着改变了,因为在调用构造函数的过程中,它链接上了 Object.prototype
,可以说,这里实例的原型链并没有断开。
实现一个更严谨的 new
在大部分的手写 new
实现中,通常都没有去检查构造函数的原型是否是一个对象。有的实现中甚至直接使用了 Object.create()
方法以快速地建立原型关系,就像这样:
function myNew(Fn,...args){
if(typeof Fn != 'function'){
throw new TypeError(Fn + 'is not a constructor')
}
const instance = Object.create(Fn.prototype)
const returnValue = Fn.call(instance,...args)
return returnValue instanceof Object ? returnValue : instance
}
这里直接使用Object.create()
方法,是有问题的。
在前面阅读规范的时候我们已经知道了,即使传给 Object.create
的参数是 null
,也会将其作为创建的对象的 __proto__
,所以这里如果使用了 Object.create
,并且构造函数的原型 Fn.prototype
还恰好就是 null
的话,就会导致实例的 __proto__
也是 null
,这和 new
的实际实现是有出入的。
所以,如果想实现一个更加严谨的 new
,那么就不应该在内部去调用 Object.create
方法,而应该选择手动创建一个对象并和构造函数建立原型关系,同时,我们还应该加入对构造函数原型的类型判断,看它到底是不是一个对象。
因此,上面的代码可以修改如下:
function myNew(Fn,...args){
if(typeof Fn != 'function'){
throw new TypeError(Fn + 'is not a constructor')
}
const instance = {}
// 检测构造函数原型是不是对象
instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype
const returnValue = Fn.call(instance,...args)
return returnValue instanceof Object ? returnValue : instance
}
现在,我们再用这个改进之后的 new
去测试文章开头的代码:
可以看到,加入了对构造函数原型可能为 null
的处理之后,返回的实例的 __proto__
明确指向了 Object.prototype
。现在我们实现的 new
就更加严谨了,而且也更接近原生的 new
操作。
本文到这里就结束了。不过,从语言设计的角度来说,为什么不将实例的 __proto__
也跟着设置为 null
呢?这里不断开实例的原型链,而是将其链接到 Object.prototype
有什么好处?大家可以在评论区留言讨论一下。另外,不排除本文存在原理性的错误或者说法上的偏颇,如果你发现了,也欢迎在评论区指正。
参考
https://262.ecma-international.org/5.1/#sec-13.2.2
http://es5.github.io/#x15.2.3.5
https://stackoverflow.com/questions/18198178/null-prototype-object-prototype-and-object-create