当面试官问“手写new”时,我掏出ES5暗黑代码,他沉默了

40 阅读4分钟

当面试官问“手写new”时,我掏出ES5暗黑代码,他沉默了

在JavaScript中,new运算符是实现面向对象编程的关键。它不仅用于实例化对象,还涉及到原型链、this指向等核心概念。今天,我们来深入探讨new的原理,并手写一个new运算符实现,特别关注ES6以下环境的兼容性问题。

一、new运算符的工作原理

当我们使用new创建一个对象实例时,它实际上执行了以下步骤:

  1. 创建一个空对象let obj = {}
  2. 设置this指向this = obj
  3. 执行构造函数obj = constructor.apply(this, args)
  4. 设置原型obj.__proto__ = constructor.prototype
  5. 返回对象return obj

二、类数组Arguments详解

在JavaScript中,函数的arguments对象是一个类数组(array-like object),它有以下特点:

  • length属性,可以使用索引访问元素
  • 有类似数组的结构,但不是真正的数组
  • 不能直接使用数组方法(如reducemapjoin等)

为什么arguments不是数组?

我们通过以下代码验证这一点:

function add() {
  console.log(arguments.__proto__); // 输出: [object Object]
  console.log(arguments.reduce);    // 报错: arguments.reduce is not a function
}

arguments对象的__proto__指向的是Object.prototype,而不是Array.prototype,这表明它不是数组。

如何将arguments转换为真正的数组?

有多种方法可以将类数组转换为数组:

  1. 使用ES6的展开运算符(Spread)、Array.from()

    const args = [...arguments];
    const args = Array.from(arguments);
    const args = Array.prototype.slice.call(arguments);//(ES5及以下)
    

三、手写new运算符实现

ES6及以上环境的实现(使用rest运算符)

function myNew(constructor, ...args) {
  // 创建一个空对象,继承构造函数的原型
  const obj = Object.create(constructor.prototype);
  
  // 执行构造函数,将this绑定到新对象
  const result = constructor.apply(obj, args);
  
  // 如果构造函数返回的是对象,则返回该对象,否则返回新创建的对象
  return result && typeof result === 'object' ? result : obj;
}

ES5及以下环境的实现(使用arguments)

function myNew(constructor) {
  // 创建一个空对象,继承构造函数的原型
  const obj = Object.create(constructor.prototype);
  
  // 将arguments转换为数组(兼容ES5及以下)
  const args = Array.prototype.slice.call(arguments, 1);
  
  // 执行构造函数,将this绑定到新对象
  const result = constructor.apply(obj, args);
  
  // 如果构造函数返回的是对象,则返回该对象,否则返回新创建的对象
  return result && typeof result === 'object' ? result : obj;
}

四、面试题解析

面试官:请手写一个new运算符的实现,并说明在ES6以下环境如何处理arguments

应聘者

"好的,我理解new运算符的工作原理。

function objectFactory(Constructor, ...args){
    //var obj = {}//使用对象字面量
    var obj = new Object(); //从空对象开始
    Constructor.apply(obj, args); //运行构造函数
    obj.__proto__ = Constructor.prototype    ;
    return obj;
}

它主要做了四件事:

  1. 创建一个新对象
  2. 将新对象的__proto__指向构造函数的prototype
  3. 将构造函数的this绑定到新对象
  4. 返回新对象

在实现中,我需要处理函数的参数。在ES6及以上,我可以用rest运算符...args来获取参数。但在ES5及以下,我需要使用arguments对象。

arguments是一个类数组,不是真正的数组,所以不能直接使用数组方法。我需要用Array.prototype.slice.call(arguments, 1)arguments转换为真正的数组,然后传递给apply方法。"

面试官:为什么不能直接使用arguments.reduce

应聘者: "因为arguments不是数组,它的原型链是Object.prototype,不是Array.prototype。所以它没有数组的方法,比如reducemap等。我们可以通过Array.prototype.slice.call(arguments)将其转换为数组,或者使用ES6的展开运算符[...arguments]。"

五、实践验证

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  return `Hello, I'm ${this.name}, ${this.age} years old.`;
};

// ES5及以下环境
const person = myNew(Person, 'John', 30);
console.log(person.sayHello()); // "Hello, I'm John, 30 years old."

// ES6及以上环境
const person2 = myNew(Person, 'Jane', 25);
console.log(person2.sayHello()); // "Hello, I'm Jane, 25 years old."

六、总结

  1. new运算符是JS面向对象的核心,理解其原理对掌握JS的原型链至关重要
  2. arguments是一个类数组对象,不是真正的数组,不能直接使用数组方法
  3. 在ES5及以下环境,使用Array.prototype.slice.call(arguments, 1)arguments转换为数组
  4. 在ES6及以上环境,可以使用展开运算符[...arguments]更简洁地转换

通过理解new运算符的原理和arguments的特性,我们不仅能写出兼容性更好的代码,还能更深入地掌握JavaScript的核心机制。在面试中,当被问及new的实现时,能够清晰地解释这些细节,将大大提升你的专业形象。

小提示:在实际开发中,我们通常不需要手写new,但理解其原理对于解决原型链相关问题、理解框架内部机制非常有帮助。特别是在处理兼容性问题时,了解这些底层机制能让你写出更健壮的代码。