跳到主要内容
预计阅读 28 分钟

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 没有原型关系,所以 instanceoffalse

Q2:不用 new,直接调用 Dog('Buddy') 会怎样?

点击查看答案

在非严格模式下,this 会指向全局对象(浏览器里是 window),所以 name 会被挂到全局上,变成 window.name = 'Buddy'。在严格模式下,thisundefined,会直接报 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:

  1. 能创建一个全新的对象
  2. 这个新对象上能访问到构造函数里挂到 this 上的属性
  3. 新对象的原型上,能访问到构造函数原型上的方法
  4. instanceof 判定正常:p instanceof Person === true

也就是说,一次 new 调用,至少要帮我们完成三件事:

  1. 创建一个对象
  2. 把对象和构造函数通过原型链“连起来”
  3. 在构造函数内部用这个对象作为 this 执行初始化逻辑

规范里是怎么描述这一过程的?


二、规范视角:构造调用做了哪几步?

在 ECMAScript 规范中,new F(...args) 触发的是 构造函数调用(constructor call),大致可以拆成四步(简化后便于记忆):

  1. 创建一个空对象
  2. 把空对象的内部原型 [[Prototype]] 指向构造函数的 prototype
  3. 以这个对象为 this,执行构造函数
  4. 根据返回值决定最终结果

用伪代码表达:

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);

这里有两个知识点被同时用上:

  1. 利用 apply/call 显式绑定 this,让构造函数体内的 this 指向我们的新对象
  2. 利用 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 是一种全新的机制。其实不是——classnew 流程和传统构造函数走的是同一套规范。真正的区别在于:

  • 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”,至少要答出箭头函数。如果能再补上 SymbolBigInt 等内建函数也不能 new,就更加分了。


小结

本章我们围绕一行 new Person('Alice'),拆解了 new 调用背后的完整流程:

  1. 创建一个空对象
  2. 把这个对象的原型指向构造函数的 prototype
  3. 以这个对象为 this 执行构造函数
  4. 按“对象优先”的规则决定最终返回值

并在此基础上手写了一个简化版的 myNew,把抽象的规范具象成了可运行的代码。


本章思维导图

JS:new 调用原理
  • 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