JS 原生方法原理探究(三):从规范解读如何实现 new 操做符

这是 JS 原生方法原理探究系列的第三篇文章。本文会介绍如何模拟实现 new 操做符。关于 new 的具体用法,MDN 已经描述得很清楚了,这里咱们不说废话,直接讲如何模拟实现。app

new 操做符的规范

注意:下面展现的全部规范都是 ES5 版本的,与如今最新的规范有些区别

首先看一下根据规范的描述, new 操做符作了什么事:函数

全是英文,不过不要紧,我简单翻译一下:post

我在使用 new 操做符的时候,后面跟着的构造函数可能带参数,也可能不带参数,若是不带参数的话,好比说 new Fn(),那么这里这个 Fn 就是一个 NewExpression;若是带参数,好比说 new Fn(name,age),那么这里的 Fn 就是一个 MemberExpressionthis

这两种状况下使用 new 操做符所进行的操做有点点不一样,这里拿带参数的状况说明一下:spa

  1. 首先会对 Fn 这个 MemberExpression 求值,其结果是指向实际函数对象的一个引用,咱们把这个引用做为 ref
  2. 接着调用 GetValue(ref) 进行求值,获得实际的函数对象,把这个对象做为 constructor
  3. Arguments 也就是传进来的参数求值,获得一个参数列表,做为 argList
  4. 若是 constructor 不是对象,则抛出类型错误
  5. 若是 constructor 没有实现内部的 [[Constructor]] 方法,也抛出类型错误
  6. 调用 constructor[[Constructor]]方法,并将 argList 传入做为参数,返回调用结果

从这些描述能够看出,更多的实现细节放在函数的 [[Constructor]] 方法里。那么这个方法具体是作什么用的呢?prototype

[[Constructor]] 的规范

在 JS 中,函数有两种调用方式,一种是正常调用,这将调用函数的内部方法 [[Call]],还有一种是经过 new 调用,此时的函数做为一个构造函数,这将调用函数的另外一个内部方法 [[Consturct]]。因此,要实现 new 操做的话,咱们得先搞懂 [[Construct]] 内部方法作了什么事。翻译

这里继续看规范是怎么说的:code

简单翻译一下:对象

当经过可能为空的参数列表调用函数 F 的内部方法 [[Construct]] 的时候,会执行以下步骤:rem

  1. obj 做为一个新建立的原生对象
  2. 按照规范指定的,为 obj 设置全部内部方法
  3. obj 的内部属性 [[Class]] 设置为 Object
  4. 传参 prototype 调用函数 F 的内部方法 [[Get]],获取函数的原型对象,做为 proto
  5. 若是 proto 是对象,则将 obj 的内部属性 [[Prototype]] 设置为 proto
  6. 若是 proto 不是对象,则将 obj 的内部属性 [[Prototype]] 设置为标准内建的 Object 的原型对象
  7. 调用函数 F 的内部方法 Callobj 做为调用时的 this 值,此前传给 [[Construct]] 的参数列表做为调用时的参数。将调用后获得的结果做为 result
  8. 若是 result 是对象,则将其返回
  9. 不然,返回 obj

能够说,规范已经讲得很清楚了,简单地说,在 new 一个构造函数的时候,具体会作下面的事情:

  • 内部建立一个实例对象,并指定实例对象的原型:

    • 若是构造函数的原型是对象,则让实例的 __proto__ 等于构造函数的 prototype
    • 若是构造函数的原型不是对象,则让实例的 __proto__ 等于 Objectprototype
  • 将实例对象绑定为构造函数中的 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 进行判断

相关文章
相关标签/搜索