
new这个操作符写 JavaScript 的人几乎每天都在敲可一旦面试官把它单独拎出来问你说说 new 到底干了什么能答利索的人反而不多。多数人的答案停在那套背了无数遍的四个步骤上创建空对象、绑定原型、执行构造函数、判断返回值。这套答案不算错但它只是个提纲一旦面试官往下追问半句——比如返回值是数组算不算对象原型上放个 null 会怎么样你不用 new 能不能把同样的效果手搓出来——很多人当场就卡住了。我前后带过几个刚入行的朋友准备面试这道题的翻车率比闭包、this、事件循环这些老牌难点都高原因很简单它太熟了熟到没人愿意再往下想一层。这篇就按面试现场的真实节奏来拆从考察意图、底层机制、手写实现一路说到边界情况和追问应对不管你是刚开始刷题的应届生还是工作几年想回头补基础的老手都能从中拎出点能直接用上的东西。1. 这道题到底在考什么先搞懂面试官的评分表1.1 标准四步答案为什么只能拿及格分先把这套标准答案完整摆出来因为后面所有的展开都建立在它上面。new Foo()大体做了四件事第一步在内存里新建一个普通对象第二步把这个对象的原型指向Foo.prototype第三步把构造函数体内部的this绑定到这个新对象上并执行函数体于是构造函数里写的this.xxx xxx全都挂到了新对象身上第四步检查构造函数有没有返回值——如果有且返回值是对象类型那就用这个返回值否则一律返回第一步创建的那个新对象。这四步背下来面试官一般会给个及格分因为它确实说对了主干。但问题在于它太像一份结论摘要看不出你到底有没有在运行时里真的观察过这些行为。我面别人的时候最常见的情况是四步说完我追问一句第二步为什么不是把obj.__proto__赋过去对方就开始含糊了说好像也行吧。这一含糊就把背过和懂的区别暴露干净了。真正拉开档次的是你能不能说出这四步背后的规范行为、能不能指出这套表述里被简化掉的细节以及能不能现场手搓一个等价实现。所以我建议准备这道题的时候把四步当成目录而不是答案每一步都往下追一层追到你能用代码验证为止。1.2 面试官真正想听到的三层递进我把这道题的考察深度拆成三层你可以对照看看自己现在停在哪一层。第一层是行为层说清楚new造成了什么结果。也就是那四步能说明实例是怎么来的方法为什么能从原型上找到。第二层是机制层说清楚为什么必须这么设计。比如为什么原型要指向Foo.prototype而不是拷贝一份方法过去——这里就牵扯到内存复用和方法共享的问题为什么返回值的判断只认对象不认原始值——这里牵扯到原始值无法承载引用语义为什么this要先绑定再执行而不是先执行再绑定——因为构造函数体里每一步都在往this上写东西顺序反了就没地方落笔。第三层是实现层在白板或编辑器里手写一个myNew能跑通常规用例也能扛住几个刁钻的边界同时知道自己手写版本和原生new在哪些地方还有差距。面试时间有限通常聊到第二层就差不多了如果你能顺手把第三层演示出来这一题基本就锁分了。下面几节我就按这个顺序往下推。2. 底层机制拆解从构造函数调用到对象落地的完整链路2.1 原型指向这一步到底发生了什么第二步是整个new里最容易被讲糊的一步也是最能体现功底的一步。当我们说把新对象的原型指向Foo.prototype本质上是新对象内部那个隐藏属性[[Prototype]]被设置成了Foo.prototype这个对象。注意是指向而不是复制新对象身上并没有多出一份方法副本它只是记住了一个找不到东西时去哪里问的地址。这就解释了一个新手经常困惑的现象给原型上加方法已经创建出来的实例立刻就能用。我见过有人问为什么我给Foo.prototype.sayHi赋值之后之前new出来的实例也能sayHi答案就在这里——实例从来没持有方法的副本它每次访问属性都是顺着[[Prototype]]这条链现场去原型对象上取。你要是把方法直接写在构造函数体里this.sayHi function(){}那才是每个实例各持一份一万个实例就是一万个函数对象。提示判断方法该放原型还是放实例标准很简单——需要每个实例各自独立状态的放实例比如this.name所有实例行为一致的放原型比如sayHi。放错地方要么浪费内存要么共享污染。那为什么手写的时候推荐用Object.create(Foo.prototype)而不是obj.__proto__ Foo.prototype第一__proto__本质是个访问器属性挂在Object.prototype上用Object.create(null)造出来的对象根本没有它写法不可靠第二赋值给__proto__在引擎层面属于改原型链操作会触发一系列去优化性能很差第三语义上也别扭Object.create从名字上就表达了以某个对象为原型造一个新对象的意图读代码的人一眼就懂。2.2 返回值判定为什么对象留、原始值丢第四步的规则只有一句话构造函数返回对象就替换返回原始值就忽略但这句话背后有明确的取舍。原始值number、string、boolean、null、undefined、symbol、bigint在 JS 里没有独立的身份它们是按值传递的new出来一个数字完全没有意义——你没法在这个数字上挂属性也没法让它参与原型链查找。所以规范干脆把原始值返回值直接丢掉统一回退到新建的对象上。这里有几个高频踩坑点我在面试里几乎每场都会问其中一两个构造函数返回值new的实际结果说明不写return新对象默认返回undefined被忽略return 123新对象原始值被忽略return abc新对象字符串也是原始值忽略return null新对象null是原始值特别注意return {}那个字面量对象对象类型替换return []那个数组数组是对象替换return function(){}那个函数函数也是对象替换return new Date()那个日期对象替换null这一行是重灾区。很多人凭直觉觉得null是空对象应该算对象类型实际上typeof null object是 JS 早期遗留的著名设计缺陷在返回值判定里null是原始值。所以手写实现里那句判断必须写全result ! null (typeof result object || typeof result function)。少了! null这一半return null的用例就错了。至于为什么要把function单独列出来因为typeof对函数返回的是function而不是object。如果你的判断只写typeof result object构造函数return function(){}的时候就会误判成原始值、丢掉函数这又是一个经典翻车点。2.3prototype万一不是对象会发生什么这是我认为这道题最有含金量的追问。Object.create(Foo.prototype)要求参数必须是对象或null如果你手动把Foo.prototype改成了null或者某个原始值原生的new会怎么办规范里的处理是构造函数的prototype如果不是对象就退回用内置的Object.prototype作为新对象的原型。也就是说function Foo() {} Foo.prototype null; const f new Foo(); console.log(Object.getPrototypeOf(f) Object.prototype); // true而如果你手写实现里直接Object.create(Foo.prototype)Foo.prototype为null时Object.create(null)造出来的是个没有原型的裸对象f.toString()直接就报错了行为跟原生不一致。正确的兜底写法是这样const proto Ctor.prototype; const isValidProto proto ! null (typeof proto object || typeof proto function); const obj Object.create(isValidProto ? proto : Object.prototype);多数人写myNew的时候压根没想过这个分支因为平时没人会去改prototype。但面试里主动提一句如果prototype不是对象原生会退回Object.prototype手写版本要补这个兜底这就是明显强于平均水平的信号。3. 手写myNew从能跑通到能扛住追问3.1 最小可用版本的逐行拆解先给一个最朴素的版本把四步一一对应上去方便你建立代码和步骤的映射感function myNew(Ctor, ...args) { // 第一步建一个空对象同时把原型挂上去1、2 两步合并了 const obj Object.create(Ctor.prototype); // 第三步把 this 绑到 obj 上执行构造函数 const result Ctor.apply(obj, args); // 第四步返回值是对象就用它否则用 obj const isObjLike result ! null (typeof result object || typeof result function); return isObjLike ? result : obj; }逐行说下为什么这么写。Object.create(Ctor.prototype)一行同时完成了第一步和第二步因为用Object.create指定原型造对象比先{}再赋__proto__更准确也更高效。第二步用Ctor.apply(obj, args)而不是Ctor.call(obj)是为了把调用new时传入的参数原封不动透传进构造函数——这一点很容易忽略写完之后new Foo(1, 2)变成myNew(Foo)就把参数丢了。第三步这里有个细节apply的第一个参数就是构造函数体内部的this所以构造函数里所有的this.xxx全部落到obj上。这也解释了为什么构造函数用普通函数写、不能用箭头函数——箭头函数没有自己的thisapply绑不上去。最后那句判断我习惯把是不是对象抽成一个单独的判断因为这里最容易写漏null和function两种情况。3.2 补上参数校验和原型兜底能跑通之后把前面提到的两个坑补上代码就稳了function myNew(Ctor, ...args) { if (typeof Ctor ! function) { throw new TypeError(${String(Ctor)} is not a constructor); } const proto Ctor.prototype; const isValidProto proto ! null (typeof proto object || typeof proto function); const obj Object.create(isValidProto ? proto : Object.prototype); const result Ctor.apply(obj, args); const isObjLike result ! null (typeof result object || typeof result function); return isObjLike ? result : obj; }第一个typeof判断是防呆因为你要是把myNew(123)传进去Object.create(undefined)会抛一个语义不清的错提前拦一下体验更好。第二个兜底就是上一节说的prototype不是对象时的处理。这两处加起来不到十行但在面试里能提供的我确实想过边界的信号非常强。我个人的经验是面试官看你手写代码往往不是在挑语法错误而是在看你会不会主动想异常分支。很多人写完主流程就停了等面试官提醒才补那种感觉完全不一样。3.3 用Reflect.construct写出更贴近原生的版本上面那版能覆盖 90% 的场景但跟原生new比还差一点它没法正确处理new.target也没法手写class的派生类构造。new.target是 ES6 加进来的元属性在一个函数内部访问它如果这个函数是被new调用的new.target就是那个构造函数本身如果被普通调用它就是undefined。这个特性常被用来做必须用 new 调用的保护function Foo() { if (!new.target) { throw new TypeError(Foo 必须通过 new 调用); } this.a 1; }如果你想知道更原生的写法可以借Reflect.constructfunction myNew2(Ctor, ...args) { if (typeof Ctor ! function) { throw new TypeError(${String(Ctor)} is not a constructor); } return Reflect.construct(Ctor, args); }Reflect.construct(target, args, newTarget)第三个参数不传时默认等于target它的行为和new target(...args)几乎完全一致包括new.target的取值、class构造函数的处理、原型链的建立。面试里如果时间充裕可以把这一版也抛出来作为我知道还有更严谨的实现方式的补充然后再说明为什么面试时更愿意展示上面那个手动版本——因为它能体现你对四步的理解而Reflect.construct更像是把问题交给引擎。3.4 一组能验证每个分支的测试用例写完不测等于没写我整理了一份覆盖主要分支的用例你可以直接拿去跑function Person(name, age) { this.name name; this.age age; } Person.prototype.sayHi function () { return hi, ${this.name}; }; // 用例 1常规实例 const p myNew(Person, A同学, 20); console.log(p.name, p.age); // A同学 20 console.log(p.sayHi()); // hi, A同学 console.log(p instanceof Person); // true // 用例 2构造函数返回对象应替换 function ReturnObj() { this.x 1; return { y: 2 }; } console.log(myNew(ReturnObj).y); // 2 console.log(myNew(ReturnObj).x); // undefined // 用例 3构造函数返回原始值应忽略 function ReturnPrim() { this.x 1; return 42; } console.log(myNew(ReturnPrim).x); // 1 // 用例 4返回 null应忽略 function ReturnNull() { this.x 1; return null; } console.log(myNew(ReturnNull).x); // 1 // 用例 5返回函数应替换 function ReturnFn() { return function () { return inner; }; } console.log(myNew(ReturnFn)()); // inner跑完之后重点看用例 4 和用例 5这两个是最能检验判断条件写全没有的。用例 3 则验证了apply的返回值确实被正确丢弃。我建议你在本地把这些用例跑一遍再故意把判断条件改坏比如去掉! null亲眼看它挂掉一次比读十遍解释印象都深。4. 边界情况与高频追问这些坑我几乎每场都见4.1new和普通函数调用到底差在哪这个追问出现的频率极高我一般用一张表答清楚又省时间对比项new Foo()Foo()直接调用是否创建新对象创建不创建函数内部this指向新创建的对象严格模式下undefined非严格模式指向全局对象返回值对象则用对象否则用新对象返回什么就是什么new.target等于Fooundefined原型链新对象原型是Foo.prototype无关这张表能自然带出下一个话题this的绑定规则。你要是能在回答里顺势提一句new绑定是this四种绑定规则里优先级最高的比显式bind还高所以bind过的函数被new调用时bind指定的this会被忽略那这一段的深度就够了。bind那个点值得单独展开一下。const BoundFoo Foo.bind({ a: 1 })然后new BoundFoo()实例的原型仍然是Foo.prototype而不是被绑定的{ a: 1 }。这就是new绑定优先级高于显式绑定的具体表现也是很多人面试时答不上来的细节。4.2 哪些函数不能被new为什么这个问题看着零碎其实考的是一条主线一个函数能不能被new取决于它在内部有没有[[Construct]]这个内部方法。类型能否new原因普通函数声明 / 函数表达式能有[[Construct]]有prototype箭头函数不能没有[[Construct]]也没有prototype对象里的方法简写{ foo() {} }不能方法简写不生成构造函数能力class声明能但必须用new直接调用会抛错async函数不能没有[[Construct]]generator函数不能没有[[Construct]]内置构造器Date、Map、Set、Proxy能有[[Construct]]Symbol、BigInt不能设计上只允许通过调用生成原始值Math、JSON、Reflect不能是普通对象不是函数箭头函数那一条可以顺手补充你可以在控制台打印一下(() {}).prototype结果是undefined。没有prototype属性new的第二步无处可指引擎干脆在第一步就拒绝执行直接抛TypeError: xxx is not a constructor。class那一条也很值得说。class Foo {}之后如果你写Foo()直接调用会抛TypeError: Class constructor Foo cannot be invoked without new。这个报错本质上是引擎在[[Call]]内部方法里加的一道检查专门为class开的特例。这也解释了为什么class的prototype属性是writable: false——引擎从属性层面就堵死了你手动替换原型的路子。4.3 常见问题速查表与排查思路我在帮人 review 手写实现的时候遇到的问题高度集中在几个点上整理成表方便你自查症状可能原因修复方式instanceof判断为false用了{}创建对象再手工补属性忘了设原型改用Object.create(Ctor.prototype)构造函数的参数全丢了用Ctor.call(obj)没透传args改成Ctor.apply(obj, args)或Reflect.constructreturn null的用例挂掉判断条件漏了result ! null补上! null的判断return function(){}的用例挂掉只判断了typeof object补上typeof function构造函数里this是undefined构造函数写成了箭头函数apply绑不上构造函数必须用普通函数class构造函数报错手写的apply调用class会失败改用Reflect.construct实例方法访问报错prototype被手动改成null或原始值加兜底退回Object.prototype构造函数体里有new.target检查时报错手写版本透传不了new.target改用Reflect.construct(Ctor, args, Ctor)这张表里前四条是写代码时就能自查的后四条是面试官追问时才暴露的。我的建议是准备的时候先把自己写的版本逐条对着表验一遍尤其是最后一条new.target真正能想到的人不多。注意面试里被问到你的实现和原生 new 有什么区别时别硬说完全一致。老老实实承认new.target和class派生构造这两块手写版本处理不了主动指出可以用Reflect.construct补齐这种知道自己不知道什么的回答比强行说一样更能拿分。5. 答题策略怎么把这道题答出层次感5.1 一套可以直接搬走的回答框架面试现场时间紧张我一般会按结论先行、逐层展开、主动收口的节奏组织答案你可以照着套。开口先给结论new的核心是创建对象、链接原型、绑定 this 执行、判定返回值这四件事一句话把主干交代清楚。接着用两三句话把每件事的具体行为说一遍配一个小例子比如function Foo(){}; Foo.prototype.x 1; new Foo().x的结果。然后主动往下走一步说明第二步的本质是把新对象的[[Prototype]]指向Foo.prototype因此实例访问属性时会顺着原型链查找这也是方法要放在原型上的原因。最后抛出手写实现边写边讲每个分支为什么这么处理。这个节奏的好处是面试官不需要一句一句追问你自己就把深度做上去了。我在实际复盘里发现主动往下推一层的候选人后续追问的数量明显变少因为该说的你都说完了。5.2 三个能主动抛出的加分点有些点不是必答内容但主动提出来能明显加分我按性价比排了序。第一个是原型链的终点。new Foo()出来的实例[[Prototype]]指向Foo.prototype再往上指向Object.prototypeObject.prototype的[[Prototype]]是null。这条链说出来说明你不是孤立地记new而是把它放进了整个对象模型里。第二个是**constructor属性的坑**。Foo.prototype.constructor默认指向Foo但如果你写Foo.prototype { sayHi(){} }整个原型对象被替换掉constructor就丢了变成从Object.prototype继承来的Object。这也是很多老代码里instanceof判断出错的原因之一。修复方式是在新原型对象上手动补一句constructor: Foo。第三个是**new与继承的关系**。ES5 时代实现继承标准套路是Child.prototype Object.create(Parent.prototype)加上Child.prototype.constructor Child再在子类构造函数里Parent.call(this, ...)。到了 ES6class的extends内部也是这套逻辑只不过引擎帮你包好了。能把这层对上说清楚说明你对new的理解已经延伸到继承模型了。5.3 几处容易说错的表述最后提醒几个我在别人答案里反复听到的错误说法避开它们能少丢不少分。别把新对象的__proto__指向Foo.prototype当成规范表述。__proto__只是个历史遗留的访问器属性规范里描述的是内部槽[[Prototype]]__proto__只是访问它的其中一种方式还有Object.getPrototypeOf和Object.setPrototypeOf。面试里说[[Prototype]]更准确。别说new会把构造函数里的属性复制到新对象上。没有任何复制发生构造函数是在新对象作为this的前提下被执行了一遍this.xxx xxx是赋值操作不是复制。别说构造函数返回undefined时用undefined。这里有个很细的区分不写return和写return undefined在返回值的处理上是一样的都会被判定为原始值然后忽略。但如果有人问构造函数里写return后面什么都不跟那结果也是undefined同样忽略。还有一个容易含糊的点new Foo不带括号和new Foo()是等价的参数为空时括号可以省略。这个冷知识面试里不一定用得上但偶尔会有人拿它来测你对语法细节的熟悉程度知道总比不知道好。我在实际带人准备面试的过程中体会到这道题真正的门槛不在记住几个步骤而在你愿不愿意为一个天天在用的东西往下多挖三层。四步背下来十分钟手写实现跑通半小时把边界情况和追问都捋一遍大概一个晚上但那一个晚上换来的东西会在后面所有跟原型、this、继承相关的问题里反复兑现。