Javascript篇 | new调用原理
前言
在前面的章节里,我们从 this 指向、闭包到防抖节流,一路拆解了 JavaScript 的核心机制。从本章开始,正式进入 JavaScript 面试里另一条非常高频的主线:对象创建与 new 调用。
new 一直是很多同学“用得多、理解少”的语法糖:
- 知道
new Foo()能创建一个对象,却不清楚背后到底发生了什么 - 只会机械记忆“
__proto__指向 prototype”,说不清执行上下文里做了哪些工作 - 手写
new时,容易漏掉边界情况
但在一线大厂的面试中,new 往往和:
- 原型链 / 继承
call / apply / bind- 类与构造函数
instanceof判定
一起出现在同一组题里。如果你能讲清楚 “new 做了哪四步 + 为什么要这么做”,往往能在这一块拿到很高的评价。
本章,我们就从一段最常见的代码开始,彻底搞懂 new 的调用原理。
诊断自测
在正式开始之前,先用几道小题摸一下自己的底——不用紧张,答错了才说明这章对你最有价值。
Q1:下面代码输出什么?为什么?
function Dog(name) {
this.name = name;
return { name: 'Hacked' };
}
const d = new Dog('Buddy');
console.log(d.name);
console.log(d instanceof Dog);
点击查看答案
输出 'Hacked' 和 false。因为构造函数显式返回了一个普通对象,new 会优先使用这个返回的对象作为结果,而不是内部创建的那个。由于返回的对象和 Dog.prototype 没有原型关系,所以 instanceof 为 false。
Q2:不用 new,直接调用 Dog('Buddy') 会怎样?
点击查看答案
在非严格模式下,this 会指向全局对象(浏览器里是 window),所以 name 会被挂到全局上,变成 window.name = 'Buddy'。在严格模式下,this 是 undefined,会直接报 TypeError。这也是为什么 new 调用和普通调用的区别很关键。
Q3:Object.create(F.prototype) 和 {} + Object.setPrototypeOf 有什么区别?手写 new 时用哪个更好?
点击查看答案
Object.create(F.prototype) 一步到位:创建一个空对象并把它的 [[Prototype]] 指向 F.prototype。而 {} + Object.setPrototypeOf 是先创建再修改原型链。功能上等价,但 Object.create 更简洁,且 MDN 和规范都建议避免在对象创建后再修改原型(性能开销更大)。手写 new 时推荐用 Object.create。
function Person(name) {
this.name = name;
}
const p = new Person('Alice');
这短短一行 new Person('Alice'),引擎到底帮我们做了什么?
一、从效果出发:我们期望 new 帮我们做什么?
先不急着上规范,先从“使用者视角”看 new:
- 能创建一个全新的对象
- 这个新对象上能访问到构造函数里挂到
this上的属性 - 新对象的原型上,能访问到构造函数原型上的方法
instanceof判定正常:p instanceof Person === true
也就是说,一次 new 调用,至少要帮我们完成三件事:
- 创建一个对象
- 把对象和构造函数通过原型链“连起来”
- 在构造函数内部用这个对象作为
this执行初始化逻辑
规范里是怎么描述这一过程的?
二、规范视角:构造调用做了哪几步?
在 ECMAScript 规范中,new F(...args) 触发的是 构造函数调用(constructor call),大致可以拆成四步(简化后便于记忆):
- 创建一个空对象
- 把空对象的内部原型
[[Prototype]]指向构造函数的prototype - 以这个对象为
this,执行构造函数 - 根据返回值决定最终结果
用伪代码表达:
function New(F, ...args) {
// 1. 创建一个空对象
const obj = {};
// 2. 关联原型链:obj.__proto__ -> F.prototype
Object.setPrototypeOf(obj, F.prototype); // 或 obj.__proto__ = F.prototype;
// 3. 以 obj 作为 this 执行构造函数
const result = F.apply(obj, args);
// 4. 按规范决定返回什么
// - 如果构造函数显式返回一个“对象”,就用它
// - 否则返回我们创建的 obj
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');
return isObject ? result : obj;
}
关键有三点:
- 原型绑定:
obj.[[Prototype]] = F.prototype - this 绑定:构造函数里的
this指向新对象obj - 返回值规则:显式返回对象优先,否则用默认创建的对象
接下来,我们把这四步逐一展开。
三、第一步:创建一个全新的对象
最朴素的方式:
const obj = {};
但规范层面,实际上做的是:
- 创建一个内部类型为
Object的新值 - 给它挂上一些内部字段(比如
[[Extensible]]等)
在面试语境下,你不需要把所有内部细节都背下来,只需要清楚:
第一步就是为了拿到一个“干净的空壳”,后面的所有操作都是围绕这个壳来完成的。
四、第二步:把原型链“接”上去
要让 p instanceof Person 为真,必须保证:
p.__proto__ === Person.prototype;
// 或者:Object.getPrototypeOf(p) === Person.prototype;
而 instanceof 做的事情,本质上就是:
沿着
p的原型链一层一层找,看有没有某一层 ===Person.prototype。
所以在 new 的第二步里,必须把这条链接好:
Object.setPrototypeOf(obj, F.prototype);
// 或者老写法:obj.__proto__ = F.prototype;
这一小步,是很多手写 new 的答案里容易漏掉或写错的关键。
五、第三步:以新对象为 this 执行构造函数
现在我们有了一个 obj,并且 obj.__proto__ 已经指向了 F.prototype。接下来就轮到构造函数本身:
const result = F.apply(obj, args);
这里有两个知识点被同时用上:
- 利用
apply/call显式绑定 this,让构造函数体内的this指向我们的新对象 - 利用
args展开传参,保证调用语义和new F(...args)一致
这一步执行完之后,构造函数体内的逻辑,比如:
function Person(name) {
this.name = name;
this.sayHi = function () {
console.log('Hi, I am ' + this.name);
};
}
就会把属性挂到 obj 上:
obj.name = name;
obj.sayHi = function () { ... };
六、第四步:返回规则——构造函数可以“抢戏”
很多同学以为 new 调用的返回值一定是那个新对象,但其实并不是。
规范里对构造函数返回值的处理是这样的:
- 如果构造函数 显式返回一个“对象类型”(对象 / 函数),那么
new表达式的结果就是这个返回值 - 否则(返回原始值或没有返回),就使用第一步创建出来的对象
举例:
function Foo() {
this.a = 1;
return { a: 2 };
}
function Bar() {
this.a = 1;
return 123;
}
const foo = new Foo();
const bar = new Bar();
console.log(foo.a); // 2 —— 使用了显式返回的对象
console.log(bar.a); // 1 —— 使用默认创建的对象
这也是为什么在我们手写 New 的时候,需要这段判断:
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');
return isObject ? result : obj;
面试时,如果你能把这条规则讲清楚,基本可以直接拉开和“只会背四步”的同学之间的差距。
七、手写一个简化版 new
把上面的四步整合起来,我们可以手写一个 myNew:
function myNew(F, ...args) {
// 1. 创建一个空对象
const obj = {};
// 2. 把原型链接上:obj.__proto__ -> F.prototype
Object.setPrototypeOf(obj, F.prototype);
// 3. 以 obj 为 this 执行构造函数
const result = F.apply(obj, args);
// 4. 返回规则:对象优先,否则返回 obj
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');
return isObject ? result : obj;
}
// 使用
function Person(name) {
this.name = name;
}
const p1 = myNew(Person, 'Alice');
console.log(p1.name); // 'Alice'
console.log(p1 instanceof Person); // true
如果你在白板/纸上能写出这个版本,并把每一步都对上前面讲过的四个阶段,基本就已经达到“可在大部分面试场景下自信讲解”的程度。
八、和 class 的关系:class 只是语法糖
很多同学在 ES6 之后更多使用 class:
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log('Hi, I am ' + this.name);
}
}
const p = new Person('Alice');
class 看起来和传统构造函数写法不太一样,但规范上它本质上仍然是一个“构造函数 + 原型”的包装:
console.log(typeof Person); // 'function'
console.log(Person === Person.prototype.constructor); // true
也就是说:
new Person()的底层调用流程,仍然是我们刚刚拆解的那四步- 区别只是:
constructor写在class体内- 原型方法写成
sayHi() {}这种更干净的语法
理解了 new 的原理之后,你就会发现:
不论是“传统构造函数 + prototype”,还是
class,它们只是语法层的差异,底层都是同一套“new 调用协议”。
常见误区
学完 new 的原理之后,下面这几个常见的”想当然”一定要纠正过来,面试里踩一个就可能扣分。
误区一:构造函数必须大写开头才能用 new
大写开头只是命名约定,不是语法要求。JavaScript 引擎不会因为你的函数名是小写就阻止 new 调用:
function animal(type) {
this.type = type;
}
const a = new animal('cat');
console.log(a.type); // 'cat' —— 完全正常
大写只是给开发者看的信号:“这个函数设计成构造函数使用”。不大写不报错,但会让同事困惑。
误区二:class 和传统构造函数的 new 行为完全不同
很多同学觉得 class 是一种全新的机制。其实不是——class 的 new 流程和传统构造函数走的是同一套规范。真正的区别在于:
class必须通过new调用,直接调用会报TypeError- 传统构造函数可以被直接调用(虽然结果通常不是你想要的)
class Car {
constructor(brand) { this.brand = brand; }
}
Car('Tesla'); // TypeError: Class constructor Car cannot be invoked without 'new'
误区三:new 出来的对象一定是构造函数创建的那个
前面第六节已经讲了——如果构造函数显式 return 一个对象,new 的结果就是那个返回的对象,而不是内部创建的。但很多同学在面试手写代码时还是会忘掉这一点:
function Tricky() {
this.value = 'original';
return function() { return 'surprise'; };
}
const t = new Tricky();
console.log(t.value); // undefined —— t 是返回的那个函数
console.log(t()); // 'surprise'
函数也是对象,所以 return function... 同样触发”对象优先”规则。
误区四:箭头函数也能被 new 调用
箭头函数没有自己的 this,也没有 prototype 属性,规范明确规定它不可被构造调用:
const Nope = (x) => { this.x = x; };
new Nope(1); // TypeError: Nope is not a constructor
面试里问到”哪些函数不能 new”,至少要答出箭头函数。如果能再补上 Symbol、BigInt 等内建函数也不能 new,就更加分了。
小结
本章我们围绕一行 new Person('Alice'),拆解了 new 调用背后的完整流程:
- 创建一个空对象
- 把这个对象的原型指向构造函数的
prototype - 以这个对象为
this执行构造函数 - 按“对象优先”的规则决定最终返回值
并在此基础上手写了一个简化版的 myNew,把抽象的规范具象成了可运行的代码。
本章思维导图
- new 想解决什么
- 创建实例对象
- 建立原型链
- 在构造函数中初始化 this
- 规范四步
- 创建空对象 obj
- obj.__proto__ → F.prototype
- F.apply(obj, args)
- 返回对象优先,否则返回 ob
- 手写 myNew
- 按四步实现一个 myNew(F, ...args)
- 与 class / 后续章节
- class 是构造函数语法糖
- 为手写 call/apply/bind 和继承打基础
练习挑战
以下三道题难度递进,建议先自己写完再看答案。
Level 1:预测输出
不运行代码,先写出你认为的输出结果:
function Widget(id) {
this.id = id;
this.el = document.createElement('div');
return 42;
}
Widget.prototype.render = function() {
return '<Widget #' + this.id + '>';
};
const w = new Widget(7);
console.log(w.id);
console.log(w.render());
console.log(w instanceof Widget);
点击查看答案
7
'<Widget #7>'
true
return 42 返回的是原始值,所以被忽略,new 仍然返回内部创建的对象。原型方法 render 也能正常访问。
Level 2:增强版 myNew
文章中的 myNew 没有处理一个边界情况:如果传进来的第一个参数不是函数怎么办? 请增强它,让它在 F 不是函数时抛出一个有意义的错误,并且用 Object.create 替换 {} + setPrototypeOf 的写法。
点击查看参考实现
function myNew(F, ...args) {
if (typeof F !== 'function') {
throw new TypeError(F + ' is not a constructor');
}
const obj = Object.create(F.prototype);
const result = F.apply(obj, args);
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');
return isObject ? result : obj;
}
// 测试
myNew(123); // TypeError: 123 is not a constructor
myNew(function(x) { this.x = x; }, 10); // { x: 10 }
Level 3:实现一个带 new.target 检测的安全构造函数
有时候我们希望一个构造函数不管用不用 new,都能正确返回实例(很多老库比如 jQuery 就这么做)。请实现一个 SafeArray 构造函数:
- 不用
new调用时,自动帮用户加上new - 接受任意个初始元素
- 实例有一个
items属性存放这些元素 - 原型上有一个
first()方法返回第一个元素
点击查看参考实现
function SafeArray() {
// 检测是否通过 new 调用
if (!(this instanceof SafeArray)) {
return new SafeArray(...arguments);
}
this.items = Array.from(arguments);
}
SafeArray.prototype.first = function() {
return this.items[0];
};
// 测试
const a = SafeArray('x', 'y', 'z'); // 没用 new,照样返回实例
const b = new SafeArray('a', 'b'); // 正常 new 调用
console.log(a.first()); // 'x'
console.log(b.first()); // 'a'
console.log(a instanceof SafeArray); // true
console.log(b instanceof SafeArray); // true
ES6 写法可以用 new.target 更优雅地做同样的事:
function SafeArray() {
if (!new.target) {
return new SafeArray(...arguments);
}
this.items = Array.from(arguments);
}
自我检测
学完本章,对照下面的清单逐条打勾。如果有任何一条打不了勾,建议翻回对应章节再看一遍。
- 我能说出
new调用的四个步骤,并解释每一步的作用 - 我能解释为什么
new出来的对象能通过instanceof检测 - 我知道构造函数返回原始值和返回对象时,
new的结果分别是什么 - 我能在白板上手写一个
myNew,并处理返回值的边界情况 - 我能说清楚
class和传统构造函数在new调用上的异同 - 我知道箭头函数为什么不能被
new调用 - 我能解释
Object.create(F.prototype)和{} + setPrototypeOf的区别
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90