ARTICLE DETAIL

资讯详情

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

JavaScript继承详解:从原型链到ES6 class,面试与实战全掌握

JavaScript继承详解:从原型链到ES6 class,面试与实战全掌握 1. 继承到底是什么为什么前端面试总要问这几年不管是校招还是社招前端面试几乎绕不开“继承”这个话题。你去翻牛客、掘金的面经十份里面至少有八份会出现“说说 JS 的继承方式”或者“ES6 的 class 继承和原型链继承有什么区别”。很多刚入行的同学觉得这就是个背诵题把几种继承方式的代码背下来就完事了。但面试官问这个问题本质上不是考你背没背过而是想通过继承来看你对 JavaScript 这门语言的核心机制——原型链——理解有多深。继承在 JavaScript 里和 Java、C 这些传统面向对象语言不一样。Java 的继承是类与类之间的事编译期就定死了。JS 里没有“类”就算 ES6 引入了 class 关键字它本质上还是基于原型链的语法糖。也就是说JS 的继承是对象与对象之间的委托关系一个对象可以通过原型链去访问另一个对象上的属性和方法。这个机制理解不到位后面看框架源码、写公共组件、做工具函数封装都会碰到各种莫名其妙的问题。我见过不少候选人代码写得挺溜React、Vue 用得飞起但一问“new 的时候到底发生了什么”就卡壳。再问“ES6 的 class 继承是怎么实现 super 调用的”基本就懵了。其实这些东西都是继承的底层逻辑搞懂了面试是一方面更关键的是你写代码的时候心里有数知道这个对象为什么能调到那个方法知道怎么设计继承结构才能避免踩坑。这篇文章我从头到尾把 JS 里的继承方式捋一遍包括原型链继承、构造函数继承、组合继承、寄生组合继承、ES6 class 继承每种方式都配上代码、分析优缺点、说清楚适用场景。最后还会整理一份面试官常追问的问题清单和排查技巧。不管是准备面试还是想补基础这篇文章应该能帮你省下不少翻资料的时间。2. 原型链继承最基础的继承方式也是所有问题的源头2.1 原型链继承的核心原理先看最经典、也最原始的原型链继承写法function Parent(name) { this.name name || parent; this.hobbies [coding, reading]; } Parent.prototype.sayHi function() { console.log(Hi, I am this.name); }; function Child(age) { this.age age || 18; } Child.prototype new Parent(); Child.prototype.constructor Child; const child1 new Child(20); const child2 new Child(22); console.log(child1.name); // parent console.log(child1.sayHi()); // Hi, I am parent这段代码的核心逻辑就一句话Child.prototype new Parent()。这句话干了两件事第一把 Child 的 prototype 指向了一个 Parent 实例第二这个 Parent 实例内部有一个[[Prototype]]指针指向 Parent.prototype。于是当 child1 访问 name 属性时先在自己身上找找不到就去 Child.prototype 上找再找不到就去 Parent.prototype 上找这就是一条完整的原型链。这里有个细节必须注意——Child.prototype.constructor Child这行。如果不加Child.prototype 的 constructor 会被指到 Parent 上因为 prototype 被整个覆盖了默认的 constructor 指向没了。虽然大多数情况下 constructor 不影响功能但代码规范检查、某些工具库的判断逻辑会用到它所以修正回来是好的习惯。2.2 原型链继承的两大隐患原型链继承最大的问题有两个面试必问。第一个问题引用类型属性会被所有实例共享。上面代码里 hobbies 是数组属于引用类型。当 child1 修改了它child1.hobbies.push(swimming); console.log(child2.hobbies); // [coding, reading, swimming]child2 的 hobbies 也被改了。这不是 child1 和 child2 共享同一个数组的问题——它们实际上就是访问了同一个数组。对基本类型属性比如 name因为赋值操作会直接在实例上创建新属性不会影响其他实例但修改引用类型内部的元素就会波及所有实例。这在真实业务场景下很危险比如一个组件实例修改了公共配置数组结果所有实例的配置都变了。第二个问题无法向 Parent 传递参数。Child.prototype new Parent()在创建 Parent 实例的时候没法接收来自 Child 实例化的参数。Child 的构造函数里想给 Parent 传值做不到。你只能写死new Parent(fixedName)但这样一来所有 Child 实例的 name 都一样继承的意义就大打折扣。所以实际工作中直接用原型链继承的场景不多。但你必须理解它因为它是后面所有继承方式的基石不理解原型链后面看组合继承、寄生组合继承都会一脸懵。3. 构造函数继承与组合继承从解决痛点开始3.1 构造函数继承解决引用共享问题原型链继承的坑主要在引用共享和参数传递上。构造函数继承也叫盗用构造函数继承就是为了解决这两个问题出现的function Parent(name) { this.name name; this.hobbies [coding, reading]; } Parent.prototype.sayHi function() { console.log(Hi, I am this.name); }; function Child(name, age) { Parent.call(this, name); this.age age; } const child1 new Child(张三, 20); const child2 new Child(李四, 22); child1.hobbies.push(swimming); console.log(child1.hobbies); // [coding, reading, swimming] console.log(child2.hobbies); // [coding, reading] console.log(child1.name); // 张三核心就是Parent.call(this, name)。在 Child 构造函数内部把 Parent 当作普通函数调用用 call 把 this 指向当前 Child 实例。这样 Parent 里 this.name name 就相当于执行 child1.name 张三每个实例都建立自己的属性引用类型不再共享。同时还能传参完美的解决了原型链继承的两个痛点。但问题也来了——Parent 原型上的方法继承不到。看代码sayHi 定义在 Parent.prototype 上构造函数继承只复制了 Parent 构造体里的属性没有处理原型链。你调用child1.sayHi()会直接报错。所以构造函数继承单独用代码复用基本为零方法全都得在构造函数里定义那每次 new 一个实例都会创建一份函数内存浪费严重。3.2 组合继承各取所需组合继承就是把原型链继承和构造函数继承结合起来这也是最直观的“取长补短”思路function Parent(name) { this.name name; this.hobbies [coding, reading]; } Parent.prototype.sayHi function() { console.log(Hi, I am this.name); }; function Child(name, age) { Parent.call(this, name); this.age age; } Child.prototype new Parent(); Child.prototype.constructor Child; const child1 new Child(张三, 20); const child2 new Child(李四, 22); child1.hobbies.push(swimming); console.log(child1.name); // 张三 console.log(child2.hobbies); // [coding, reading] console.log(child1.sayHi()); // Hi, I am 张三 console.log(child1 instanceof Parent); // true console.log(child1 instanceof Child); // true思路很清晰用Parent.call(this, name)在实例上建立自己的属性保证引用不共享用Child.prototype new Parent()让 Child 实例能够沿着原型链访问 Parent.prototype 上的方法。属性是私有的方法是共享的这是当时最常用的继承方案也完美支持 instanceof 判断。但组合继承有个效率问题。Parent 构造函数被执行了两次一次是Parent.call(this)一次是new Parent()。第一次在 Child 实例上建立属性第二次在 Child.prototype 上也建立了一份同样的属性。实例上的属性会覆盖原型上的对应属性所以原型上那些属性是多余的纯粹浪费内存。这就是组合继承的瑕疵面试官如果不追问到这个层面说明他水平也一般。3.3 组合继承的优化方向既然new Parent()在原型上创建的属性是多余的那能不能不要它可以但前提是 Child.prototype 仍然要能串到 Parent.prototype 上。这个方向就引出了寄生组合继承——目前公认最优的继承方式下一章重点讲。4. 寄生组合继承面试官眼中的标准答案4.1 从组合继承到寄生组合继承的演进寄生组合继承的思路不再是通过new Parent()来建立原型链而是直接把 Parent.prototype 拿过来用。实现方式有很多最常见的是借助一个空函数做桥接function Parent(name) { this.name name; this.hobbies [coding, reading]; } Parent.prototype.sayHi function() { console.log(Hi, I am this.name); }; function Child(name, age) { Parent.call(this, name); this.age age; } // 核心用一个空函数做中间层 function inherit(Child, Parent) { const prototype Object.create(Parent.prototype); prototype.constructor Child; Child.prototype prototype; } inherit(Child, Parent); const child1 new Child(张三, 20); console.log(child1.sayHi()); // Hi, I am 张三 console.log(child1 instanceof Parent); // true console.log(child1 instanceof Child); // trueObject.create(Parent.prototype)做了什么它创建了一个新对象这个对象的[[Prototype]]指向 Parent.prototype。然后把这个新对象赋值给 Child.prototype。这样 Child.prototype 和 Parent.prototype 之间就建立了委托关系但不会执行 Parent 构造函数就不会在原型上多出一份多余的属性。顺便说一句Object.create是 ES5 引入的方法面试时候前面那些 ES3 时代的写法也可以用这个思路来优化。这也是为什么有些老面试题会说“用 Object.create 实现继承”的原因。4.2 为什么说寄生组合继承是“标准答案”寄生组合继承只调用了一次 Parent 构造函数避免了原型链上多余属性效率高同时保持了 instanceof 的判断能力implements 的语义也没有破坏。原型链清晰属性私有方法共享基本没有硬伤。实际上很多框架和工具库里的继承工具函数都是这个思路。比如 Backbone 的继承、较早版本 React 的React.createClass内部的类继承实现底层逻辑都和寄生组合继承一致。包括 Babel 把 ES6 class 转译成 ES5 代码时编译出来的辅助函数_inherits也是这个模式。你可以把下面这段代码放到 Babel 官网的 Try it out 里看// ES6 class Child extends Parent {} // Babel 转译后的核心 function _inherits(subClass, superClass) { subClass.prototype Object.create(superClass superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.setPrototypeOf(subClass, superClass); }看到没本质就是Object.create加 constructor 修正再补一个Object.setPrototypeOf把静态属性和方法的继承链也接上。所以很多人以为寄生组合继承是个偏门技巧其实它一直在你每天构建的代码里躺着。4.3 寄生组合继承的局限性寄生组合继承也不是万能的。它在处理“多级继承”的时候比如 Child → Parent → GrandParent每层都要手动调用 inherit 函数代码稍显繁琐。另外它仍然没有解决一种场景Parent 有某些实例属性需要依赖私有变量或闭包状态这时候通过原型链共享方法是做不到的。不过这种场景极少见大部分业务代码根本踩不到。如果面试官问“你知道哪些继承方式”并且想把你往深里挖你可以主动提寄生组合继承然后说出“这就是 ES6 class 继承转译后的底层实现”——这一句话就能把话题引向你熟悉的领域面试主动权就到手了。5. ES6 class 继承日常开发最常用底层是语法糖5.1 class 继承的基本写法现在日常开发中大家基本都写 ES6 的 class 了很少手写上面那些原型链操作class Parent { constructor(name) { this.name name; this.hobbies [coding, reading]; } sayHi() { console.log(Hi, I am this.name); } static create() { return new Parent(static); } } class Child extends Parent { constructor(name, age) { super(name); this.age age; } sayHi() { super.sayHi(); console.log(and I am this.age years old); } } const child new Child(张三, 20); child.sayHi(); // Hi, I am 张三 // and I am 20 years oldclass 继承表面上看和 Java 很像但底层依旧是原型链。extends关键字做的事情等价于寄生组合继承那一套把 Child.prototype 的[[Prototype]]指向 Parent.prototype把 Child 的[[Prototype]]指向 Parent。5.2 super 的完整理解super 是 class 继承里最容易被问到的点也是很多人容易搞混的地方。它有两种用法一种是作为函数调用即super(name)只能在 constructor 里用代表“调用 Parent 的构造函数并把 this 绑定到当前实例”。另一种是作为对象访问即super.sayHi()会从 Parent.prototype 上找方法同时方法里的 this 会绑定到当前实例。注意一个细节super.sayHi()里的 this 不是 Parent 的实例而是 Child 的实例。所以 Parent.prototype 上的方法如果依赖 this.name拿到的其实是 Child 实例上的 name。这个机制保证了继承的方法能正确访问子类实例的属性而不是父类原型上的值。很多人在这块会绕晕记住一句话——super 只是用来找方法的方法执行时 this 永远是当前实例。另一个细节是super(name)和this的使用顺序。在 constructor 里必须先调用super()才能访问this。因为 ES6 的 class 继承机制里子类实例的 this 是在执行完父类构造函数后才“初始化”出来的。这在面试里被称为“TDZ暂时性死区的一个表现”。Babel 转译后的代码里会插入一个_this变量来模拟这个过程你可以自己编译看看理解会更深刻。5.3 静态属性和方法的继承ES6 class 还支持静态方法的继承这也是和 ES5 时代手写继承不一样的地方class Child extends Parent {} console.log(Child.create()); // Parent 实例Child.create能调用是因为 class 继承不只是设置了 prototype 的委托还用Object.setPrototypeOf(Child, Parent)把 Child 的[[Prototype]]指向了 Parent。这样 Child 上找不到静态方法时会顺着[[Prototype]]链到 Parent 上找。这个细节面试问到的人不少。如果只实现 prototype 层面的继承静态方法是无法继承的。你可以试试直接用Object.create(Parent.prototype)的方式写继承然后调用Child.create()会得到 undefined。这就是 class 继承在语言层面做得更完善的地方。5.4 内置对象的继承扩展class 继承还有一个 ES5 时代很难做到的事——继承内置对象。比如你想创建一个自定义数组自动去重class UniqueArray extends Array { push(...items) { items.forEach(item { if (!this.includes(item)) { super.push(item); } }); return this.length; } } const arr new UniqueArray(1, 2, 3); arr.push(2, 4, 5); console.log(arr); // [1, 2, 3, 4, 5]ES5 时代通过Child.prototype new Array()这种方式继承数组出来的实例.map()、.filter()返回的结果都不再是自定义类型行为很诡异。ES6 class 继承内置类型时引擎会做特殊处理让返回的新数组继续是子类实例。这在业务里确实用得上我之前封装过一个基于 Map 的 LRU 缓存类也是这么干的。6. 各种继承方式对比一张表看清楚6.1 六种继承方式横评以下是我整理的 JS 常见继承方式对照表覆盖了从 ES3 到 ES6 的主流写法面试前扫一眼能快速回忆继承方式核心实现引用属性是否共享是否支持传参能否继承原型方法父类构造函数调用次数instanceof 是否正常原型链继承Child.prototype new Parent()是否是1是构造函数继承Parent.call(this, ...)否是否1否组合继承Parent.callnew Parent()做原型否是是2是原型式继承Object.create(Parent)是否是0是寄生式继承在原型式基础上增强对象是否是0是寄生组合继承Object.create(Parent.prototype)否是是1是ES6 classextendssuper否是是1是这里面原型式和寄生式继承用的人不多但它们和Object.create的关系密切。特别是当你想创建一个以某个对象为原型的普通对象时Object.create本身就是原型式继承的封装。面试里如果被问到“Object.create 的实现原理”本质上就是在考原型式继承。6.2 面试回答的推荐思路面试官问“讲讲 JS 的继承”不建议按顺序把所有方式背一遍。更好的策略是先抛结论——“JS 继承的本质是原型链委托我按发展脉络讲一下”然后从原型链继承的缺陷讲起引出构造函数继承、组合继承、寄生组合继承最后落到 ES6 class。这样讲既体现你懂历史演进又说明你清楚每种方案的取舍比单纯罗列几种写法强得多。讲到某一种方式的时候把对应的缺陷顺带说一句。比如讲完原型链继承你可以加一句“但它引用共享的问题在真实业务里会很坑所以有了构造函数继承”。面试官能感觉到你是真的理解而不是背稿。6.3 多继承和菱形继承的问题有同学会问JS 能不能实现多继承从语言层面说一个对象的[[Prototype]]只能指向一个对象所以原生不支持和 Java 那样的多继承。但可以通过 Mixin混入模式模拟本质是把多个来源对象的方法拷贝到目标原型上const canWalk { walk() { console.log(walking); } }; const canSwim { swim() { console.log(swimming); } }; class Animal {} Object.assign(Animal.prototype, canWalk, canSwim); const dog new Animal(); dog.walk(); dog.swim();Object.assign 会把 canWalk 和 canSwim 上的方法浅拷贝到 Animal.prototype 上变相实现了“多继承”。但这种做法有个硬伤——如果多个来源里有同名方法后面的会覆盖前面的而且覆盖顺序靠拷贝顺序决定容易出诡异 bug。所以 Mixin 一般只用于无状态的纯方法集合不推荐带属性。还有菱形继承问题简单说就是 B 和 C 都继承 AD 又同时继承 B 和 C那么 D 的实例里 A 的属性会出现两份。JS 因为原生不支持多继承所以基本没有这个问题。但 C 里解决菱形继承用的是虚继承和 JS 的 Mixin 完全不是一个思路。如果你转后端或者平时写 C这个对比还是值得了解一下的。前端面试问菱形继承的概率不大但后端出身或者面全栈岗时偶尔会遇到。7. 实际项目里的继承从面试题到真实场景7.1 组件库中的基类继承前端框架里继承用得最多的地方其实是组件库。拿 Vue 2 来说Vue.extend 就是实现组件继承的 APIconst BaseButton { props: { size: { type: String, default: medium }, disabled: Boolean }, methods: { handleClick(e) { this.$emit(click, e); } }, render(h) { return h(button, { attrs: { disabled: this.disabled }, on: { click: this.handleClick } }, this.$slots.default); } }; const PrimaryButton { extends: BaseButton, props: { type: { type: String, default: primary } }, render(h) { return h(BaseButton, { props: { ...this.$props }, on: { click: e this.$emit(click, e) } }, this.$slots.default); } };Vue 2 的 extends 是“对象继承”思路它把父组件的选项props、methods、生命周期钩子合并到子组件上。生命周期钩子会按顺序执行父先子后。和你手写原型链继承不同Vue 2 的选项合并是浅拷贝加数组拼接自定义策略可以改写。Vue 3 里 Composition API 时代extends 虽然还在但更多人倾向于用 composable 函数组合逻辑面向对象继承在 Vue 3 中变得不那么主流了。React 类组件时代继承用得也很多比如高阶组件HOC本质上就是对组件的“包装”比面向对象继承更灵活。函数组件加 hooks 之后继承更是退居二线。但不管你怎么写理解继承底层逻辑对看源码还是很有帮助。比如 React 源码里Component.prototype.isReactComponent {}这一行就是靠原型属性来标记类组件的不懂原型链就理解不了这个标志位的意义。7.2 业务代码里的继承设计在业务代码里我见过比较典型的继承用法是做“基础业务类”的抽取。举个例子CRM 系统里有多种类型的订单每种订单有公共字段订单号、金额、状态和公共方法查询状态、更新状态但不同类型订单的校验逻辑和结算规则不一样。这时候可以设计一个 Order 基类再派生出 NormalOrder、GiftOrder、RefundOrder 等子类。class Order { constructor(orderNo, amount) { this.orderNo orderNo; this.amount amount; this.status pending; } validate() { // 默认校验逻辑 return this.amount 0; } getStatusText() { const map { pending: 待处理, paid: 已支付, cancelled: 已取消 }; return map[this.status] || this.status; } } class GiftOrder extends Order { constructor(orderNo, amount, giftName) { super(orderNo, amount); this.giftName giftName; this.status paid; } validate() { // 礼品订单要求必须有礼品名称 return super.validate() !!this.giftName; } }这种设计的核心是复用公共逻辑、预留扩展点。Order 基类管公共字段和方法子类只管差异。但要注意别过度设计如果一个基类只有两三个子类且子类之间差异不大用继承反而增加理解成本。我在项目里见过有人把三层结构拆得特别细最后找一个方法定义得往上翻好几个文件调试起来极其痛苦。继承不是目的可维护性才是。7.3 继承中的设计原则实际写代码时我总结了几条关于继承的经验虽然不是 JS 特有但在 JS 的动态特性下尤其重要优先组合而非继承。能用函数组合、hooks、composable 解决的问题不要为了“面向对象”硬上继承。继承层级不要超过三层。超过三层之后心智负担会急剧上升出问题排查成本很高。父类里不要写太多和子类强耦合的逻辑。父类越通用子类越好维护。子类重写方法时尽量复用父类逻辑super.xxx()避免整段复制粘贴。复制粘贴意味着后续父类修复 bug子类不会自动同步。对 JS 来说public、protected、private 都是约定不是强制的。可以通过下划线前缀约定私有属性也可以用 ES2022 的#真正实现私有属性但后者在 Babel 转译和调试时有额外成本项目里需要统一约定。8. 面试追问合集继承相关的常见问题与排查思路8.1 new 的时候JavaScript 到底做了什么这算是继承的“前置知识”面试官极大概率会问。new 一个构造函数时引擎会走这四步创建一个新对象这个对象的[[Prototype]]指向构造函数的 prototype 属性。把构造函数的 this 绑定到这个新对象上并执行构造函数体。如果构造函数没有显式返回对象new 表达式会返回这个新对象。如果构造函数显式返回了一个对象类型那么 new 的结果是那个对象否则返回第一步创建的新对象。第四点很多人会忽略。构造函数里如果 return 了一个普通对象new 出来就不是构造函数的实例了。这在写继承相关的代码时容易踩坑比如你写一个工厂函数想同时支持 new 和直接调用搞不好就返回了错误的对象类型。8.2 prototype 和__proto__的区别这个问题问得频率也挺高。prototype 是函数对象的属性只有函数才有__proto__是每个对象都有的隐式属性指向创建这个对象的构造函数的 prototype。当你访问一个对象的属性时引擎先查对象自身再沿着__proto__指向的原型链往上查。有个记忆技巧函数也是对象所以函数也有__proto__。函数 Foo 的__proto__指向 Function.prototype而 Foo.prototype 是一个普通对象它的__proto__指向 Object.prototype。这个链条画出来就是经典的“原型链全景图”。面试时如果被问到你可以直接在纸上画出来比背定义清晰得多。8.3 class 继承和原型链继承的实际差异除了语法层面的差别class 继承还引入了几个原型链继承没有的机制静态方法继承class 会通过Object.setPrototypeOf把子类构造函数本身的[[Prototype]]指向父类。内置对象如 Array、Map的行为修正子类实例调用内置方法时返回的结果仍然是子类类型。构造函数不能作为普通函数调用class 里的 constructor 直接调用会报错。super()必须在访问 this 之前调用这是所谓的 TDZ 限制。这些问题在面试时都是很好的“上分点”。你提到任何一条面试官都会觉得你确实写过代码、踩过坑而不只是背了八股文。8.4 继承相关 bug 的实际排查技巧最后分享几个我在实际开发中遇到过的继承相关 bug 和排查思路希望能帮你少踩坑。第一个问题是“子类实例修改父类数组其他实例全部受影响”。这种情况基本可以锁定是原型链继承或原型式继承导致的引用共享检查代码里是否出现Object.create(parentObj)或者Child.prototype new Parent()。修复手段一般改成寄生组合继承或 class 继承。第二个问题是“子类方法调用时报错 xxx is not a function”。排查思路是打印实例的__proto__链看看目标方法到底挂在哪个对象上、是否被覆盖、是否因为 constructor 被改掉导致原型链断裂。常见原因是继承后没有修正Child.prototype.constructor。第三个问题是“对象经过 JSON 序列化再反序列化后方法全没了”。这不是继承本身的 bug而是 JSON.parse 只会还原普通对象不会还原原型链。如果你需要保留方法得用 class 的fromJSON类静态方法做手动重建class User { constructor(name, role) { this.name name; this.role role; } canAccess(permission) { return [admin, editor].includes(this.role) || permission read; } static fromJSON(json) { const data typeof json string ? JSON.parse(json) : json; return new User(data.name, data.role); } } const restored User.fromJSON(JSON.stringify(new User(张三, admin))); console.log(restored.canAccess(write)); // true第四个问题是“Object.create(null) 创建的对象没有原型链访问 toString 会报错”。这个其实不算 bug但很多人踩过。如果你想造一个纯粹的字典对象用Object.create(null)可以避免原型链上属性名的干扰但代价就是没有 toString、hasOwnProperty 这些方法需要用Object.prototype.toString.call(obj)这种方式调用。9. 结尾几点实操体会回头再看继承这件事我觉得它最大的价值不是让你在面试里多拿几分而是帮你建立对 JavaScript 对象模型的直觉。我自己在写工具库、组件库之前对原型链也是一知半解后来因为排查一个诡异的“所有实例共享状态”的 bug逼着自己把原型链和几种继承方式完整撸了一遍从那以后再也没被这类问题坑过。如果你现在准备面试我的建议是别死记硬背写法把每种继承方式的“为什么出现”和“有什么缺陷”理清楚。面试官问的时候用发展的脉络来讲会有更好的效果。如果你是日常工作用到继承记住一个原则——能用 class 就用 class真的需要底层控制的时候寄生组合继承是兜底方案。另外一个小技巧遇到对象方法丢失、实例属性相互影响这类问题第一步不是查业务逻辑而是打印几行原型链看看。Chrome DevTools 的 console 里直接展开对象的[[Prototype]]就是原型链的可视化比打十几行 console.log 都直观。我自己排查继承相关 bug 时这个动作是最常用的效率最高。希望这篇文章能帮你把继承这个知识点彻底打通。
返回列表