当面试官问“手写new”时,我掏出ES5暗黑代码,他沉默了
在JavaScript中,new运算符是实现面向对象编程的关键。它不仅用于实例化对象,还涉及到原型链、this指向等核心概念。今天,我们来深入探讨new的原理,并手写一个new运算符实现,特别关注ES6以下环境的兼容性问题。
一、new运算符的工作原理
当我们使用new创建一个对象实例时,它实际上执行了以下步骤:
- 创建一个空对象:
let obj = {} - 设置this指向:
this = obj - 执行构造函数:
obj = constructor.apply(this, args) - 设置原型:
obj.__proto__ = constructor.prototype - 返回对象:
return obj
二、类数组Arguments详解
在JavaScript中,函数的arguments对象是一个类数组(array-like object),它有以下特点:
- 有
length属性,可以使用索引访问元素 - 有类似数组的结构,但不是真正的数组
- 不能直接使用数组方法(如
reduce、map、join等)
为什么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转换为真正的数组?
有多种方法可以将类数组转换为数组:
-
使用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;
}
它主要做了四件事:
- 创建一个新对象
- 将新对象的
__proto__指向构造函数的prototype - 将构造函数的
this绑定到新对象 - 返回新对象
在实现中,我需要处理函数的参数。在ES6及以上,我可以用rest运算符...args来获取参数。但在ES5及以下,我需要使用arguments对象。
arguments是一个类数组,不是真正的数组,所以不能直接使用数组方法。我需要用Array.prototype.slice.call(arguments, 1)将arguments转换为真正的数组,然后传递给apply方法。"
面试官:为什么不能直接使用arguments.reduce?
应聘者: "因为arguments不是数组,它的原型链是Object.prototype,不是Array.prototype。所以它没有数组的方法,比如reduce、map等。我们可以通过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."
六、总结
new运算符是JS面向对象的核心,理解其原理对掌握JS的原型链至关重要arguments是一个类数组对象,不是真正的数组,不能直接使用数组方法- 在ES5及以下环境,使用
Array.prototype.slice.call(arguments, 1)将arguments转换为数组 - 在ES6及以上环境,可以使用展开运算符
[...arguments]更简洁地转换
通过理解new运算符的原理和arguments的特性,我们不仅能写出兼容性更好的代码,还能更深入地掌握JavaScript的核心机制。在面试中,当被问及new的实现时,能够清晰地解释这些细节,将大大提升你的专业形象。
小提示:在实际开发中,我们通常不需要手写
new,但理解其原理对于解决原型链相关问题、理解框架内部机制非常有帮助。特别是在处理兼容性问题时,了解这些底层机制能让你写出更健壮的代码。