第一次被问到“实现 new 操作符”时我愣了几秒——写了好几年 JavaScript天天 new Date()、new 各种对象真要从头实现一个 new脑子里却只有一个模糊的“大概要建个对象、绑一下 this”。后来我发现这个题目几乎所有前端面试都会出现并不是因为它难而是它能把原型链、this 绑定、构造函数返回值这些基础考点一次串起来。不管你是在准备面试还是想真正搞懂 JS 的对象创建机制花半小时把 new 手写一遍绝对不亏。这篇内容我会按自己的实操路径来写先拆解 new 内部到底干了什么再写第一个能跑的版本然后逐个处理边界情况最后给出完整版代码和一些真实场景里的用法。中间会穿插我踩过的坑和排查经验你可以直接照着练习。1. new 操作符到底做了什么1.1 四步拆解先从一个最普通的例子看起function Person(name, age) { this.name name; this.age age; } const p new Person(张三, 18);new Person(张三, 18)这行代码执行时JavaScript 引擎在内部做了四件事创建一个全新的空对象。把这个空对象的原型指向Person.prototype。让Person函数内部的this指向这个新对象并传入参数执行函数体。如果Person函数返回了一个对象类型对象或函数则返回这个返回值否则返回第一步创建的那个空对象。这就是new的全部秘密。很多人背过这四点但没想过每一步为什么存在所以写出来的实现总是漏掉第四步。第四步尤其关键原生的new不是“一定返回新创建的对象”而是“只要构造函数显式返回了对象就听构造函数的”。1.2 一个生活化类比这四步其实特别像办证流程。假设你要去办一张新卡第一步窗口给你一张空白申请表第二步申请表上印好格式模板你按模板填第三步你把自己的信息填进去这张表开始有内容了第四步如果窗口后面突然递给你一张已经办好的卡那你就拿那张卡走人否则就拿你刚填好的申请表。对应到代码里“空白申请表”就是空对象“格式模板”就是Constructor.prototype“填信息”就是构造函数里写this.name xxx“突然递给你一张卡”就是构造函数里的return 某个对象。1.3 为什么返回对象类型的规则如此重要这一条被很多人忽略但它能解释不少诡异现象。比如单例模式里构造函数可以返回事先缓存好的唯一实例let cache null; function Singleton(name) { if (cache) { return cache; } this.name name; cache this; } const s1 new Singleton(A); const s2 new Singleton(B); console.log(s1 s2); // true因为第二次构造函数直接 return 了缓存的实例如果new不理会返回的对象这种写法就不会生效。所以规范明确只要返回值是对象或函数就采用返回值其余情况一律用新对象。基本类型数字、字符串、布尔、null、undefined的返回值全部忽略。这一点在你手写实现时必须一模一样地模拟否则写出来的就是一个“伪 new”。2. 手写一个可用的 myNew2.1 基于 arguments 的第一版先写一个能跑的基础版。思路就是按上面四步来function myNew() { // 取出第一个参数构造函数 const Constructor Array.prototype.shift.call(arguments); // 第一步创建空对象并设置原型 const obj {}; obj.__proto__ Constructor.prototype; // 第二步以 obj 为 this 调用构造函数 const result Constructor.apply(obj, arguments); // 第三步根据返回值决定最终结果 return result ! null (typeof result object || typeof result function) ? result : obj; }注意这里用Array.prototype.shift.call(arguments)而不是arguments.shift()因为arguments是类数组对象没有数组的shift方法。shift会把第一个元素构造函数从arguments里移出来剩下的参数原封不动传给apply。第一版已经能实现new的大致行为function Person(name, age) { this.name name; this.age age; } const p myNew(Person, 张三, 18); console.log(p.name); // 张三 console.log(p.age); // 18 console.log(p instanceof Person); // true2.2 用 Object.create 修正原型第一版用了obj.__proto__ Constructor.prototype。这在现代浏览器里能跑但有两个问题一是__proto__本身是个非标准属性早期被广泛使用但规范上是后来才承认的二是直接改__proto__会带来性能上的额外开销而且很容易让人忽略原型链的完整含义。更推荐的做法是用Object.createfunction myNew() { const Constructor Array.prototype.shift.call(arguments); const obj Object.create(Constructor.prototype); const result Constructor.apply(obj, arguments); return result ! null (typeof result object || typeof result function) ? result : obj; }Object.create(proto)会创建一个新对象并且这个新对象的原型指向proto。它等价于创建一个__proto__指向proto的对象但是更干净、更标准也能保留整个原型链。用Object.create之后obj instanceof Constructor的判断就会自然而然地成立。如果你不理解Object.create和直接写{}的区别记住一句话{}的原型是Object.prototype而Object.create(Constructor.prototype)创建出来的对象它的第一个上级原型是Constructor.prototype再往上一层才是Object.prototype。这决定了一条新对象的属性查找路径。2.3 参数处理apply 还是 call在myNew里调用构造函数时必须用apply而不是call。原因很简单我们需要把arguments里剩余的所有参数一次性传进去。call的传参方式是Constructor.call(obj, arg1, arg2, ...)参数数量是写死的而apply的第二个参数接收一个数组或类数组正好匹配arguments这种不定长场景。如果你用 ES6 语法可以用 rest 参数让代码更直观function myNew(Constructor, ...args) { const obj Object.create(Constructor.prototype); const result Constructor.apply(obj, args); return result ! null (typeof result object || typeof result function) ? result : obj; }这里顺便解释一下...args做了什么它把调用myNew时除去第一个参数之外的所有参数收集成一个真数组。第 2.1 节用shift是从类数组中“抠”出构造函数...args则是在语法层面就把构造函数和其他参数分开最终效果一样。3. 边界情况构造函数返回值这件小事3.1 返回基本类型的情况很多人的实现里对返回值的判断只写了typeof result object这是不完整的。先看这个例子function Foo() { this.a 1; return 42; } const f new Foo(); console.log(f.a); // 1 console.log(f); // Foo { a: 1 }构造函数返回了数字42但new结果还是那个新对象。JS 规范规定只有返回值为对象或函数时“覆盖”才生效其他所有值数字、字符串、布尔、undefined、null都不影响new的默认行为。你可能会问为什么不直接用typeof result object因为typeof null的结果是object这是 JavaScript 从诞生起就存在的历史 bugconsole.log(typeof null); // object所以在判断时必须先排除nullresult ! null (typeof result object || typeof result function)这样null会被挡在“对象”之外构造函数返回null时new还是使用新创建的对象这跟原生行为一致。3.2 返回对象和函数的情况当构造函数返回一个对象new的结果就是那个对象新创建出来的对象被丢弃。这在某些缓存、复用、装饰场景里很常见function CacheFactory(name) { this.name name; return { read: () cache:${this.name}, }; } const c new CacheFactory(main); console.log(c.read()); // cache:main console.log(c.name); // undefined因为返回的不是 this而是那个新的普通对象需要注意函数类型也在“覆盖”范围内。也就是说构造函数返回一个函数时new出来的结果是一个函数对象。这一点很容易漏掉function Factory() { this.x 1; return function () { console.log(hello); }; } const f new Factory(); console.log(f.x); // undefined typeof f; //function所以我在实现里把typeof result function也放进了条件里。3.3 null 这个特殊选手大部分边界情况都集中在返回值判断上。你可以在浏览器控制台里直接验证原生new的行为function ReturnNull() { this.name x; return null; } const r1 new ReturnNull(); console.log(r1.name); // x返回值 null 被忽略这个案例的价值在于如果你只写typeof result object那么null会被错误地当成合法对象函数最终返回null实例化就失败了。这个 bug 在手写实现里非常隐蔽因为它只在构造函数显式return null时出现平时不容易测出来。3.4 箭头函数与 class 的限制新的 JavaScript 特性给“手写 new”增加了一点限制需要提前知道。箭头函数没有自己的prototype也不能作为构造函数。如果你试图myNew(() {})Object.create会直接报错因为Constructor.prototype是undefined而Object.create(undefined)会抛TypeError。class的情况更微妙。class本身也是一个函数但它的内部逻辑要求必须通过new来调用直接apply调用会抛出TypeError: Class constructor cannot be invoked without new。这意味着我的简易实现不能百分之百模拟原生new对class的实例化行为。如果你有“连 class 也要支持”的需求可以换用引擎提供的Reflect.construct(Constructor, args)但这相当于直接调用了底层的实例化能力不属于“手写实现”的范畴了。我个人的建议是作为学习目的先搞清楚 ES5 构造函数下的行为更关键class的限制知道原理即可面试中也很少要求手写实现连class都全覆盖。4. 完整版实现与真实场景扩展4.1 完整版 myNew把前面的讨论收拢成完整版加上类型检查后长这样function myNew(Constructor, ...args) { // 检查合法性构造函数必须是函数 if (typeof Constructor ! function) { throw new TypeError(Constructor is not a function); } // 第一步创建新对象并将原型指向构造函数的 prototype const obj Object.create(Constructor.prototype); // 第二步以 obj 为 this 执行构造函数 const result Constructor.apply(obj, args); // 第三步构造函数返回对象/函数时采用返回值否则使用新对象 return result ! null (typeof result object || typeof result function) ? result : obj; }这个版本我建议直接背下来并在心里标出四个关键点类型检查、Object.create、apply传参、返回值判定。面试时只要这四个点都答到基本不会扣分。4.2 用在工厂函数与依赖注入场景手写new不只是为了面试。在实际项目中很多框架的“实例实例化工具”其实就是这个逻辑的花样变体。举个我实际遇到的场景我在一个跨平台系统里写过一个通用的创建实例工具用于按配置文件动态创建不同类型的处理器。调用方不关心具体构造函数长什么样只要传入类型名和参数工具函数负责从注册表里找到构造函数并实例化const registry { jsonParser: JsonParser, textParser: TextParser, // ... }; function createFromRegistry(type, ...args) { const Constructor registry[type]; if (!Constructor) { throw new Error(Unknown type: ${type}); } return myNew(Constructor, ...args); }这样做的好处是创建过程统一被收口可以在实例化前后统一增加日志、埋点、参数校验而不需要每个调用方都自己new。如果你的项目里“实例化”这个动作出现了很多次不妨试着封装一个这样的工具内部实现和myNew完全一致。还有些依赖注入容器会做更复杂的“自动解析构造函数参数并注入依赖”那是在myNew的基础上加了“读取构造函数形参”的能力。第一步拿参数名、第二步到容器里找对应依赖、第三步还是Object.create加apply那一套。所以理解myNew对阅读这类框架源码很有帮助。4.3 性能与适用场景需要提醒的是自制myNew的每次调用都包含Object.create和apply比直接用语言内置的new多一层封装开销。在性能敏感的高频代码路径上不要拿它替代原生new。它更适合用在“无法直接写 new 关键字”或者“需要统一控制创建流程”的地方。举一个不适合的场景如果你在一个每秒执行几万次的循环里创建对象用原生new就好别再加函数调用层。自制myNew的价值在于灵活性和可观测性而不在于性能。5. 常见错误与排查经验5.1 忘记 new 关键字直接调用构造函数而不加new是新手最常见的错误它的行为极具迷惑性function User(name) { this.name name; } const u User(张三); console.log(u); // undefined console.log(window.name); // 张三非严格模式下 this 指向全局对象当User作为普通函数执行时this指向全局对象浏览器里就是window于是name被挂到了全局函数本身返回undefined。这个 bug 的排查难点在于代码写起来没错控制台也不报红就是拿到的u是undefined。如果你在排查这类问题时发现this指向了全局第一反应就应该是某个构造函数前面漏了new。启用严格模式可以更快暴露问题因为严格模式下this会是undefined给undefined.name赋值会直接抛TypeError。5.2 instanceof 失灵的排查有时你用自己写的“伪 new”创建对象发现instanceof结果不对。大部分原因是创建过程中没有把对象的原型链正确指向构造函数的prototype。比如有人图省事直接写const obj {}; const result Constructor.apply(obj, args); return result;这样obj的原型是Object.prototype而不是某个构造函数构造的原型obj instanceof Constructor自然就是false。如果你在做框架封装时遇到“明明是这个类创建的实例怎么instanceof不过”排查方向有两个创建对象时用的是不是Object.create(Constructor.prototype)构造函数有没有显式返回了其他对象。第二个原因更隐蔽。构造函数一旦返回了新对象instanceof判断的就是那个返回对象的原型链而不是this的原型链。我在实际排查中遇到过两次类似的问题最后都是因为构造函数里有类似“返回缓存单例”的逻辑导致业务方以为拿到的是新实例实际拿到的却是旧实例。5.3 构造函数的 this 绑定混乱有些构造函数内部会调用其他函数并在回调里继续使用this这时this可能会丢function Dog(name) { this.name name; setTimeout(function () { console.log(this.name); // undefinedthis 指向了其他对象 }, 100); }这个问题的根因不是new而是普通函数在回调里的this不会被“绑定”到实例上。解决方案有几种把回调改成箭头函数、在构造函数里const self this、或者用bind(this)。我在排查这类问题时总结出一个经验凡是用到new的地方先确认“构造函数内部的this在异步回调里是否还是实例”。异步回调里的this丢失非常隐蔽它不会立刻报错只会让你在某一天发现回调里拿不到name。5.4 一张速查表把上面这些问题汇总成一张小表方便排查时快速对照症状可能原因检查方向拿到的实例是 undefined构造函数忘了加new且无显式返回值this被挂到全局对象实例的instanceof为 false自定义实现没有把原型指对确认是否用Object.create(Constructor.prototype)实例上的属性缺失构造函数内部this被回调改变查看构造函数里是否用了普通函数回调实例返回了旧对象构造函数显式返回了缓存对象检查构造函数return语句Object.create报错构造函数类型不对比如传入箭头函数确认构造函数是否具有prototype6. 实操体会与后续扩展6.1 我把 myNew 用进了一个模拟实例化工具做完这个练习后我顺手把myNew用进了一个内部项目的模拟实例化工具里。当时的需求是一个统一的创建入口根据传入的配置类型动态创建一系列处理对象并且在创建前后埋日志。这个工具跑了大半年没出过问题。真正让我印象深刻的不是代码本身而是写完它之后我对“对象创建”这件事的认知清晰了很多。以前看到new只觉得是个魔法操作现在我会下意识地把它拆成四步建对象、挂原型、绑 this、看返回。这种拆解思维比代码本身更有价值。6.2 还想深入的话如果你还有精力建议去做三件事第一自己动手在浏览器控制台里比较new和myNew在不同构造函数下的行为差异尤其是“返回对象”“返回函数”“返回 null”三种情况。第二读一遍 ECMAScript 规范里关于new的段落理解[[Construct]]内部方法。你不需要背术语只要把“创建对象、绑定原型、执行构造函数、判断返回”这四步和规范描述对应上就好。第三试试用Reflect.construct写一版支持class的myNew看看它在错误处理上跟手写版有什么区别。我试过之后最大的感受是语言内置能力完善了很多但理解底层机制仍然不可替代。这个练习看似简单却能把 JavaScript 最核心的“对象、原型、this”三座大山串起来。把它彻底吃透比背一百个 API 都值。