引言
在 JavaScript 的世界里,new 是一个看似简单却蕴含深意的操作符。你可能每天都在用它创建对象,但你是否真正理解它背后发生了什么?更有趣的是,在不使用现代语法(如剩余参数 ...args)的情况下,我们还能借助一个神秘的内置对象------arguments------来实现相同的功能。
本文将带你从零开始手写一个完全模拟 new 行为的函数 ,并在此过程中深度剖析 arguments 对象的本质、特性与陷阱。我们将像 JavaScript 引擎一样思考,揭开对象创建与参数传递的底层逻辑。内容详尽、生动有趣,哪怕你是初学者,也能跟上节奏;如果你已是老手,也会有新的收获!
一、new 到底做了什么?四步拆解
当你写下:
ini
let p = new Person('张三', 18);
JavaScript 引擎实际上执行了以下四个关键步骤:
- 创建一个全新的空对象
- 将这个对象的内部原型(
[[Prototype]])链接到构造函数的prototype属性 - 将构造函数内部的
this绑定到这个新对象,并执行函数体(传入参数) - 如果构造函数显式返回一个对象,则返回该对象;否则返回新创建的对象
这四步缺一不可。尤其是第 2 步,决定了实例能否继承原型上的方法和属性。
注意:第 4 步常被忽略!如果构造函数返回
{}或其他对象,new的结果就是那个返回值,而不是新创建的对象。
二、手写 objectFactory:用 arguments 实现 new
现在,我们不用 ES6 的 ...args,而是用传统的 arguments 来实现一个 objectFactory 函数,完全模拟 new 的行为。
原始代码
javascript
function objectFactory() {
var obj = new Object();
var constructor = [].shift.call(arguments)
constructor.apply(obj, [...arguments])
obj.__proto__ = constructor.prototype
return obj;
}
让我们逐行解析这段"魔法代码"。
第一步:创建一个空对象
ini
var obj = new Object();
这等价于 obj = {},创建一个普通的空对象。它是未来实例的"容器"。
小知识:
new Object()和{}在大多数情况下行为一致,但{}更高效,也更常用。这里用new Object()只是为了语义清晰。
第二步:从 arguments 中提取构造函数
css
var constructor = [].shift.call(arguments)
这是整段代码最精妙的部分!
什么是 arguments?
arguments是函数内部自动可用的类数组对象。- 它包含调用函数时传入的所有实参。
- 虽然可以通过索引(如
arguments[0])和length访问,但它不是真正的数组 ,没有push、map、reduce等方法。
例如:
scss
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 的类型
javascript
console.log(Object.prototype.toString.call(arguments)); // "[object Arguments]"
console.log(Object.prototype.toString.call([1,2,3])); // "[object Array]"
这清楚地表明:arguments 不是数组,而是一个特殊的"类数组对象"。
第三步:执行构造函数,绑定 this
arduino
constructor.apply(obj, [...arguments])
apply允许我们指定函数执行时的this值。- 这里把
obj作为this传入Person函数。 [...arguments]使用展开运算符 ,将类数组arguments转换为真正的参数列表。
关键细节:此时
arguments已经被shift修改过,只包含'李四'和20,正好对应Person(name, age)的参数。
如果没有这一步,obj 就不会有 name 和 age 属性!
第四步:建立原型链
ini
obj.__proto__ = constructor.prototype
这一步至关重要!它让 obj 能访问 Person.prototype 上的属性和方法。
- 比如
p2.species能输出'人类',就是因为obj.__proto__ === Person.prototype。 - 同样,
p2.sayHi()能调用,也是因为方法在原型上。
注意:虽然
__proto__是非标准但广泛支持的属性,现代推荐做法是使用Object.create(constructor.prototype)来创建对象并自动设置原型 。但为了教学清晰,这里直接操作__proto__有助于理解原型链的建立过程。
第五步:返回对象
kotlin
return obj;
目前我们的实现没有处理构造函数返回对象的情况 。严格来说,完整的 new 模拟应包含:
ini
const result = constructor.apply(obj, [...arguments]);
return (typeof result === 'object' && result !== null) ? result : obj;
但在当前测试用例中,Person 没有显式返回值(即返回 undefined),所以直接返回 obj 是安全的。
三、完整测试:验证功能一致性
javascript
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,反之亦然(在非严格模式下)。
ini
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 的写法:
ini
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(含返回值处理)
javascript
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!