十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手写 new 实现:一题覆盖原型链、this 绑定与返回值判断

手写 new 实现:一题覆盖原型链、this 绑定与返回值判断 前两天面了一个写了两年前端的候选人前面聊项目、聊工程化表现都还可以结果一让手写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的不少但能同时指出箭头函数没有自己的 thisapply 无效的很少。你一定要把两条都说出来。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 核心知识体系的浓缩。你能把它讲得越清楚说明你对这门语言的理解就越扎实。
返回列表