前两天面了一个写了两年前端的候选人,前面聊项目、聊工程化表现都还可以,结果一让手写new的实现,他写了两三行就卡住了。他不是不会用new,而是在那一刻想不起来new背后到底做了哪几件事。这个题我特别喜欢,因为它能同时测出原型链、this绑定、引用类型判断和函数调用四块基本功,覆盖面非常广。很多面试官喜欢在初试复试里都带上它,真的一点都不意外。这篇就把new从原理到实现彻底讲透,如果你最近在准备面试,这绝对是值得优先吃透的手写题之一。
1. 为什么面试官爱考 new:一层简单语法背后的四层基础
1.1 new 的日常面貌和它要解决的问题
如果你平时写 React 或者 Vue,可能并不常直接看到new的身影,但在构建工具、基础库、SDK 封装这些底层代码里,new可以说是随处可见。比如new Promise(...)、new URL(...)、new Map()、new Set(),甚至像 Axios 内部也是靠构造器来管理实例的。我们普通业务代码里用new创建一个实例,通常只是几行代码的事:
const person = new Person('张三', 18); console.log(person.name); // 张三 console.log(person instanceof Person); // true但是new到底做了什么,很多人只停留在"调用构造函数、返回实例"这个粗浅的理解上。面试官之所以爱问,是因为它不像Promise那样有复杂的异步逻辑,但它把 JavaScript 最核心的几个机制——原型链、this绑定、函数返回值——全部串在了一个关键字里。一个题写下来,面试官基本就能判断出你对 JS 语言本身的理解程度有多少。
从面试的角度看,这道题还有一个优势:它没有唯一答案。你可以用Object.create写得很简洁,也可以用Object.setPrototypeOf把每一步都写得很显式,还可以用Reflect.construct直接调底层 API。方案越多,越能看出候选人平时对语言机制的思考有多深。
1.2 拆解 new 的四个执行步骤
用标准化的语言描述,new在执行时做四件事:
- 创建一个全新的空对象。
- 把这个空对象的原型指向构造函数的
prototype属性。 - 让构造函数内部的
this指向这个新对象,并执行构造函数代码。 - 根据构造函数的返回值决定最终
new出来的结果:如果构造函数返回了一个对象,则返回这个对象;否则返回第一步新建的那个对象。
这四步看着简单,但每一步背后都是一块独立的知识点。
第一步考察的是对象创建语义。很多人会想到用{}来创建对象,这没错,但要真正和原型挂钩,还得用Object.create(Constructor.prototype),这一步在语义上最贴近new的行为。
第二步考察原型链。new出来的对象能访问构造函数prototype上的方法,靠的就是这条隐式链接。很多人在这一步会漏掉,然后发现自己手写的new出来的对象instanceof永远返回false,就是因为原型没有接上。
第三步考察this绑定的显式指定。函数调用方式不同,this指向就不同,new的机制本质上是在用引擎底层帮我们做了一次apply(this, args)的调用。
第四步考察引用类型判断。这是最容易踩坑的地方,因为typeof对null会返回"object",如果判断条件写得不严谨,一个简单的手写题直接翻车。
我把这四步和你需要会的基本功整理成了下面的对照关系:
| 步骤 | new 内部行为 | 对应知识点 | 推荐写法 |
|---|---|---|---|
| 1 | 创建空对象 | 对象创建 | Object.create(null)或{} |
| 2 | 链接原型 | 原型链 | Object.create(Ctor.prototype) |
| 3 | 以新对象为 this 调用构造函数 | this 绑定 | Ctor.apply(obj, args) |
| 4 | 判断返回值 | 类型判断 | typeof result === 'object' |
理解了这四步,接下来手写就只是把思路翻译成代码的问题了。
2. 手写一个 new:从原理到实现的完整落地
2.1 一个能跑通的初版实现
我们先把四步直接翻译成代码,这一步不需要任何花哨的技巧,纯粹是把上面的表格写出来:
function myNew(Ctor, ...args) { // 第一步:创建新对象,并让它的原型指向 Ctor.prototype const obj = Object.create(Ctor.prototype); // 第二步:执行构造函数,绑定 this const result = Ctor.apply(obj, args); // 第三步:处理返回值 if (result !== null && typeof result === 'object') { return result; } return obj; }这段代码已经能应对绝大多数情况。测试一下:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayName = function () { console.log(this.name); }; const p = myNew(Person, '张三', 18); console.log(p.name); // 张三 console.log(p.age); // 18 console.log(p instanceof Person); // true console.log(p.sayName()); // 张三到这里,一个初版new实现是能跑通的。但如果你以为这样就能在面试里拿到满分,还差得远。面试官接下来大概率会追问几个变体问题。
2.2 更严谨的处理:不要漏掉函数返回值
上面代码里有一个隐藏 bug:构造函数有可能返回一个函数,而不是普通对象。在 JavaScript 的类型体系里,函数也是对象,new之后也应该把这个函数返回出来。但我们的判断条件typeof result === 'object'会漏掉typeof result === 'function'的情况。
举个例子:
function Foo() { this.a = 1; return function () { console.log('I am a function'); }; } const f = new Foo(); typeof f; // 应该返回 function如果按照初版实现,f会被丢失,返回的是obj,这不是标准new的行为。为了严谨,判断条件要加上函数类型:
function myNew(Ctor, ...args) { const obj = Object.create(Ctor.prototype); const result = Ctor.apply(obj, args); // 注意:除了普通对象,函数也要走返回逻辑 return (result !== null && typeof result === 'object') || typeof result === 'function' ? result : obj; }这里有个细节非常值得在面试中主动讲出来:typeof null === 'object',所以必须先在条件里排除null。很多候选人就是死在这一步,写出来的代码在构造函数返回null时直接返回一个null,逻辑就错了。
2.3 用 Object.create 还是显式原型赋值
我在初版实现里用了Object.create(Ctor.prototype),一次搞定"创建对象 + 连接原型"两步。但如果是为了在面试中展示思路,你也可以写得更加显式:
function myNew(Ctor, ...args) { // 显式创建空对象 const obj = {}; // 手动建立原型联系 Object.setPrototypeOf(obj, Ctor.prototype); // 绑定 this 并执行构造函数 const result = Ctor.apply(obj, args); // 判断返回类型 return (result !== null && typeof result === 'object') || typeof result === 'function' ? result : obj; }这两种写法在实现效果上没有本质区别,但Object.create更贴近标准语义,因为它不触发obj上的任何属性操作;而{}+Object.setPrototypeOf更直观,适合讲解。
面试的时候我推荐你从显式版本讲起,讲完再补一句"其实用Object.create可以更简洁",这会让面试官觉得你既懂原理又有代码品位。
顺带一提,有些老文章会写obj.__proto__ = Ctor.prototype,如果有面试官揪这个细节,你可以解释一下:__proto__在规范中早已被标记为 legacy 特性,虽然在现代浏览器里仍然可用,但严谨的项目代码不应该依赖它。用Object.setPrototypeOf或Object.create是更规范的选择。
3. 躲不过的隐藏坑:构造函数返回值和 this 绑定
3.1 返回基本类型和返回引用类型的差异
很多人在手写new之后,会忽略一个非常关键的行为差异:构造函数里的return语句在new调用下,并不总是生效。
我们先看返回基本类型的情况:
function Bar() { this.x = 1; return 100; // 返回一个数字 } const b = new Bar(); console.log(b.x); // 1 console.log(b); // Bar { x: 1 }构造函数的执行结果返回了一个数字,但new出来的b依然是新创建的那个对象,数字直接被忽略了。这其实很好理解:如果new可以随随便便被一个基本类型替代,那new创建的实例就没有意义了。
再看返回引用类型的情况:
function Foo() { this.a = 1; return { b: 2 }; } const f = new Foo(); console.log(f.a); // undefined console.log(f.b); // 2这次new的结果就是那个对象{ b: 2 },之前对this.a的赋值完全被丢掉了。也就是说,当构造函数显式返回一个引用类型时,它就像一个脱轨的火车头,直接把整趟列车带走了。这是new最容易被忽视的规则之一。
3.2 手写 new 时的类型判断技巧
回到手写实现的类型判断上。标准new对于构造函数返回值有两条规则:
- 如果构造函数返回了一个对象(包括函数、数组、正则等),返回这个对象。
- 如果构造函数返回了基本类型(数字、字符串、布尔值、
null、undefined、symbol、bigint),返回新创建的对象。
所以我们的条件应该写成:
const isObject = result !== null && typeof result === 'object'; const isFunction = typeof result === 'function'; return isObject || isFunction ? result : obj;如果你想写得更简练,也可以这样:
if (result && (typeof result === 'object' || typeof result === 'function')) { return result; } return obj;因为null是 falsy 值,所以用result &&可以直接排除null。这个写法在面试里很常见,但建议你在注释里说明为什么能排除null,而不是靠死记硬背。
3.3 实战中遇到的一个真实事故
手写这个规则看起来像是在应付面试,但它真的会在日常开发中埋雷。我之前写一个小型工具库,里面有个createStore构造函数,一开始内部实现时为了提前返回状态对象,随手写了句return { state }。结果所有调用方在new createStore()之后拿到的对象,跟createStore.prototype上的方法彻底失联了,instanceof全部返回false,排查了很久才发现是构造函数返回值把新对象顶掉了。
从那以后我养成了一个习惯:构造函数里尽量不写显式的return引用类型语句。如果你确实需要返回某个对象,那说明这个函数更适合设计成工厂函数,而不是构造函数。在代码评审的时候,看到构造函数里有return语句且返回了对象,我都会多问一句"你确定这是有意的吗",这是很多新手容易忽略的隐患。
4. 面试官还会追问什么:new.target、箭头函数与绑定优先级
4.1 箭头函数为什么不能作为构造函数
这个问题是new的经典延伸。你要明白,箭头函数没有自己的this,它的this在定义时就确定了,作用域继承自外层。所以在new的第三步里,就算我们用apply强行把this指向新对象,箭头函数内部也根本不会理会。
同时,箭头函数也没有prototype属性。new的第二步需要把新对象的原型指向构造函数的prototype,指向一个undefined自然无法完成原型链的组装。所以在引擎层面,new一个箭头函数会直接抛出TypeError。
验证一下:
const Foo = () => { this.a = 1; }; console.log(Foo.prototype); // undefined new Foo(); // TypeError: Foo is not a constructor这个特性和Symbol、BigInt类似,属于"看你会不会从底层机制去推导"。我面了那么多人,能直接答出"箭头函数没有 prototype"的不少,但能同时指出"箭头函数没有自己的 this,apply 无效"的很少。你一定要把两条都说出来。
4.2 new.target 的用途
new.target是 ES6 引入的元属性。在函数内部读取它,可以判断当前函数是被new调用的,还是被普通方式调用的。
function Foo() { if (!new.target) { return new Foo(); } this.name = 'fulishe'; } const obj1 = new Foo(); const obj2 = Foo(); // 这里也能正常得到一个实例 console.log(obj1 instanceof Foo); // true console.log(obj2 instanceof Foo); // true这种写法在一些基础库中很常见,比如Vue的早期版本就鼓励用户用new Vue()来创建实例。在写工具库时,用new.target做一层"即使你忘了new我也帮你new"的保护,体验会好很多。
在手写new的面试场景里,如果面试官追问"你怎么知道你的实现是被 new 调用的",你可以自然地提到new.target,然后顺势写一段上面的示例代码。这个知识点一提出来,瞬间就和普通候选人拉开了差距。
4.3 new 绑定和显式绑定的优先级
JavaScript 里的this绑定有四条规则:默认绑定、隐式绑定、显式绑定(call/apply/bind)和new绑定。这四条规则的优先级从高到低是:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
很多人会忽略这一点,这里我专门写一个容易让人困惑的例子:
function Foo() { this.a = 1; } const obj = { b: 2 }; const BoundFoo = Foo.bind(obj); const instance = new BoundFoo(); console.log(instance.a); // 1 console.log(instance.b); // undefinedFoo被bind绑定到了obj上,但new BoundFoo()执行时,this并没有指向obj,而是指向了new创建出来的新对象。所以instance.b是undefined。原因就是new绑定的优先级比bind更高,这就是面试里常见的"new bind 优先级"考点。
顺着这个思路,你还能引出Reflect.construct。Reflect.construct(Ctor, args, newTarget)是 ES6 提供的专门用来执行new语义的 API,它的第三个参数可以指定新对象的原型,这是普通new表达式做不到的。比如实现继承时,可以用它来让实例对象的原型指向正确的prototype。如果你在回答手写new时提一句"严格来说,手写只是模拟,引擎层面可以直接用Reflect.construct完成",面试官的印象分会直接拉高。
5. 常见问题与排查技巧实录
5.1 手写 new 时的三个高频翻车点
第一,返回值判断漏掉函数。很多人写完new后测试对象类型没有问题,结果构造函数返回一个函数时就出 bug 了。我们要记得加typeof result === 'function'的判断。
第二,忘记排除null。typeof null === 'object'是 JavaScript 的历史遗留,判断时必须用result !== null或result &&这种方式先排除掉。
第三,用instanceof验证原型链时发现是false。这种情况十有八九是第二步没写对,或者不小心把对象的原型指到了别的地方。手写时检查一下Object.create的参数是否是构造函数的prototype。
这三个问题我都整理成了一个速查表:
| 问题表现 | 根本原因 | 解决方案 |
|---|---|---|
| 构造函数返回对象时结果错误 | 类型判断条件遗漏 | 补上引用类型的判断 |
| 构造函数返回 null 时结果错误 | typeof null === 'object' | 增加排空判断 |
| instanceof 结果始终为 false | 原型链接步骤缺失 | 使用Object.create(Ctor.prototype) |
5.2 面试时怎么答才加分
我见过不少候选人把代码写对之后,就安静等下一个问题,这其实浪费了一个展示自己的好机会。回答这道题,你完全可以把思考过程也讲出来。
第一步先说结论:new本质上是语言层面的语法糖,它帮我们完成了"创建对象、连接原型、绑定 this"三个动作,同时处理了构造函数的返回值。
第二步再边说边写,把四步翻译成代码。写的时候可以提一句"如果用Object.create会简洁一些,但为了让你看清楚每一步,我先用显式写法"。
第三步主动展开边界情况:如果构造函数返回引用类型会怎样、箭头函数为什么不行、new.target能干什么。这些不是面试官逼问出来的,而是你主动展示出来的,效果完全不同。
记住,手写题看重的不只是最终结果,更是你的表达是否结构化、是否考虑全面。能主动补全边界情况的候选人,通常代码功底都不差。
5.3 面试之外的实战建议
最后分享一个经验:new的原理不只是面试考题,在平时看源码、写工具时真的会用到。比如你要写一个通用的createInstance函数,允许传入构造函数、参数和自定义原型,实现原理就和手写new高度重合。理解了底层机制,遇到instanceof失效、构造函数返回值异常、this无法绑定这类诡异问题,都能快速定位到根因。
另外,不管你是为了面试还是为了日常开发,我都建议你找时间把Object.create、Reflect.construct、new.target这几个和对象创建相关的 API 一起过一遍。它们单独看都简单,但组合起来就是 JavaScript 对象模型的完整拼图。把这些弄透,写代码时你会明显感觉到自己更能预判程序的运行结果了。
我自己在带团队做 code review 时,经常会给新人说这句话:很多问题不是因为语法不熟,而是因为你不知道一个简单语法背后藏着多少机制。new就是这样——看起来只是一个关键字,拆开之后却是一整套 JavaScript 核心知识体系的浓缩。你能把它讲得越清楚,说明你对这门语言的理解就越扎实。