手写 new:从大厂面试题深入理解 JavaScript 的构造机制
在前端大厂的面试中,手写 new 是一道经典题目。它不仅考察你对 JavaScript 面向对象的理解,还涉及原型链、函数调用、this 绑定以及参数处理等核心概念。
本文将从内置 new 的行为出发,逐步实现一个功能完整的 objectFactory 函数,并深入剖析其中的“借用方法”技巧和“返回值”陷阱。
一、核心原理:new 到底做了什么?
当我们使用 new 调用一个构造函数时(如 new Person('Alice')),JavaScript 引擎在幕后执行了以下四步:
- 创建新对象:在内存中创建一个全新的空对象。
- 链接原型:将这个新对象的
[[Prototype]](即__proto__)指向构造函数的prototype属性,从而继承原型上的方法。 - 绑定 this 并执行:将构造函数内部的
this指向这个新对象,并执行构造函数代码(为对象添加属性)。 - 返回结果:如果构造函数返回了一个对象,则返回该对象;否则,返回第一步创建的新对象。
二、初探:ES6 语法下的基础实现
如果你习惯使用 ES6 的剩余参数(...args),代码会非常简洁清晰。
function objectFactory(Constructor, ...args) {
// 1. 创建新对象,并将其原型指向构造函数的 prototype
// 以前我们用 obj.__proto__ = Constructor.prototype,现在推荐用 Object.create
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,绑定 this 到新对象
const result = Constructor.apply(obj, args);
// 3. 处理返回值(这是面试最容易漏掉的点!)
// 如果构造函数返回了对象或函数,则返回它;否则返回我们创建的 obj
return (result instanceof Object) ? result : obj;
}
三、进阶:ES5 经典面试题版本(无显式参数)
在很多老牌大厂的面试题中,面试官会限制你使用 ...args,或者直接考察你对 arguments 对象的理解。这时,我们需要处理参数的提取。
代码实现
function objectFactory() {
// 1. 借用数组方法,从 arguments 中取出第一个参数(即构造函数)
// 注意:shift 会修改原 arguments 对象
var Constructor = [].shift.call(arguments);
// 2. 创建新对象,链接原型
var obj = new Object();
obj.__proto__ = Constructor.prototype;
// 3. 绑定 this 并传参
// 此时 arguments 已经被 shift 移除了第一个元素,剩下的就是参数了
// 这里的 [...arguments] 是为了将类数组转为数组,兼容性写法可用 Array.from 或循环
var result = Constructor.apply(obj, arguments);
// 4. 返回值判断(处理 null 的边界情况)
return (typeof result === 'object' && result !== null) || typeof result === 'function'
? result
: obj;
}
亮点解析:[].shift.call(arguments)
这行代码是整个函数的高光时刻,包含两个知识点:
-
类数组借用方法:
arguments是一个类数组对象(Array-like),它有length属性和索引,但没有shift、push等数组方法。 -
call 的作用:我们通过
[].shift.call(arguments)或者Array.prototype.shift.call(arguments),强行让arguments对象借用了数组的shift方法。- 这会移除
arguments的第一个元素(即传入的构造函数)。 - 同时,
arguments剩余的部分就自动变成了后续需要的参数列表。
- 这会移除
四、测试验证
让我们用一个包含原型的构造函数来验证我们的代码:
function Person(name, age) {
this.name = name;
this.age = age;
// 故意不返回对象,测试默认返回行为
}
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
}
// 另一个构造函数,测试显式返回对象的情况
function Robot(name) {
this.name = name;
return { name: "我是被返回的新对象" }; // 显式返回对象
}
// 测试 Case 1: 标准情况
let p = objectFactory(Person, '极客邦', 18);
console.log(p.name); // '极客邦'
p.sayHi(); // '你好,我是极客邦' (原型链正常)
// 测试 Case 2: 构造函数返回对象
let r = objectFactory(Robot, 'AI');
console.log(r.name); // '我是被返回的新对象' (而非 'AI')
五、总结与对比
| 步骤 | 内置 new | 我们的 objectFactory | 备注 |
|---|---|---|---|
| 创建对象 | 自动 | Object.create() 或 new Object() | 推荐使用 Object.create 以符合标准 |
| 参数处理 | new Fn(arg) | [].shift.call(arguments) | 利用 call 借用数组方法处理类数组 |
| this 绑定 | 自动 | Fn.apply(obj, args) | 核心步骤,确立作用域 |
| 返回值 | 自动判断 | 手动判断 result 类型 | 面试丢分高频点!一定要判断是否返回了对象 |
手写 new 不仅是一道面试题,更是理解 JavaScript 面向对象本质的钥匙。掌握了它,你就真正理解了“构造函数”、“原型”、“this”以及“类数组”这几个核心概念是如何协同工作的。
💡 思考题:如果构造函数返回的是基本类型(如
return 1;),new会怎么处理?(答案:忽略返回值,返回新创建的实例obj。这也是为什么我们在代码中判断typeof result === 'object'的原因。)