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
}
参考资料: