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

引言

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

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


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

当你写下:

ini 复制代码
let p = new Person('张三', 18);

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

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

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

例如:

scss 复制代码
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 的类型

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


第四步:建立原型链

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


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

相关推荐
浩星2 小时前
css实现类似element官网的磨砂屏幕效果
前端·javascript·css
一只小风华~2 小时前
Vue.js 核心知识点全面解析
前端·javascript·vue.js
2022.11.7始学前端2 小时前
n8n第七节 只提醒重要的待办
前端·javascript·ui·n8n
SakuraOnTheWay2 小时前
React Grab实践 | 记一次与Cursor的有趣对话
前端·cursor
阿星AI工作室2 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕2 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx2 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder2 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy2 小时前
Cursor 前端Global Cursor Rules
前端·cursor
红彤彤2 小时前
前端接入sse(EventSource)(@fortaine/fetch-event-source)
前端