先记住这个答案
用 new 调用函数时,引擎会做四步。第一步,创建一个新的空对象。第二步,把这个对象的原型(也就是 __proto__)指向构造函数的 prototype。第三步,把这个对象当作 this 去执行构造函数,构造函数里给 this 加的属性都加到了这个对象上。第四步,如果构造函数自己返回了一个对象,new 的结果就是那个对象;如果没写 return 或者返回的是数字、字符串这类原始值,结果就是第一步创建的新对象。
- 实例的原型指向构造函数的
prototype - 构造函数里的
this是新建的实例 - 返回对象会替换实例,返回原始值被忽略
- 箭头函数和对象方法简写不能
new
手写一个 new
面试里这道题的标准动作是手写 myNew,四步正好对应四行代码。
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype) // 1、2:建对象并连上原型
const result = Ctor.apply(obj, args) // 3:以 obj 为 this 执行
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function')
return isObject ? result : obj // 4:返回了对象就用它,否则用 obj
}
function User(name) {
this.name = name
}
User.prototype.greet = function () {
return `Hi ${this.name}`
}
function Weird() {
this.a = 1
return { b: 2 } // 返回了一个对象
}
const u = myNew(User, 'Ada')
console.log(u.greet(), u instanceof User)
console.log(JSON.stringify(new Weird()), JSON.stringify(myNew(Weird)))查看输出与解释
Hi Ada true
{"b":2} {"b":2}Object.create(Ctor.prototype) 一步完成了「建对象」和「连原型」,所以 u 能调用原型上的 greet,instanceof 也成立。Ctor.apply(obj, args) 把新对象作为 this 传进去,this.name = name 就写到了 u 上。
Weird 演示了第四步的例外:它返回了 { b: 2 },原生 new 和手写版都丢掉了带 a 的那个实例,直接用返回的对象。判断条件要注意 null,typeof null 是 'object',但返回 null 时 new 仍然返回新建的实例。
this 在构造调用里为什么优先级最高
普通函数的 this 看怎么调用,new 是其中优先级最高的一种。哪怕函数已经用 bind 绑死了 this,用 new 调用时绑定的 this 也会被忽略,this 仍然是新创建的实例,只有 bind 预置的参数还保留。
- 忘了写
new会怎样 - 构造函数就是个普通函数,不写
new也能调用,这时它按普通调用处理。非严格模式下this是全局对象,this.name = name会悄悄创建一个全局变量;严格模式下this是undefined,给它赋属性直接抛TypeError。ES6 的class不允许这种情况,不写new调用会直接抛TypeError。 - 哪些函数不能
new - 只有带「构造能力」的函数才能
new。普通函数声明、函数表达式和class可以;箭头函数、对象方法简写、async函数和生成器函数都不行,new它们会抛TypeError。简单的判断方法是看它有没有prototype属性,箭头函数和方法简写都没有。生成器函数是个例外,它有prototype,但照样不能new。
容易答错的地方
- 以为
prototype上的方法会复制到每个实例 - 实例上只有构造函数里用
this.xxx加的属性,原型上的方法是通过原型链找到的,所有实例共用同一个函数。这也是方法要写在prototype上而不是构造函数里的原因,写在构造函数里每个实例都会各生成一份。 - 手写
new时只判断typeof result === 'object' - 这样会漏掉两个情况:返回
null时会错误地返回null,返回函数时又会错误地忽略它。原生new的规则是「返回值是对象或函数就用它」,null不算对象。
面试官还会怎么问?
Object.create 和 new 有什么区别?
Object.create(proto) 只做「建对象、连原型」这一步,不执行任何构造函数。new 在此基础上还会执行构造函数、处理返回值。所以手写 new 时可以用 Object.create 完成前两步。
如果 Foo.prototype 被改成了一个原始值会怎样?
新对象的原型会退回到 Object.prototype,因为原型只能是对象或 null。这时实例 instanceof Foo 的判断还会直接抛 TypeError,因为 instanceof 要求右边的 prototype 是对象。
怎么让函数被直接调用时也自动 new?
在函数开头判断 new.target:普通调用时它是 undefined,这时 return new Foo(...arguments) 就行。以前没有 new.target 时用 this instanceof Foo 判断,但它能被 Foo.call(实例) 骗过去,不如 new.target 准确。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。