new操作符在JS中做了什么:构造器执行的四个步骤

作者:拾光

时间:2026-11-03

浏览:371

深入解析JavaScript中new操作符的底层运作机制,详细拆解创建对象、链接原型、绑定this与处理返回值的执行步骤与代码实现。

文章目录
分类:JavaScript核心机制 阅读时长:约 6 分钟 标准规范:ECMAScript 262 [[Construct]]
new操作符在JS中做了什么:构造器执行的四个步骤
在 JavaScript 中,当使用 new 关键字调用一个构造函数时,语言运行时并不仅仅是执行该函数内部的代码,而是按严格次序完成四项底层工作:① 创建一个全新的普通对象;② 将该新对象的原型链关联到构造函数的原型对象;③ 改变执行上下文,将构造函数内部的 this 绑定到该新对象并传入实参执行;④ 判定构造函数的返回值类型以裁定最终产物。理解这四个步骤,是搞懂 JavaScript 面向对象、原型链继承以及上下文绑定机制的关键基石。

一、为什么需要 new:构造器模式的核心职责

JavaScript 本质上是基于原型的动态语言。在引入 ES6 的 class 语法糖之前,开发者完全依托普通函数扮演“构造器”(Constructor)的角色。即使在现代前端开发中,class 底层的实例生成方式仍然遵循传统构造函数的调用规范。

如果直接以普通函数方式调用构造器(例如 Person("张三", 25)),函数内部的 this 在非严格模式下会逃逸绑定至全局对象(浏览器环境为 window,Node.js 环境为 global),在严格模式下则为 undefined,导致不仅无法生成独立实例,反而会污染全局作用域或直接抛出运行时异常。

new 操作符的作用,就是为普通函数包裹一套“生命周期装配流水线”,使得构造函数专注于为新生成的对象赋予状态与属性,而将对象的申请、继承纽带连接以及返回值托底收拢由运行时引擎自动化完成。

二、核心解析:构造器执行的四个详细步骤

根据 ECMAScript 官方规范中关于内置对象内部方法 [[Construct]] 的描述,new Func(...args) 的执行过程严格划分为以下四个阶段:

new操作符在JS中做了什么:构造器执行的四个步骤
图 1:new 操作符四步执行流水线时序图

步骤 1:在堆内存中创建一个全新的空对象

引擎首先向内存管理器申请一块全新的对象存储空间。此时新创建的对象是一个纯净的空对象(在概念层面相当于 {},在规范层面对应 OrdinaryCreateFromConstructor)。

这个空对象目前尚未被赋予任何自有业务属性,但它拥有独立合法的内存地址。后续步骤中所有对属性的装载,都将依托这一块内存空间展开。

步骤 2:将新对象的内部原型链接到构造函数的 prototype

生成空对象后,引擎将该对象的内部隐藏属性 [[Prototype]](浏览器及Node环境中可通过 __proto__ 访问,现代代码推荐使用 Object.getPrototypeOf())指向构造函数的 prototype 属性对象。

这一步正是 JavaScript 实例继承共享属性与方法的根本来源。自此,凡是挂载在 Constructor.prototype 上的公共方法,该新对象均可通过原型链回溯机制顺畅访问。

new操作符在JS中做了什么:构造器执行的四个步骤
图 2:实例化后的内存拓扑与原型链引用结构

步骤 3:将构造函数的 this 绑定到新对象并执行代码

完成原型链搭建后,引擎正式调用构造函数本身。此时关键之处在于改变函数的执行上下文:构造函数体内的 this 被明确强制指向步骤 1 创建出的新对象。

同时,调用 new 时传入的所有实参(arguments)也会一并传递给构造函数。在此阶段,函数体中的赋值语句(如 this.name = name; this.age = age;)被逐行执行,新对象逐步获得其特有的实例属性与状态。

步骤 4:根据函数返回值决定最终输出

构造函数执行完毕后,引擎需要决定 new 表达式最终返回给调用方的具体内容。这里包含一条十分重要却常被忽视的分支逻辑:

  • 若构造函数显式返回了一个对象类型(Object、Array、Function、Date 等引用类型):引擎将放弃在步骤 1 创建的新对象,转而直接返回构造函数显式返回的那个对象。
  • 若构造函数没有显式返回值(默认返回 undefined)或返回的是原始值(Primitive values,如数字、字符串、布尔值、Symbol、null 等):引擎将直接忽略该返回值,如实返回步骤 1 中创建并经过步骤 3 初始化完毕的那个新实例对象。
new操作符在JS中做了什么:构造器执行的四个步骤
图 3:步骤四返回值裁定分支与类型对比决策图
注意事项:在 JavaScript 的历史遗留判定中,typeof null === "object",但在构造函数返回值裁定时,规范将 null 判定为原始值类型。因此,如果构造函数中写了 return null;,new 操作符依然会返回步骤 1 创建的新实例,而绝不会返回 null!

三、代码实现:用 JavaScript 手写实现 myNew

为了彻底吃透这四个步骤,我们可以脱离原生 new,完全用原生 JavaScript 编写一个 myNew 模拟函数,把上述四个动作按顺序具象化:

/**
 * 模拟实现 JavaScript 的 new 操作符
 * @param {Function} Constructor - 构造函数
 * @param {...any} args - 传递给构造函数的参数
 * @returns {Object} 最终的实例化对象
 */
function myNew(Constructor, ...args) {
  // 基础防御:确保传入的 Constructor 是合法的函数
  if (typeof Constructor !== 'function') {
    throw new TypeError('Constructor must be a function');
  }

  // 【步骤 1 & 步骤 2】:创建新对象并将原型链关联到 Constructor.prototype
  // Object.create() 会直接以传入的对象为原型创建一个全新的空对象
  // 若 Constructor.prototype 不是对象(例如为 null),则回退到 Object.prototype
  const prototype = typeof Constructor.prototype === 'object' && Constructor.prototype !== null
    ? Constructor.prototype
    : Object.prototype;
  const instance = Object.create(prototype);

  // 【步骤 3】:将构造函数的 this 绑定到新创建的 instance 对象上,并传入参数执行
  const result = Constructor.apply(instance, args);

  // 【步骤 4】:判定返回值。如果返回值是对象或函数类型(排除 null),则返回它;否则返回 instance
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';

  return (isObject || isFunction) ? result : instance;
}

测试验证与行为对比

我们可以编写对应的测试用例,分别检验普通属性赋值、原型方法继承以及返回值覆盖等场景:

// 1. 定义测试构造函数
function User(name, age) {
  this.name = name;
  this.age = age;
}

// 在原型上挂载共享方法
User.prototype.sayHi = function() {
  return `你好,我是 ${this.name},今年 ${this.age} 岁。`;
};

// 使用自定义 myNew 创建实例
const u1 = myNew(User, '李四', 28);

console.log(u1.name);          // 示例输出: "李四"
console.log(u1.age);           // 示例输出: 28
console.log(u1.sayHi());       // 示例输出: "你好,我是 李四,今年 28 岁。"
console.log(u1 instanceof User); // 示例输出: true
console.log(u1.constructor === User); // 示例输出: true

// 2. 验证步骤四:返回原始值的情况
function PrimitiveReturn(val) {
  this.val = val;
  return "我是字符串原始值";
}
const pObj = myNew(PrimitiveReturn, 42);
console.log(pObj.val); // 示例输出: 42 (字符串被忽略,成功拿到实例)

// 3. 验证步骤四:返回自定义对象的情况
function ObjectReturn() {
  this.status = 'ignored';
  return { custom: 'override' };
}
const oObj = myNew(ObjectReturn);
console.log(oObj); // 示例输出: { custom: 'override' } (原实例被覆盖)

四、适用边界与限制:哪些函数不能被 new?

并非所有函数都能配合 new 操作符调用。在 ECMAScript 规范中,一个函数能否被 new 调用的根本判定标准是:该函数内部是否实现了 [[Construct]] 内部方法。

函数类型 是否拥有 [[Construct]] 能否使用 new 运行时表现与原因说明
普通函数 / 函数声明 具备 可以 拥有 prototype 属性与 [[Construct]] 插槽,默认作为构造器支持
ES6 Class 构造器 具备 必须使用 new 只能通过 new 调用,直接调用会抛出 TypeError: Class constructor cannot be invoked without 'new'
箭头函数 (Arrow Function) 不具备 不能 抛出 TypeError: ... is not a constructor。无 prototype 属性,this 词法绑定外层
对象方法简写 (Method Shorthand) 不具备 不能 例如 const o = { foo() {} },简写方法不挂载 prototype,禁止 new
Symbol / BigInt 函数 内部被禁用 不能 作为基本包装函数受到语言保护,抛出 TypeError: Symbol is not a constructor
ES6 new.target 元属性:在函数或构造器内部,可以通过 new.target 检测当前函数是否通过 new 关键字被调用。若以常规方式调用,new.target 为 undefined;若以 new 实例化,它指向该构造函数本身。很多库利用此特性自动修正调用方式(例如 if (!new.target) return new Person(...args);)。

五、全景归纳与总结

综上所述,new 操作符的核心价值在于规范化了实例创建、继承关联、属性赋予和产物兜底这四个阶段。

new操作符在JS中做了什么:构造器执行的四个步骤
图 4:new 操作符原理全景总结与规范映射表

无论是在手写模拟实现面试题中,还是在日常排查诸如原型方法丢失、构造函数内部属性覆盖或 this 丢失等实际 Bug 时,只要紧扣“创对象 → 链原型 → 绑 this → 判返回”这四条清晰的主线,即可快速定位并解决绝大多数与对象实例化相关的疑难杂症。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章