在 JavaScript 面试中,"new 做了什么"和"手写 new"几乎成了必考题。本文从底层原理出发,一步步实现 myNew,覆盖所有边界情况,帮你彻底吃透这个知识点。
目录
- [前言:为什么 new 这么重要](#前言:为什么 new 这么重要 "#section-1")
- [new 到底做了什么?------四步拆解](#new 到底做了什么?——四步拆解 "#section-2")
- 手写实现:从基础版到严谨版
- 边界情况分析:那些容易踩的坑
- 完整测试:验证我们的实现
- [原理深入:原型链、Object.create 与 new.target](#原理深入:原型链、Object.create 与 new.target "#section-6")
- 面试加分项:怎么回答更出彩
- 总结
前言:为什么 new 这么重要
如果你参加过前端面试,大概率被问过这个问题:"说说 new 操作符做了什么?" ,紧接着就是 "能手写一个 new 吗?"
这不是一个简单的背诵题。它考察的是你对 JavaScript 原型链 、this 绑定 、对象创建 三个核心机制的整合理解。很多候选人能说出概念,但一写代码就漏洞百出------返回值判断遗漏 null、原型指向写错、边界情况没考虑......
本文将从零开始,带你彻底吃透 new 的原理 ,并手写一个覆盖所有边界情况的 myNew。
new 到底做了什么?------四步拆解
先来看一段最常见的代码:
JavaScript
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
const p = new Person('张三', 25);
p.sayHello(); // Hello, I'm 张三
console.log(p instanceof Person); // true
当我们写下 new Person('张三', 25) 时,JavaScript 引擎在背后做了四件事:
1
创建一个新的空对象
引擎在内存中开辟空间,创建一个全新的普通对象 {}。这个对象此时没有任何自定义属性。
2
设置原型链(链接 prototype)
将新对象的 __proto__ 指向构造函数的 prototype。这一步建立了原型链,让实例能访问构造函数原型上的方法。
3
绑定 this 并执行构造函数
将构造函数的 this 指向新对象,传入参数执行。构造函数内部的 this.xxx = xxx 就是在给这个新对象添加属性。
4
判断返回值
如果构造函数显式返回了一个对象 (或函数),则 new 的结果就是那个返回值;否则返回第一步创建的新对象。
关键点
第四步是很多人容易忽略的。记住规则:返回对象类型则用返回值,返回原始值或无 return 则用新对象 。null 虽然 typeof 返回 'object',但它属于原始值,需要特殊处理。
手写实现:从基础版到严谨版
基础版:最直观的实现
根据上面的四步,我们可以直接翻译成代码:
JavaScript · 基础版
javascript
// 基础版 myNew
function myNew(constructor, ...args) {
// 第一步:创建一个新对象
const obj = {};
// 第二步:设置原型链
obj.__proto__ = constructor.prototype;
// 第三步:绑定 this 并执行构造函数
const result = constructor.apply(obj, args);
// 第四步:判断返回值
return (typeof result === 'object' || typeof result === 'function') && result !== null
? result
: obj;
}
这个版本已经能处理大部分场景了,但有一个问题:直接操作 __proto__ 并不是推荐做法 。虽然现代浏览器都支持 __proto__,但它本质上是内部属性,在极端环境(如某些非浏览器 JS 引擎)中可能不可用。
严谨版:使用 Object.create
更标准、更严谨的实现应该使用 Object.create() 来同时完成"创建对象 + 设置原型"两步:
JavaScript · 严谨版
javascript
// 严谨版 myNew ------ 面试推荐写法
function myNew(constructor, ...args) {
// 第一步 + 第二步:创建新对象并设置原型
const obj = Object.create(constructor.prototype);
// 第三步:绑定 this 并执行构造函数
const result = constructor.apply(obj, args);
// 第四步:判断返回值类型
const isObject = typeof result === 'object' && result !== null;
const isFunction = typeof result === 'function';
return isObject || isFunction ? result : obj;
}
为什么用 Object.create?
Object.create(proto) 会创建一个以 proto 为原型的新对象,等价于创建空对象再设置 __proto__,但它是 ES5 标准 API,更规范、更安全。在面试中用 Object.create 是明确的加分项。
边界情况分析:那些容易踩的坑
面试官不会只满足于基础实现。他们会追问各种边界情况,来验证你是否真的理解了原理。下面逐一分析:
情况一:构造函数返回原始值
返回 string、number、boolean 等原始值时,会被忽略 ,new 仍然返回新创建的对象。
JavaScript
ini
function Foo() {
this.value = 1;
return 2; // 返回原始值 → 被忽略
}
const foo = myNew(Foo);
console.log(foo.value); // 1 ✅ 返回的是新对象
情况二:构造函数返回对象
返回一个对象 时,new 的结果就是这个返回的对象,而非新创建的对象。
JavaScript
ini
function Bar() {
this.value = 1;
return { value: 2 }; // 返回对象 → 作为 new 的结果
}
const bar = myNew(Bar);
console.log(bar.value); // 2 ✅ 返回的是构造函数的 return 值
情况三:构造函数返回 null
这是最经典的坑 。typeof null 返回 'object',但 null 实际上是原始值。如果不做特殊处理,myNew 会错误地返回 null 而非新对象。
JavaScript
javascript
function Baz() {
this.value = 1;
return null; // typeof null === 'object',但 null 是原始值!
}
const baz = myNew(Baz);
console.log(baz.value); // 1 ✅ 正确返回新对象,而非 null
高频面试追问
"typeof null 为什么返回 'object'?"------这是 JS 的历史遗留问题。早期版本中,值的类型用低位标识:对象类型标记为 000,而 null 在大多数平台被表示为全 0 指针,所以被误判为对象类型。这个 bug 一直延续至今。
情况四:构造函数返回函数
函数也是对象类型,所以如果构造函数返回一个函数,new 的结果应该是那个返回的函数。
JavaScript
javascript
function Qux() {
this.value = 1;
return function() { return 'I am a function'; };
}
const qux = myNew(Qux);
console.log(typeof qux); // 'function' ✅ 返回的是函数
console.log(qux()); // 'I am a function' ✅
完整测试:验证我们的实现
将所有边界情况整合在一起,写一个完整的测试套件:
JavaScript · 完整测试
javascript
// ============ 完整测试套件 ============
// 测试 1:基本功能 + 原型链
function Animal(type) {
this.type = type;
}
Animal.prototype.getType = function() {
return this.type;
};
const animal = myNew(Animal, 'cat');
console.log(animal.getType()); // 'cat'
console.log(animal instanceof Animal); // true
console.log(animal.__proto__ === Animal.prototype); // true
// 测试 2:返回对象 → 使用返回值
function ReturnObj() {
this.name = 'inner';
return { name: 'returned' };
}
console.log(myNew(ReturnObj).name); // 'returned'
// 测试 3:返回原始值 → 忽略,使用新对象
function ReturnPrimitive() {
this.name = 'inner';
return 'I am a string';
}
console.log(myNew(ReturnPrimitive).name); // 'inner'
// 测试 4:返回 null → 忽略,使用新对象
function ReturnNull() {
this.name = 'inner';
return null;
}
console.log(myNew(ReturnNull).name); // 'inner'
// 测试 5:返回函数 → 使用返回值
function ReturnFunc() {
this.name = 'inner';
return function() { return 'fn'; };
}
const rf = myNew(ReturnFunc);
console.log(typeof rf); // 'function'
console.log(rf()); // 'fn'
// 测试 6:无参数构造函数
function NoArgs() {
this.flag = true;
}
console.log(myNew(NoArgs).flag); // true
// 测试 7:多参数构造函数
function User(name, age, role) {
this.name = name;
this.age = age;
this.role = role;
}
const user = myNew(User, 'Alice', 28, 'admin');
console.log(user.name, user.age, user.role); // Alice 28 admin
所有测试用例均通过:
| 测试项 | 预期结果 | 状态 |
|---|---|---|
| 基本功能 + 原型链 | getType() 返回 'cat',instanceof 为 true |
通过 |
| 返回对象 | 使用返回的对象,name 为 'returned' |
通过 |
| 返回原始值 | 忽略返回值,name 为 'inner' |
通过 |
| 返回 null | 忽略 null,name 为 'inner' |
通过 |
| 返回函数 | 使用返回的函数,typeof 为 'function' |
通过 |
| 无参数 | flag 为 true |
通过 |
| 多参数 | 三个参数正确赋值 | 通过 |
原理深入:原型链、Object.create 与 new.target
原型链:为什么要链接 prototype?
JavaScript 的继承机制是基于原型链的。当你在实例上访问一个属性或方法时,引擎会先在实例自身查找,找不到就沿着 __proto__ 往上找。
new 操作的第二步------将 obj.__proto__ 指向 constructor.prototype------正是为了建立这条链路:
原型链示意
javascript
// 原型链查找路径
instance → 查找自身属性
↓ __proto__
constructor.prototype → 查找原型上的方法(如 sayHello)
↓ __proto__
Object.prototype → 查找 toString、valueOf 等
↓ __proto__
null → 查找结束,返回 undefined
如果我们漏了第二步,实例就访问不到构造函数原型上的方法,instanceof 也会返回 false。
Object.create vs 直接设置 proto
这两种方式在功能上等价:
JavaScript
ini
// 方式 A:Object.create(推荐)
const obj1 = Object.create(Constructor.prototype);
// 方式 B:手动设置 __proto__(不推荐)
const obj2 = {};
obj2.__proto__ = Constructor.prototype;
// 两者效果相同,但 Object.create 更标准
| 对比维度 | Object.create | proto 赋值 |
|---|---|---|
| 标准化 | ES5 标准 API | ES6 才标准化(附录) |
| 一步完成 | 创建 + 设置原型一步到位 | 需要先创建再赋值 |
| 性能 | 引擎优化更好 | 可能触发额外的隐藏类转换 |
| 可读性 | 意图清晰 | 需要了解 __proto__ 语义 |
| 兼容性 | IE9+ | IE11+(非 IE6-9) |
new.target:判断函数是否被 new 调用
ES6 引入了 new.target 元属性,用于检测函数是否通过 new 调用:
JavaScript
javascript
function Person(name) {
if (!new.target) {
throw new Error('必须使用 new 关键字调用 Person');
}
this.name = name;
}
Person('张三'); // ❌ Error: 必须使用 new 关键字
new Person('张三'); // ✅ 正常创建实例
当通过 new 调用时,new.target 指向构造函数本身;当作为普通函数调用时,new.target 为 undefined。
箭头函数为什么不能作为构造函数?
箭头函数没有自己的 this、arguments、super 和 new.target,也没有 [[Construct]] 内部方法。因此对箭头函数使用 new 会直接抛出 TypeError:
JavaScript
javascript
const ArrowFn = () => { this.x = 1; };
new ArrowFn();
// TypeError: ArrowFn is not a constructor
同样的,我们的 myNew 如果传入箭头函数,constructor.apply() 不会绑定 this(箭头函数的 this 在定义时就确定了),所以行为会不符合预期。可以在 myNew 中加入防御检查:
JavaScript · 加防御版
javascript
function myNew(constructor, ...args) {
// 防御:箭头函数没有 prototype
if (!constructor.prototype) {
throw new TypeError(`${constructor.name} is not a constructor`);
}
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return (typeof result === 'object' && result !== null) || typeof result === 'function'
? result
: obj;
}
面试加分项:怎么回答更出彩
面试时,如果只是把代码写出来,只能拿到基础分。以下这些点能让你的回答脱颖而出:
面试加分清单
- 使用 Object.create 而非 proto ------ 展示你对标准 API 的了解,说明 Object.create 更规范、性能更好。
- 处理 null 返回值 ------ 主动提到
typeof null === 'object'的历史遗留问题,说明你理解 JavaScript 类型系统的缺陷。 - 箭头函数不能作为构造函数 ------ 主动说明箭头函数没有
[[Construct]]内部方法,没有自己的this和prototype。 - 提到 new.target ------ 说明 ES6 引入了
new.target元属性,可以判断函数是否通过new调用,用于实现"必须 new 调用"的约束。 - 提到 ES6 class 的 new 行为 ------
class必须用new调用,否则报TypeError;且class内部默认使用严格模式。 - 用一行代码验证 ------ 写完后主动用
instanceof和原型链检查验证正确性,展示测试意识。
总结
把 new 的原理浓缩成一张表:
| 步骤 | 操作 | 对应代码 |
|---|---|---|
| 1 | 创建新对象 | Object.create(constructor.prototype) |
| 2 | 设置原型链 | ↑ 已包含在上面 |
| 3 | 绑定 this 并执行 | constructor.apply(obj, args) |
| 4 | 判断返回值 | typeof result === 'object' && result !== null |
最终,面试推荐版本(20 行以内,覆盖所有边界情况):
JavaScript · 终版
ini
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return result && (typeof result === 'object' || typeof result === 'function')
? result
: obj;
}
一行简记
创建空对象 → 链接原型 → 绑定 this 执行 → 判断返回值 。四步缺一不可,null 要特判,Object.create 优于 proto。