news 2026/4/16 14:24:55

手写 new:深入 JavaScript 对象创建机制,彻底搞懂 arguments 这个“伪装者”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写 new:深入 JavaScript 对象创建机制,彻底搞懂 arguments 这个“伪装者”

引言

在 JavaScript 的世界里,new是一个看似简单却蕴含深意的操作符。你可能每天都在用它创建对象,但你是否真正理解它背后发生了什么?更有趣的是,在不使用现代语法(如剩余参数...args)的情况下,我们还能借助一个神秘的内置对象——arguments——来实现相同的功能。

本文将带你从零开始手写一个完全模拟new行为的函数,并在此过程中深度剖析arguments对象的本质、特性与陷阱。我们将像 JavaScript 引擎一样思考,揭开对象创建与参数传递的底层逻辑。内容详尽、生动有趣,哪怕你是初学者,也能跟上节奏;如果你已是老手,也会有新的收获!


一、new到底做了什么?四步拆解

当你写下:

let p = new Person('张三', 18);

JavaScript 引擎实际上执行了以下四个关键步骤:

  1. 创建一个全新的空对象
  2. 将这个对象的内部原型([[Prototype]])链接到构造函数的prototype属性
  3. 将构造函数内部的this绑定到这个新对象,并执行函数体(传入参数)
  4. 如果构造函数显式返回一个对象,则返回该对象;否则返回新创建的对象

这四步缺一不可。尤其是第 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访问,但它不是真正的数组,没有pushmapreduce等方法。

例如:

function foo(a, b) { console.log(arguments); // [Arguments] { '0': 1, '1': 2 } } foo(1, 2);
为什么用[].shift.call(arguments)
  • Array.prototype.shift方法会移除并返回数组的第一个元素
  • 但我们不能直接对arguments调用shift(),因为它没有这个方法。
  • 于是我们“借用”数组的方法:[].shiftArray.prototype.shift的简写。
  • 通过.call(arguments),我们让shiftarguments上下文中执行,成功取出第一个参数!

结果: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就不会有nameage属性!


第四步:建立原型链

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的行为!


希望这篇详尽又生动的讲解,让你对newarguments有了全新的认识。动手试试吧,亲手写出属于你的objectFactory

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!