共计 2646 个字符,预计需要花费 7 分钟才能阅读完成。
这是 JS 原生办法原理探索系列的第三篇文章。本文会介绍如何模仿实现 new
操作符。对于 new
的具体用法,MDN 曾经形容得很分明了,这里咱们不说废话,间接讲如何模仿实现。
new 操作符的标准
留神:上面展现的所有标准都是 ES5 版本的,与当初最新的标准有些区别
首先看一下依据标准的形容,new
操作符做了什么事:
全是英文,不过没关系,我简略翻译一下:
我在应用 new
操作符的时候,前面跟着的构造函数可能带参数,也可能不带参数,如果不带参数的话,比如说 new Fn()
,那么这里这个 Fn
就是一个 NewExpression
;如果带参数,比如说 new Fn(name,age)
,那么这里的 Fn
就是一个 MemberExpression
。
这两种状况下应用 new
操作符所进行的操作有点点不同,这里拿带参数的状况阐明一下:
- 首先会对
Fn
这个MemberExpression
求值,其后果是指向理论函数对象的一个援用,咱们把这个援用作为ref
- 接着调用
GetValue(ref)
进行求值,失去理论的函数对象,把这个对象作为constructor
- 对
Arguments
也就是传进来的参数求值,失去一个参数列表,作为argList
- 如果
constructor
不是对象,则抛出类型谬误 - 如果
constructor
没有实现外部的[[Constructor]]
办法,也抛出类型谬误 - 调用
constructor
的[[Constructor]]
办法,并将argList
传入作为参数,返回调用后果
从这些形容能够看出,更多的实现细节放在函数的 [[Constructor]]
办法里。那么这个办法具体是做什么用的呢?
[[Constructor]]
的标准
在 JS 中,函数有两种调用形式,一种是失常调用,这将调用函数的外部办法 [[Call]]
,还有一种是通过 new 调用,此时的函数作为一个构造函数,这将调用函数的另一个外部办法 [[Consturct]]
。所以,要实现 new
操作的话,咱们得先搞懂 [[Construct]]
外部办法做了什么事。
这里持续看标准是怎么说的:
简略翻译一下:
当通过可能为空的参数列表调用函数 F
的外部办法 [[Construct]]
的时候,会执行如下步骤:
- 让
obj
作为一个新创建的原生对象 - 依照标准指定的,为
obj
设置所有外部办法 - 将
obj
的外部属性[[Class]]
设置为Object
- 传参
prototype
调用函数F
的外部办法[[Get]]
,获取函数的原型对象,作为proto
- 如果
proto
是对象,则将obj
的外部属性[[Prototype]]
设置为proto
- 如果
proto
不是对象,则将obj
的外部属性[[Prototype]]
设置为规范内建的Object
的原型对象 - 调用函数
F
的外部办法Call
,obj
作为调用时的 this 值,此前传给[[Construct]]
的参数列表作为调用时的参数。将调用后失去的后果作为result
- 如果
result
是对象,则将其返回 - 否则,返回
obj
能够说,标准曾经讲得很分明了,简略地说,在 new 一个构造函数的时候,具体会做上面的事件:
-
外部创立一个实例对象,并指定实例对象的原型:
- 如果构造函数的原型是对象,则让实例的
__proto__
等于构造函数的prototype
- 如果构造函数的原型不是对象,则让实例的
__proto__
等于Object
的prototype
- 如果构造函数的原型是对象,则让实例的
- 将实例对象绑定为构造函数中的 this,此前传递进来的参数作为参数,并执行一遍构造函数
- 如果构造函数返回了对象,则将其作为返回值,否则将实例对象作为返回值
代码实现
ES3 版本的实现如下:
function myNew(Fn){if(typeof Fn != 'function'){throw new TypeError(Fn + 'is not a constructor')
}
myNew.target = Fn
var instance = {}
// 检测构造函数原型是不是对象
instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype
const returnValue = Fn.apply(instance,Array.prototype.slice.call(arguments,1))
if(typeof returnValue === 'object' && returnValue !== null || typeof returnValue === 'function'){return returnValue} else {return instance}
}
ES6 版本的实现如下:
function myNew(Fn,...args){if(typeof Fn != 'function'){throw new TypeError(Fn + 'is not a constructor')
}
myNew.target = Fn
const instance = {}
// 检测构造函数原型是不是对象
instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype
const returnValue = Fn.call(instance,...args)
return returnValue instanceof Object ? returnValue : instance
}
留神几个要点:
1)当函数是通过 new 调用的时候,new.target
会指向函数本身,这个“指向”的操作在代码里就是通过 myNew.target = Fn
体现的
2)为什么不间接应用 const instance = Object.create(Fn.prototype)
创立实例呢?依据标准,咱们在实现 new 的时候,须要检测构造函数的原型是不是对象,如果不是对象,比如说是 null,那么实例的 __proto__
会从新链接到 Object.prototype
,而这里如果应用了 Object.create
,则会导致实例的 __proto__
依然指向 null。网上很多 new
的模仿实现间接应用了 Object.create
,或者基本没有对构造函数的原型进行类型查看,这是不够谨严的(留神,我没有说这是谬误的,只是不够贴近原生 [[Consturct]]
的实现细节)。具体能够浏览我之前写的另一篇文章。
3)如果无奈应用 instanceof
,咱们也能够改用 typeof Fn.prototype === 'Object' && Fn.prototype !== null
进行判断