Skip to content

new 操作符具体干了什么 ​

难度等级:🟢 初级 分类:JavaScript 归档日期:2026-10-09


说说 new 操作符具体干了什么?

一、难度与考察点 ​

难度:基础题,但资深候选人需要答出规范层细节和边界情况。

考察点:

  • 原型链、this 绑定、构造函数返回值规则
  • 能否手写 new,并清楚手写版本的局限
  • 是否了解 new.target、class 继承中对象的创建时机、箭头函数为什么不能 new

二、核心原理 ​

先说结论:new Foo(...args) 主要做 4 件事。

  1. 校验:Foo 必须是可构造的,也就是具有内部方法 [[Construct]],否则抛出 TypeError。箭头函数、对象方法简写、async 函数、generator 函数,以及 Math.max 这类内置普通函数,都不能被 new。
  2. 创建对象并连接原型:新建一个空对象,令它的 [[Prototype]] 等于 Foo.prototype。如果 Foo.prototype 不是对象,比如被改成了 null 或原始值,就退回到 Object.prototype。
  3. 执行构造函数:把 this 绑定到这个新对象,传入参数执行函数体。同时 new.target 指向 Foo。
  4. 决定返回值:如果构造函数显式返回一个对象(包括函数、数组),就返回它;如果返回原始值或什么都不返回,原始值会被忽略,返回第 1 步创建的新对象。

手写实现:

js
function myNew(Ctor, ...args) {
  if (typeof Ctor !== 'function') {
    throw new TypeError(`${Ctor} is not a constructor`);
  }
  const proto = Ctor.prototype;
  const isObj = v => v !== null && (typeof v === 'object' || typeof v === 'function');

  // 第 2 步:创建对象并连接原型;prototype 非对象时退回 Object.prototype
  const obj = Object.create(isObj(proto) ? proto : Object.prototype);

  // 第 3 步:绑定 this 执行
  const result = Ctor.apply(obj, args);

  // 第 4 步:返回对象则用它,否则用新建对象
  return isObj(result) ? result : obj;
}

这个手写版的局限(资深候选人最好主动说出来):

  • 无法准确判断函数是否可构造:箭头函数、class 都能通过 typeof 检查。其中 class 被 apply 调用时会直接报错。
  • 构造函数内部的 new.target 是 undefined。
  • 对 class A extends B 这类派生类不成立。原因见追问 3。

生产代码里需要模拟 new 时,应使用 Reflect.construct(Ctor, args, newTarget),它走的是真正的 [[Construct]] 流程。

三、口语化答案(约 1.5 分钟) ​

new 本质上是调用函数的内部 [[Construct]] 方法,大致分四步。

第一步,校验这个函数能不能被构造,箭头函数、async 函数、对象的简写方法都不行,会直接报 TypeError。

第二步,创建一个新对象,把它的原型指向构造函数的 prototype,所以实例才能访问到原型上的方法,instanceof 也是基于这条链判断的。

第三步,把 this 绑定到这个新对象上执行构造函数,函数里 this.xxx = ... 实际就是在给新对象挂属性。

第四步,看返回值:如果构造函数返回了一个对象,就用这个对象;返回原始值会被忽略,最终返回新创建的对象。

手写可以用 Object.create 加 apply 实现,但它有局限,比如模拟不了 new.target,也处理不了 class 和派生类。所以实际要动态构造对象时,我会用 Reflect.construct。

另外,ES6 的派生类有点特殊:子类构造函数里的 this 不是一开始就创建好的,而是调用 super() 时由父类创建的。所以在 super() 之前访问 this 会报错。

四、常见错误 ​

  • 说“返回值会覆盖新对象”而不区分类型:只有返回对象才会生效,return 1 会被忽略。
  • 说“新对象的 __proto__ 指向构造函数”:实际指向的是构造函数的 prototype。
  • 手写时用 obj.__proto__ = Ctor.prototype:可以工作,但 __proto__ 是遗留特性,用 Object.create 更规范,性能也更好。
  • 手写时用 typeof result === 'object' 判断返回值:会漏掉返回函数的情况,同时会把 null 误判成对象。

五、面试官可能的追问 ​

1. 箭头函数为什么不能 new? 箭头函数没有 [[Construct]] 内部方法,也没有 prototype 属性。它的 this 是词法绑定的,不支持动态绑定,从设计上就不是构造器。

2. new.target 有什么用? 它可以判断函数是不是通过 new 调用的。常见用途有两个:

  • 强制要求必须用 new 调用:if (!new.target) throw ...
  • 实现抽象类:if (new.target === Base) throw new Error('abstract')

3. class 继承时,对象是谁创建的? 基类的实例在 [[Construct]] 开始时就创建好了。派生类不一样:this 处于未初始化状态,直到调用 super(),由最顶层的基类创建对象,原型取自 new.target.prototype。这就是为什么继承 Array、Error 这类内置类时,ES6 class 能得到真正的数组或错误对象,而 ES5 的“call 父类 + 改原型”做不到。

4. 对 bind 之后的函数使用 new 会怎样? 绑定的 this 会被忽略,但预置的参数仍然有效,原型使用的是原函数的 prototype。这一点也常出现在手写 bind 的题里。

5. new Foo.getName() 和 new Foo().getName() 的区别? 这考的是运算符优先级:

  • new Foo.getName() 相当于 new (Foo.getName)()
  • new Foo().getName() 相当于 (new Foo()).getName()

带参数列表的 new 优先级高于不带参数列表的 new。

6. 工程上有什么相关实践?(加分项)

  • 在构造函数中以固定顺序初始化全部属性,不要事后动态增删属性。这样同一类的实例能共享 V8 的 hidden class(shape),inline cache 才能稳定命中。这一点在高频创建对象的场景(如渲染节点、数据模型)中影响明显。
  • 方法应该放在原型上,不要在构造函数里写 this.fn = function(){},否则每个实例都会生成一份新函数。

六、简易版手写 ​

从上面介绍中,我们可以看到 new 关键字主要做了以下的工作:

  • 创建一个新的对象 obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的 this 绑定到新建的对象 obj 上
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
js
function myNew(fn, ...args) {
  // 1. 创建对象,让它的原型指向构造函数的 prototype
  const obj = Object.create(fn.prototype)

  // 2. 执行构造函数,让 this 指向新对象
  const result = fn.apply(obj, args)

  // 3. 如果构造函数返回对象或函数,就使用该返回值
  const isObject =
    result !== null &&
    (typeof result === 'object' || typeof result === 'function')

  return isObject ? result : obj
}

function A(a, b) {
  this.name = 'A'
  this.files = [a, b]
}

const instance = myNew(A, 'a', 'b')

console.log(instance) // A { name: 'A', files: ['a', 'b'] }

Object.getPrototypeOf(instance) === A.prototype // true
instance instanceof A                          // true

个人面试题归档,持续更新中