别再死记硬背!3分钟彻底搞懂 new 的底层原理

48 阅读3分钟

引言

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>

image.png


这段代码使用 ES6 的 rest 参数语法(...args 实现了一个模拟 new 操作符的函数 objectFactory,其目标是:

使 objectFactory(Person, '李四', 19) 的行为与 new Person('李四', 19) 完全一致。

解释在后面有 image.png 这是深入理解 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>

image.png

关键代码解析: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)。

image.png

  • 虽然 __proto__ 不是 ECMAScript 标准属性(正式标准中应使用 Object.setPrototypeOf),但在所有主流浏览器中均被支持。
  • 此步骤确保 zzp instanceof Person 返回 true,并能调用 zzp.sayHi()

这种写法不依赖 ES6 新特性,而是通过操作 arguments 对象和手动设置原型链来实现相同的功能,体现了早期 JavaScript 开发中常见的模式与技巧。

🌟 写在最后:new 背后,是 JavaScript 的温柔与秩序

我们花了几十行代码,去还原一个只用 3 个字母就能完成的操作——new
看起来像是“造轮子”,但正是这种“手搓底层”的过程,让我们看清了 JavaScript 并非魔法,而是一套严谨、可推演、可复现的机制

那些看似神秘的 this、原型链、执行上下文……其实都藏在一行行朴素的代码里。
真正的高手,不是记住 API,而是理解规则;不是调用黑盒,而是亲手点亮它。

你今天写的每一行“模拟 new”的代码,都是在和 V8 引擎隔空对话。
而这份对话的底气,就来自你对语言本质的理解。