引言
在 JavaScript 的世界里,new是一个看似简单却蕴含深意的操作符。你可能每天都在用它创建对象,但你是否真正理解它背后发生了什么?更有趣的是,在不使用现代语法(如剩余参数...args)的情况下,我们还能借助一个神秘的内置对象——arguments——来实现相同的功能。
本文将带你从零开始手写一个完全模拟new行为的函数,并在此过程中深度剖析arguments对象的本质、特性与陷阱。我们将像 JavaScript 引擎一样思考,揭开对象创建与参数传递的底层逻辑。内容详尽、生动有趣,哪怕你是初学者,也能跟上节奏;如果你已是老手,也会有新的收获!
一、new到底做了什么?四步拆解
当你写下:
let p = new Person('张三', 18);JavaScript 引擎实际上执行了以下四个关键步骤:
- 创建一个全新的空对象
- 将这个对象的内部原型(
[[Prototype]])链接到构造函数的prototype属性 - 将构造函数内部的
this绑定到这个新对象,并执行函数体(传入参数) - 如果构造函数显式返回一个对象,则返回该对象;否则返回新创建的对象
这四步缺一不可。尤其是第 2 步,决定了实例能否继承原型上的方法和属性。
注意:第 4 步常被忽略!如果构造函数返回
{}或其他对象,new的结果就是那个返回值,而不是新创建的对象。
二、手写objectFactory:用arguments实现new
现在,我们不用 ES6 的...args,而是用传统的arguments来实现一个objectFactory函数,完全模拟new的行为。
原始代码
function objectFactory() { var obj = new Object(); var constructor = [].shift.call(arguments) constructor.apply(obj, [...arguments]) obj.__proto__ = constructor.prototype return obj; }让我们逐行解析这段“魔法代码”。
第一步:创建一个空对象
var obj = new Object();这等价于obj = {},创建一个普通的空对象。它是未来实例的“容器”。
小知识:
new Object()和{}在大多数情况下行为一致,但{}更高效,也更常用。这里用new Object()只是为了语义清晰。
第二步:从arguments中提取构造函数
var constructor = [].shift.call(arguments)这是整段代码最精妙的部分!
什么是arguments?
arguments是函数内部自动可用的类数组对象。- 它包含调用函数时传入的所有实参。
- 虽然可以通过索引(如
arguments[0])和length访问,但它不是真正的数组,没有push、map、reduce等方法。
例如:
function foo(a, b) { console.log(arguments); // [Arguments] { '0': 1, '1': 2 } } foo(1, 2);为什么用[].shift.call(arguments)?
Array.prototype.shift方法会移除并返回数组的第一个元素。- 但我们不能直接对
arguments调用shift(),因为它没有这个方法。 - 于是我们“借用”数组的方法:
[].shift是Array.prototype.shift的简写。 - 通过
.call(arguments),我们让shift在arguments上下文中执行,成功取出第一个参数!
结果:
constructor得到Person函数,而arguments对象本身也被修改——第一个元素被移除,剩下的就是['李四', 20]。
验证arguments的类型
console.log(Object.prototype.toString.call(arguments)); // "[object Arguments]" console.log(Object.prototype.toString.call([1,2,3])); // "[object Array]"这清楚地表明:arguments不是数组,而是一个特殊的“类数组对象”。
第三步:执行构造函数,绑定this
constructor.apply(obj, [...arguments])apply允许我们指定函数执行时的this值。- 这里把
obj作为this传入Person函数。 [...arguments]使用展开运算符,将类数组arguments转换为真正的参数列表。
关键细节:此时
arguments已经被shift修改过,只包含'李四'和20,正好对应Person(name, age)的参数。
如果没有这一步,obj就不会有name和age属性!
第四步:建立原型链
obj.__proto__ = constructor.prototype这一步至关重要!它让obj能访问Person.prototype上的属性和方法。
- 比如
p2.species能输出'人类',就是因为obj.__proto__ === Person.prototype。 - 同样,
p2.sayHi()能调用,也是因为方法在原型上。
注意:虽然
__proto__是非标准但广泛支持的属性,现代推荐做法是使用Object.create(constructor.prototype)来创建对象并自动设置原型。但为了教学清晰,这里直接操作__proto__有助于理解原型链的建立过程。
第五步:返回对象
return obj;目前我们的实现没有处理构造函数返回对象的情况。严格来说,完整的new模拟应包含:
const result = constructor.apply(obj, [...arguments]); return (typeof result === 'object' && result !== null) ? result : obj;但在当前测试用例中,Person没有显式返回值(即返回undefined),所以直接返回obj是安全的。
三、完整测试:验证功能一致性
function Person(name, age){ this.name = name this.age = age } Person.prototype.species = '人类' Person.prototype.sayHi = function(){ console.log(`你好,我是${this.name}`) } let p1 = new Person('张三', 18) let p2 = objectFactory(Person, '李四', 20) console.log(p1) // Person {name: "张三", age: 18} console.log(p2.age, p2.species); // 20 "人类" // p2.sayHi() // "你好,我是李四"结果完全一致!p2拥有实例属性(name,age)和原型属性(species,sayHi),说明我们的objectFactory成功复刻了new的核心行为。
四、深入arguments:类数组的真相
1.arguments的本质
- 它是一个自动绑定在函数作用域内的对象。
- 在非箭头函数中可用(箭头函数没有自己的
arguments)。 - 它是实时绑定的:修改命名参数会影响
arguments,反之亦然(在非严格模式下)。
function demo(a) { console.log(arguments[0]); // 10 a = 20; console.log(arguments[0]); // 20(非严格模式下) } demo(10);在严格模式下,这种双向绑定被切断,
arguments和参数互不影响。
2. 为什么叫“类数组”?
因为它具备数组的部分特征:
- 有
length - 可通过数字索引访问
- 但没有
Array.prototype上的方法
因此,不能直接调用arguments.map()或arguments.reduce()。
3. 如何安全转换为真数组?
| 方法 | 说明 |
|---|---|
[...arguments] | ES6 最简洁方式,推荐 |
Array.from(arguments) | 语义清晰,支持类数组和可迭代对象 |
Array.prototype.slice.call(arguments) | 传统兼容写法,ES5 时代常用 |
在我们的代码中,[...arguments]既简洁又高效。
五、对比两种实现方式
还有一种用...args的写法:
function objectFactory(constructor, ...args) { var obj = new Object(); constructor.apply(obj, args); obj.__proto__ = constructor.prototype; return obj; }这种方式更现代、更清晰,不需要操作arguments,也避免了shift修改原参数的问题。
但使用arguments的版本更有教学意义:
- 展示了如何在不支持 ES6 的环境中实现相同功能
- 深入理解了
arguments的行为 - 学会了“借用数组方法”的经典技巧
六、总结:不只是代码,更是思维跃迁
通过手写new,我们不仅掌握了对象创建的底层机制,还深入理解了:
- 原型链如何建立(
__proto__ = constructor.prototype) this如何绑定(apply的妙用)arguments的本质与局限(类数组 vs 真数组)- 函数式编程技巧(借用方法、展开运算符)
更重要的是,我们学会了像 JavaScript 引擎一样思考——不再把new当作黑盒,而是理解其每一步的逻辑。
下次当你再看到new Date()或new Map(),你会微微一笑:我知道你在背后干了什么!
编程的最高境界,不是记住 API,而是理解原理。而你,已经走在了这条路上。🚀
附录:完整增强版objectFactory(含返回值处理)
function objectFactory() { const obj = new Object(); const Constructor = [].shift.call(arguments); // 执行构造函数 const result = Constructor.apply(obj, [...arguments]); // 设置原型链 obj.__proto__ = Constructor.prototype; // 如果构造函数返回对象,则返回该对象;否则返回 obj if (result !== null && typeof result === 'object') { return result; } return obj; }这样就 100% 模拟了原生new的行为!
希望这篇详尽又生动的讲解,让你对new和arguments有了全新的认识。动手试试吧,亲手写出属于你的objectFactory!