手写源码4--实现一个new运算符

254 阅读5分钟

new一个构造函数发生了什么

我们大概都知道通过new运算符去调用一个构造函数,在构造函数内部完成了这些操作:

  • 创建了一个新的空对象{}
  • 链接该对象(设置该对象的constructor)到另一个对象
  • 将该对象作为构造函数this的上下文,并执行构造函数中的代码
  • 将这个新对象返回

“链接该对象(设置该对象的constructor)到另一个对象”有些拗口,不太好理解,先看看怎么使用new运算符的:

// 构造函数
function Person(name,age){
    this.name=name;
    this.age=age;
}
Person.prototype.sayName=function(){
    console.log(this.name)
}
// 通过new运算符实例化一个对象
const p=new Person('zhangsan',20)
console.log(p) // {name: "zhangsan", age: 20}
p.sayName() // zhangsan

可以发现返回的对象p调用到了sayName这个方法,但sayName这个方法在构造函数内部是不存在的,而是挂在构造函数的原型上的,所以在new的过程中,将空对象的__proto__属性指向了构造函数的prototype属性。

实现一个简单的new

现在将前面new一个构造函数的过程翻译成代码,基本就实现了一个new运算符:

function myNew(func,...args){
    // 1,创建一个空对象
    let obj={};
    // 2,将构造函数的原型赋值给新对象
    // obj.__proto__=func.prototype //(查资料说访问__proto__有性能问题)
    Object.setPrototypeOf(obj,func.prototype);
    // 3,将构造函数的this指向新创建的对象,并执行构造函数中的代码
    let result=func.apply(obj,args);
    // 一般情况下声明构造函数是很少在它内部写一个return的,但万一呢
    // 如果构造函数本身return 了一个对象,那就return出去构造函数内return的对象
    // 如果构造函数内部return了基本类型值、null,new的时候不会受影响该咋样还咋样
    // 如果构造函数本身没有return对象,那就将创建的这个新对象返回出去
    return result instanceof Object?result:obj
}  

不是所有的函数都能new

但是有一些函数是不能通过new运算符来作为构造函数调用的,如:

  • 箭头函数 ()=>{}
  • generator函数 function *a(){}
  • Symbol ()
  • 对象简写的方法{method(){}}.method

所以需要对传入的参数进行一个判断,首先要确保传入的参数是一个函数,而我们知道每个函数都有一个prototype属性,指向函数的原型对象,而这个原型对象又有一个constructor属性,指向prototype属性所在的函数。所以传入的参数func能不能实例化,只需要检测func是不是一个函数、有没有prototype属性,prototype上的constructor是否指向func即可。

function myNew(func,...args){
    // 1,判断传入的参数能否作为构造函数
    // 从左往右计算,如果存在constructor属性func.prototype.constructor会返回
    let isConstructor= (func!=null && typeof func === "function" && func.prototype && func.prototype.constructor)===func;
    if(!isConstructor){
        throw new Error(`${func} has no constructor`)
    }
    // 2,创建一个空对象
    let obj={};
    // 3,将构造函数的原型赋值给新对象
    // obj.__proto__=func.prototype //(查资料说访问__proto__有性能问题)
    Object.setPrototypeOf(obj,func.prototype);
    // 4,将构造函数的this指向新创建的对象,并执行构造函数中的代码
    let result=func.apply(obj,args);
    // 一般情况下声明构造函数是很少在它内部写一个return的,但万一呢
    // 如果构造函数本身return 了一个对象,那就return出去构造函数内return的对象
    // 如果构造函数内部return了基本类型值、null,new的时候不会受影响该咋样还咋样
    // 如果构造函数本身没有return对象,那就将创建的这个新对象返回出去
    return result instanceof Object?result:obj
}  

经过测试发现,除了Symbol之外传入其他类型的参数都能达成预期效果。传入的参数是Symbol时,返回结果显示可以实例化,但期望是不能实例化,原因是Symbol也像其他普通函数一样有prototype属性,它的prototype.constructor指向自己,所以针对Symbol需要特殊处理:

function myNew(func,...args){
    // 1,判断传入的参数能否作为构造函数
    // Symbol需要特殊处理
    if(func===Symbol){
        return false;
    }
    // 从左往右计算,如果存在constructor属性func.prototype.constructor会返回
    let isConstructor= (func!=null && typeof func === "function" && func.prototype && func.prototype.constructor)===func;
    if(!isConstructor){
        throw new Error(`${func} has no constructor`)
    }
    // 2,创建一个空对象
    let obj={};
    // 3,将构造函数的原型赋值给新对象
    // obj.__proto__=func.prototype //(查资料说访问__proto__有性能问题)
    Object.setPrototypeOf(obj,func.prototype);
    // 4,将构造函数的this指向新创建的对象,并执行构造函数中的代码
    let result=func.apply(obj,args);
    // 一般情况下声明构造函数是很少在它内部写一个return的,但万一呢
    // 如果构造函数本身return 了一个对象,那就return出去构造函数内return的对象
    // 如果构造函数内部return了基本类型值、null,new的时候不会受影响该咋样还咋样
    // 如果构造函数本身没有return对象,那就将创建的这个新对象返回出去
    return result instanceof Object?result:obj
}

Reflect.construct与constructor

另外也发现如果传入的参数是generator函数function *a(){},它原型其实是有constructor属性的,只不过指向了GeneratorFunction并不是指向它自己,勉强可以达到不能new一个generator函数的效果。但最终通过func.prototype.constructor===func来判断能否实例化func的最大问题是、如果func.prototype.constructor的指向是经过了修改的,则该判断方式就不适用了。

查阅资料发现可以通过Reflect.construct(target, argumentsList,newTarget)这个API来判断,原理是利用传入的参数target或者newTarget不是构造函数会抛出异常这一特性,这个API的作用本身就相当于new运算符,也能实例化一个对象:

function OneClass() {
    this.name = 'one';
}
function OtherClass() {
    this.name = 'other';
}
// 实例的原型默认指向传入的第一个参数的原型
var obj1 = Reflect.construct(OneClass,[]);
console.log(obj1) // {name:'one'}
console.log(obj1.__proto__===OneClass.prototype) // true

// 传入第三个参数,实例的原型会指向第三个参数的原型
var obj2 = Reflect.construct(OneClass, [], OtherClass);
console.log(obj2)  // {name:'one'}
console.log(obj2.__proto__===OtherClass.prototype) // true

Reflect.construct传入的第一个参数会像new运算符调用一个构造函数时、构造函数会执行内部的代码,这里是在探讨new运算符的内部实现,所以就不能用Reflect.construct(func,[])来判断传入到myNew中的参数func是不是构造函数,改为Reflect.construct(String,[],func)即可,这样在判断func是不是构造函数的过程中,它内部的代码不会去执行。

最后的代码如下:

function myNew(func,...args){
    // 1,判断传入的参数能否作为构造函数
    if(typeof func !=='function'){
        return;
    }
    // Symbol需要特殊处理
    if(func===Symbol){
        return
    }
    // 检测传入的函数是否为一个构造函数
    let isConstructor=true;
    try{
        Reflect.construct(String, [], func);
    } catch(err){
        isConstructor=false;
    }
    if(!isConstructor){
        throw new TypeError(`${func} is not a constructor`)
    }
    // 2,创建一个空对象
    let obj={};
    // 3,将构造函数的原型赋值给新对象
    // obj.__proto__=func.prototype //(查资料说访问__proto__有性能问题)
    Object.setPrototypeOf(obj,func.prototype);
    // 4,将构造函数的this指向新创建的对象,并执行构造函数中的代码
    let result=func.apply(obj,args);
    // 一般情况下声明构造函数是很少在它内部写一个return的,但万一呢
    // 如果构造函数本身return 了一个对象,那就return出去构造函数内return的对象
    // 如果构造函数内部return了基本类型值、null,new的时候不会受影响该咋样还咋样
    // 如果构造函数本身没有return对象,那就将创建的这个新对象返回出去
    return result instanceof Object?result:obj
}  

参考资料:

How to check if a Javascript function is a constructor

Reflect.construct