实现 new 操作符

随笔,前端基础 2019-10-21 279 次浏览 次点赞

实现 _new,达到如下效果

function A(){ this.name = "test" }
var a = _new(A)
a.name //test

what is new

new 运算符是一个左值表达式,

NewExpression :
MemberExpression
new NewExpression

MemberExpression :
PrimaryExpression
FunctionExpression
MemberExpression [ Expression ]
MemberExpression . IdentifierName
new MemberExpression Arguments

Arguments :
( )
( ArgumentList )

我们以

function A(){
  this.name = "test"
}

为例,

new A() // new MemberExpression Arguments, MemberExpression =>FunctionExpression
new A // new NewExpression, NewExpression =>MemberExpression =>FunctionExpression
new new A // 按上面语法描述,这里是可以这么写的,且不会报语法错误,仅是报了 TypeError
new A.name // TypeError 错误。new NewExpression, NewExpression=> MemberExpression =>MemberExpression . IdentifierName 。

前两种写法均可,在有传参的时候只能使用第一种,同时注意语法解释过程,第四种写法语法解析完变成 new MemberExpression . IdentifierName,即 new "A", 导致异常

我们以无参数调用 new NewExpression ,分析 new A 的执行过程:

  1. 令 ref 为解释执行 NewExpression 的结果 . 这里 ref = A
  2. 令 constructor 为 GetValue(ref). 这里 constructor = A
  3. 如果 Type(constructor) 不是对象 ,抛出 TypeError 异常 . 这里 Type(constructor) 为 对象
  4. 如果 constructor 没有实现 [[Construct]] 内置方法 ,抛出一个 TypeError 异常 . 该函数对象会设定 [[Construct]] 内部属性。
  5. 返回调用 constructor 的 [[Construct]] 内置方法的结果 , 不传入任何参数 ( 就是一个空的 arguments 列表 ).

先讲下规范,调用函数对象 F 的 [[Construct]] 内部方法时,执行过程如下:

  1. 令 obj 为新创建的 ECMAScript 原生对象。// 本规范定义的对象为原生对象,宿主环境定义的( 如 window, document) 为宿主对象,两者互补形成对象集合。
  2. 依照 8.12 设定 obj 的所有内部方法。
  3. 设定 obj 的 [[Class]] 内部方法为 "Object"。
  4. 设定 obj 的 [[Extensible]] 内部方法为 true。
  5. 令 proto 为以参数 "prototype" 调用 F 的 [[Get]] 内部属性的值。
  6. 如果 Type(proto) 是 Object,设定 obj 的 [[Prototype]] 内部属性为 proto。
  7. 如果 Type(proto) 不是 Object,设定 obj 的 [[Prototype]] 内部属性为 15.2.4 描述的标准内置的 Object 的 prototype 对象。
  8. 以 obj 为 this 值,[[Construct]] 的参数列表为 args,去调用 F 的 [[Call]] 内部属性,令 result 为调用结果。
  9. 如果 Type(result) 是 Object,则返回 result。
  10. 返回 obj

函数对象 A 按上面的规范执行,其执行过程如下:

  1. obj = {}
  2. 设定内部方法
  3. 设定 [[Class]] = "Object",使得 Object.prototype.toString.call(obj) = "[object Object]"
  4. 设定 [[Extensible]] = true ,允许 obj 添加属性
  5. 令 proto = A.prototype
  6. Object.setPrototypeOf(obj, proto)
  7. 相当于 result = F.call(obj,...args)
  8. Type(result) 为 undefined
  9. 返回 obj

实现

快速实现

根据以上描述,我们可以很快的写出如下代码(不考虑异常情况): 其中很多地方采用的新语法,当然你可以选择用 polyfill 代替

function _new (F, ...args) {
  var obj = Object.create(F.prototype); // 相当于 ({}).__proto__ = F.prototype
  var result = F.call(obj, ...args)
  return typeof result === "object" ? result : obj
}

Type(result) 判断

接着我们考虑 如果 Type(result) 是 Object 这个判断,

ECMAScript 语言类型包括 未定义 (Undefined)、 空值 (Null)、 布尔值(Boolean)、 字符串 (String)、 数值 (Number)、 对象 (Object)

注意1,这里 Type(Null) 为 Null,不是 Object。而 js 中 typeof null ==="object" 注意2,这里 Type(function(){}) 为 Object。而 js 中 typeof function(){}==="function"

因此 Type(result) 的实现应该为

(typeof result === 'object' && result !== null ) || typeof result === 'function' 

function _new (F, ...args) {
  var obj = Object.create(F.prototype); // 相当于 ({}).__proto__ = F.prototype
  var result = F.call(obj, ...args)
  var isESObject = (typeof result === 'object' && result !== null ) || typeof result === 'function' 
  return isESObject  ? result : obj
}

构造函数判断

接着考虑异常情况:

  • 如果 Type(constructor) 不是 Object ,抛出一个 TypeError 异常 . 结合后面的要求 constructor 实现 [[Construct]] 内置方法,constructor 只能是 Type Object 中的 function
    var isFunction = typeof constructor === 'function' 
    if(!isFunction){
    throw TypeError(`${constructor} is not a constructor`)
    }
  • 如果 constructor 没有实现 [[Construct]] 内置方法 ,抛出一个 TypeError 异常

我们在外部难以实现 [[Construct]] 构造与否的判断,因此只能根据规律来总结。

  1. 特定函数不是构造方法

    除非特别说明,es6+ 实现的特定函数都没有实现 [[Construct]] 内置方法 简单的说,特定函数设计之初肯定不是为了用来构造的

    var A  ={
    g:function* (){},
    arrow:()=>{},
    shorthand(){},
    cs:function(){}
    }
    new A.g // TypeError
    new A.arrow // TypeError
    new A.shorthand // TypeError
    new A.cs // cs {}

对所有方法的 prototype 进行输出,发现

A.g.prototype // Generator {}
A.arrow.prototype // undefined
A.shorthand.prototype // undefined
A.cs.prototype // {constructor: ƒ}

发现构造函数满足该条件

function is_constructor(f){
  return !!f && f.hasOwnProperty("prototype") && f.prototype.hasOwnProperty("constructor")
}
  1. 内置函数不是构造方法
    new Math.max //  TypeError
    new String.prototype.indexOf    //  TypeError

    内置函数无 prototype,因此共用上面的判断逻辑即可

值得注意的是还有一个 Symbol

其不能使用 new 实例化。但是 Symbol.prototype.hasOwnProperty("constructor") is true

综合判断如下:

function is_constructor(f){
  if (f === Symbol) return false;
  return !!f && f.hasOwnProperty("prototype") && f.prototype.hasOwnProperty("constructor")
}

// true
is_constructor(function(){});
is_constructor(class A {});
is_constructor(Array);
is_constructor(Function);
is_constructor(new Function);

// false
is_constructor();
is_constructor(undefined);
is_constructor(null);
is_constructor(1);
is_constructor(new Number(1));
is_constructor(Array.prototype);
is_constructor(Function.prototype);
is_constructor(() => {})
is_constructor({method() {}}.method)
is_constructor(Symbol)
is_constructor(Math.max)
is_constructor(String.prototype.indexOf)

但是处理不了手动修改 constructor 值的做法

var a = ()=>{}
a.prototype = {constructor:1}
new a() // TypeError
_new(a) // {}

属性都是可以随意设置的,因此判断属性存在与否是不靠谱的,网上继续搜索, stackoverflow 上看到有人用 实例化-捕获异常的方式判断一个函数是否为构造函数

function is_constructor(f) {
  // 特殊判断,Symbol 能通过检测
  if (f === Symbol) return false;
  try {
    Reflect.construct(String, [], f);
  } catch (e) {
    return false;
  }
  return true;
}

// true
is_constructor(function(){});
is_constructor(class A {});
is_constructor(Array);
is_constructor(Function);
is_constructor(new Function);

// false
is_constructor();
is_constructor(undefined);
is_constructor(null);
is_constructor(1);
is_constructor(new Number(1));
is_constructor(Array.prototype);
is_constructor(Function.prototype);
is_constructor(() => {})
is_constructor({method() {}}.method)
is_constructor(Symbol)
is_constructor(Math.max)
is_constructor(String.prototype.indexOf)

看到这,你一定在想 Reflect.construct(String, [], f); 和直接 new f 捕获异常有什么差别

我们先拿个例子运行下

function A(){
  console.log("hh")
}
new A() // 输出hh 返回 A {}
Reflect.construct(A,[]) // 输出hh 返回 A {}
var a = Reflect.construct(String, [], A) // 不输出,a =  A {""}
a instanceof A // true
a.toString() // [object String]

MDN Reflect.construct 上面的的解释

Reflect.construct(target, argumentsList[, newTarget]) 用给定的 argumentsList 参数列表初始化 target 构造函数,返回一个 target 或 newTarget (如果存在) 的实例。如果 target 或 newTarget 不是构造函数,抛出 TypeError

因此,当 f 不是构造函数时,抛出错误;当 f 是构造函数时,也不会执行 f 构造函数导致造成影响

最终实现

function _new (F, ...args) {
  function is_constructor (f) {
    // 特殊判断,Symbol 能通过检测
    if (f === Symbol) return false;
    try {
      Reflect.construct(String, [], f);
    } catch (e) {
      return false;
    }
    return true;
  }
  var isFunction = typeof F === 'function'
  if (!isFunction || !is_constructor(F)) {
    throw TypeError(`${F.name||F} is not a constructor`)
  }
  var obj = Object.create(F.prototype); // 相当于 ({}).__proto__ = F.prototype
  var result = F.call(obj, ...args)
  var isESObject = (typeof result === 'object' && result !== null) || typeof result === 'function'
  return isESObject ? result : obj
}

鄙人水平不足,有些知识点可能遗漏或理解错误,欢迎指正~

拓展阅读

  1. 面试官问:能否模拟实现JS的new操作符

测试用例

  • 基础用例
    function A(name){
    this.name = name
    }
    new A("test") // A {name: "test"}
    _new (A,"test") // A {name: "test"}
  • 判断 null
    function A(name){
    this.name = name
    return null
    }
    new A("test") // {name: "test"}
    _new (A,"test") // {name: "test"}
  • 判断 function
    function A(name){
    this.name = name
    return ()=>{}
    }
    new A("test") // ()=>{}
    _new (A,"test") // ()=>{}
  • 判断异常
    var A = ()=>{}
    new A() // Uncaught TypeError: A is not a constructor
    _new (A) // Uncaught TypeError: A is not a constructor

拓展

在模拟 new 的基础上,模拟 es6 的 new.target 属性

这里模拟的 new.target 是用于构造函数中,举例:

function Person(name) {
  if (_new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}
// 预期输出
Person("test") // Uncaught Error: 必须使用 new 命令生成实例
_new(Persion,"test") // Person {name:'test'}

因此我们可以在 F.call(obj, ...args) 前给 _new.target 赋值,在调用完后删除该属性,即

function _new (F, ...args) {
  function is_constructor (f) {
    // 特殊判断,Symbol 能通过检测
    if (f === Symbol) return false;
    try {
      Reflect.construct(String, [], f);
    } catch (e) {
      return false;
    }
    return true;
  }
  var isFunction = typeof F === 'function'
  if (!isFunction || !is_constructor(F)) {
    throw TypeError(`${F.name||F} is not a constructor`)
  }
  _new.target = F
  var obj = Object.create(F.prototype); // 相当于 ({}).__proto__ = F.prototype
  var result = F.call(obj, ...args)
  var isESObject = (typeof result === 'object' && result !== null) || typeof result === 'function'
  delete _new.target
  return isESObject ? result : obj
}
// 测试用例
Person("test") // Uncaught Error: 必须使用 new 命令生成实例
_new(Person,"test") // Person {name: "test"}
Person("test") // Uncaught Error: 必须使用 new 命令生成实例

本文由 GaHingZ 创作,采用 署名-非商业性使用-相同方式共享 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!