Appearance
new 操作符具体干了什么
难度等级:🟢 初级 分类:JavaScript 归档日期:2026-10-09
说说
new操作符具体干了什么?
一、难度与考察点
难度:基础题,但资深候选人需要答出规范层细节和边界情况。
考察点:
- 原型链、
this绑定、构造函数返回值规则 - 能否手写
new,并清楚手写版本的局限 - 是否了解
new.target、class继承中对象的创建时机、箭头函数为什么不能new
二、核心原理
先说结论:new Foo(...args) 主要做 4 件事。
- 校验:
Foo必须是可构造的,也就是具有内部方法[[Construct]],否则抛出TypeError。箭头函数、对象方法简写、async函数、generator 函数,以及Math.max这类内置普通函数,都不能被new。 - 创建对象并连接原型:新建一个空对象,令它的
[[Prototype]]等于Foo.prototype。如果Foo.prototype不是对象,比如被改成了null或原始值,就退回到Object.prototype。 - 执行构造函数:把
this绑定到这个新对象,传入参数执行函数体。同时new.target指向Foo。 - 决定返回值:如果构造函数显式返回一个对象(包括函数、数组),就返回它;如果返回原始值或什么都不返回,原始值会被忽略,返回第 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