手写 `new`:从大厂面试题深入理解 JavaScript 的构造机制

47 阅读4分钟

手写 new:从大厂面试题深入理解 JavaScript 的构造机制

在前端大厂的面试中,手写 new 是一道经典题目。它不仅考察你对 JavaScript 面向对象的理解,还涉及原型链、函数调用、this 绑定以及参数处理等核心概念。

本文将从内置 new 的行为出发,逐步实现一个功能完整的 objectFactory 函数,并深入剖析其中的“借用方法”技巧和“返回值”陷阱。

一、核心原理:new 到底做了什么?

当我们使用 new 调用一个构造函数时(如 new Person('Alice')),JavaScript 引擎在幕后执行了以下四步:

  1. 创建新对象:在内存中创建一个全新的空对象。
  2. 链接原型:将这个新对象的 [[Prototype]](即 __proto__)指向构造函数的 prototype 属性,从而继承原型上的方法。
  3. 绑定 this 并执行:将构造函数内部的 this 指向这个新对象,并执行构造函数代码(为对象添加属性)。
  4. 返回结果如果构造函数返回了一个对象,则返回该对象;否则,返回第一步创建的新对象。

二、初探: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)

这行代码是整个函数的高光时刻,包含两个知识点:

  1. 类数组借用方法arguments 是一个类数组对象(Array-like),它有 length 属性和索引,但没有 shiftpush 等数组方法。

  2. 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' 的原因。)