这是 JS 原生方法原理探究系列的第三篇文章。本文会介绍如何模拟实现 new
操做符。关于 new
的具体用法,MDN 已经描述得很清楚了,这里咱们不说废话,直接讲如何模拟实现。app
注意:下面展现的全部规范都是 ES5 版本的,与如今最新的规范有些区别
首先看一下根据规范的描述, new
操做符作了什么事:函数
全是英文,不过不要紧,我简单翻译一下:post
我在使用 new
操做符的时候,后面跟着的构造函数可能带参数,也可能不带参数,若是不带参数的话,好比说 new Fn()
,那么这里这个 Fn
就是一个 NewExpression
;若是带参数,好比说 new Fn(name,age)
,那么这里的 Fn
就是一个 MemberExpression
。this
这两种状况下使用 new
操做符所进行的操做有点点不一样,这里拿带参数的状况说明一下:spa
Fn
这个 MemberExpression
求值,其结果是指向实际函数对象的一个引用,咱们把这个引用做为 ref
GetValue(ref)
进行求值,获得实际的函数对象,把这个对象做为 constructor
Arguments
也就是传进来的参数求值,获得一个参数列表,做为 argList
constructor
不是对象,则抛出类型错误constructor
没有实现内部的 [[Constructor]]
方法,也抛出类型错误constructor
的 [[Constructor]]
方法,并将 argList
传入做为参数,返回调用结果从这些描述能够看出,更多的实现细节放在函数的 [[Constructor]]
方法里。那么这个方法具体是作什么用的呢?prototype
[[Constructor]]
的规范在 JS 中,函数有两种调用方式,一种是正常调用,这将调用函数的内部方法 [[Call]]
,还有一种是经过 new 调用,此时的函数做为一个构造函数,这将调用函数的另外一个内部方法 [[Consturct]]
。因此,要实现 new
操做的话,咱们得先搞懂 [[Construct]]
内部方法作了什么事。翻译
这里继续看规范是怎么说的:code
简单翻译一下:对象
当经过可能为空的参数列表调用函数 F
的内部方法 [[Construct]]
的时候,会执行以下步骤:rem
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
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
进行判断