引言
new 是 JavaScript 中创建对象实例的常用操作符,但它的内部机制常被忽视。在面试中,“手写 new”是考察原型链、this 绑定和对象创建原理的经典题目。本文将带你一步步实现一个模拟 new 功能的函数,并解释其中的关键细节。
核心问题分析
我们日常使用 new 创建对象非常自然:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`你好,我是 ${this.name}`);
};
const p = new Person('张三', 18);
p.sayHi(); // 你好,我是 张三
但 new Person(...) 这一行背后,JavaScript 引擎到底做了什么?
更进一步:如果不使用 new,我们能否用一个普通函数实现完全相同的行为?
这就是本文要解决的核心问题:
理解
new的内部机制,并手动模拟它的完整功能。
只有真正实现过,才能说我们掌握了构造函数、原型链和对象创建的本质。
new 的内部机制:从代码出发
📌 代码目的
这段代码试图用 ES6 的 rest 参数语法(...args) 来实现一个模拟 new 操作符的函数 objectFactory。其目标是:
让
objectFactory(Person, '李四', 19)的行为与new Person('李四', 19)完全一致。
这是理解 JavaScript 对象创建机制的经典练习。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模拟 new 操作符(带原型链)</title>
</head>
<body>
<script>
// 模拟 new 的功能(ES6 写法)
function objectFactory(Constructor, ...args) {
// 1. 创建一个空对象
const obj = new Object(); // 或直接写 obj = {};
// 2. 手动设置它的原型为 Constructor.prototype
obj.__proto__ = Constructor.prototype;
// 3. 执行构造函数,将 this 绑定到新对象
Constructor.apply(obj, args);
return obj;
}
// 测试构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在原型上添加方法
Person.prototype.sayHi = function() {
console.log(`你好,我是 ${this.name}`);
};
// 对比:原生 new vs 手动模拟
const p = new Person('张三', 18);
const zzp = objectFactory(Person, '李四', 19);
// 输出结果
console.log('p:', p);
console.log('zzp:', zzp);
// 验证原型链是否正确
console.log('p instanceof Person:', p instanceof Person); // true
console.log('zzp instanceof Person:', zzp instanceof Person); // true
// 验证原型方法是否可用
p.sayHi(); // 你好,我是 张三
zzp.sayHi(); // 你好,我是 李四
</script>
</body>
</html>
这段代码使用 ES6 的 rest 参数语法(...args) 实现了一个模拟 new 操作符的函数 objectFactory,其目标是:
使
objectFactory(Person, '李四', 19)的行为与new Person('李四', 19)完全一致。
解释在后面有
这是深入理解 JavaScript 对象创建机制的经典练习。
我们再来看看ES6之前的写法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function objectFactory() {
var obj = new Object(); // ① 创建空对象
var Constructor = [].shift.call(arguments); // ② 取出第一个参数作为构造函数
Constructor.apply(obj, [...arguments]); // ③ 执行构造函数,绑定 this
obj.__proto__ = Constructor.prototype; // ④ 手动设置原型链
return obj;
}
function Person (name,age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function(){
console.log(`你好,我是${this.name}`);
}
let p = new Person('张三',18);
let zzp = objectFactory(Person,'郑志鹏',18);
console.log(p)
console.log(zzp)
</script>
</body>
</html>
关键代码解析:ES5 风格的手写 new 实现
function objectFactory() {
var obj = new Object(); // ① 创建空对象
var Constructor = [].shift.call(arguments); // ② 取出第一个参数作为构造函数
Constructor.apply(obj, [...arguments]); // ③ 执行构造函数,绑定 this
obj.__proto__ = Constructor.prototype; // ④ 手动设置原型链
return obj;
}
这段代码试图在 不使用 new 关键字 的前提下,手动模拟其核心行为。虽然写法属于 ES5 时代(依赖 arguments 和 __proto__),但它精准地捕捉了 new 操作符的四个关键步骤。
② var Constructor = [].shift.call(arguments);
从 arguments 中“弹出”第一个参数,作为构造函数。
-
arguments是一个类数组对象,包含所有传入参数(如[Person, '郑志鹏', 18])。 -
[].shift.call(arguments)利用数组的shift方法,移除并返回第一个元素,同时修改arguments本身。 -
执行后:
Constructor→Person(构造函数)arguments变为['郑志鹏', 18](仅剩实际参数)
这是 ES5 中提取首个参数并获取剩余参数的经典技巧。
③ Constructor.apply(obj, [...arguments]);
将构造函数的执行上下文(this)绑定到新对象 obj,并传入剩余参数。
- 相当于调用
Person.call(obj, '郑志鹏', 18)。 - 在
Person内部,this.name = name等操作会直接作用于obj,使其获得实例属性。
⚠️ 注意:此处使用了 ES6 的展开语法
[...arguments]。若严格遵循 ES5,应写作Array.prototype.slice.call(arguments)。
④ obj.__proto__ = Constructor.prototype;
手动建立原型链,使 obj 能访问构造函数原型上的方法(如 sayHi)。
- 虽然
__proto__不是 ECMAScript 标准属性(正式标准中应使用Object.setPrototypeOf),但在所有主流浏览器中均被支持。 - 此步骤确保
zzp instanceof Person返回true,并能调用zzp.sayHi()。
这种写法不依赖 ES6 新特性,而是通过操作 arguments 对象和手动设置原型链来实现相同的功能,体现了早期 JavaScript 开发中常见的模式与技巧。
🌟 写在最后:new 背后,是 JavaScript 的温柔与秩序
我们花了几十行代码,去还原一个只用 3 个字母就能完成的操作——new。
看起来像是“造轮子”,但正是这种“手搓底层”的过程,让我们看清了 JavaScript 并非魔法,而是一套严谨、可推演、可复现的机制。
那些看似神秘的 this、原型链、执行上下文……其实都藏在一行行朴素的代码里。
真正的高手,不是记住 API,而是理解规则;不是调用黑盒,而是亲手点亮它。
你今天写的每一行“模拟 new”的代码,都是在和 V8 引擎隔空对话。
而这份对话的底气,就来自你对语言本质的理解。