JavaScript多重继承:类定义顺序影响继承效果的原因及解决方法
这个问题问到点子上了——本质是JavaScript里函数声明提升和原型链动态构建的特性在不同继承场景下的表现差异,咱们掰开揉碎了说:
先搞懂:单继承为啥不受顺序影响?
JavaScript里的函数声明会被提升到当前作用域的顶部,所以哪怕你先写子类再写父类,比如先定义Foo2再定义Foo1,函数本身在代码执行前就已经存在了,不会出现“Foo1未定义”的错误。
更关键的是,原型对象是引用类型:比如你先执行Foo2.prototype = Object.create(Foo1.prototype),此时Foo1.prototype只是个空对象,但后续给Foo1.prototype添加displayMessage方法时,修改的是同一个对象的引用——Foo2.prototype.__proto__指向的就是这个对象,所以Foo2的实例依然能顺着原型链找到这个方法。
简单说:单继承里,原型链的指向是动态关联的,后续修改父类原型,子类的原型链会自动同步。
那链式继承(Foo1→Foo2→Foo3)为啥顺序错了就失效?
看你给出的代码:
function Foo1() { this.message = 'Hello World'; } Foo1.prototype.displayMessage = function() { console.log(this.message); } function Foo3() { Foo2.call(this); } Foo3.prototype = Object.create(Foo2.prototype); // 这里有问题! function Foo2() { Foo1.call(this); } Foo2.prototype = Object.create(Foo1.prototype); var x = new Foo3(); x.displayMessage(); // 报错
问题出在原型赋值的执行顺序上:
- 函数声明提升后,
Foo1、Foo2、Foo3都存在了,但原型赋值是按代码顺序执行的。 - 当执行
Foo3.prototype = Object.create(Foo2.prototype)时,Foo2.prototype还是JavaScript默认给函数创建的空原型(只有constructor属性指向Foo2),还没被后续的Foo2.prototype = Object.create(Foo1.prototype)修改。 - 之后执行
Foo2.prototype = Object.create(Foo1.prototype),其实是给Foo2.prototype替换成了一个全新的对象——这个新对象的原型链指向Foo1.prototype,但Foo3.prototype的原型还是之前那个旧的Foo2.prototype,两者彻底断开了联系。
最终x的原型链是:x → Foo3.prototype → 旧Foo2.prototype → Object.prototype,根本没连上Foo1的原型,自然找不到displayMessage方法。
怎么解决这个问题?
核心原则就是:给子类设置原型之前,必须确保父类的原型已经完成了正确的赋值。有几种靠谱的实现方式:
1. 按继承顺序定义(最直观)
把类的定义和原型设置从顶层父类到底层子类依次写:
function Foo1() { this.message = 'Hello World'; } Foo1.prototype.displayMessage = function() { console.log(this.message); } // 先定义Foo2并设置原型 function Foo2() { Foo1.call(this); } Foo2.prototype = Object.create(Foo1.prototype); Foo2.prototype.constructor = Foo2; // 建议加上,修复constructor指向,更规范 // 最后定义Foo3并设置原型 function Foo3() { Foo2.call(this); } Foo3.prototype = Object.create(Foo2.prototype); Foo3.prototype.constructor = Foo3; var x = new Foo3(); x.displayMessage(); // 正常输出 "Hello World"
2. 用IIFE控制执行顺序
如果没法调整代码顺序,可以用立即执行函数(IIFE)先初始化父类的原型:
function Foo1() { this.message = 'Hello World'; } Foo1.prototype.displayMessage = function() { console.log(this.message); } // 用IIFE先完成Foo2的原型构建 const Foo2 = (function() { function Foo2() { Foo1.call(this); } Foo2.prototype = Object.create(Foo1.prototype); Foo2.prototype.constructor = Foo2; return Foo2; })(); // 现在再定义Foo3就没问题了 function Foo3() { Foo2.call(this); } Foo3.prototype = Object.create(Foo2.prototype); Foo3.prototype.constructor = Foo3; var x = new Foo3(); x.displayMessage();
3. 用ES6 Class语法(推荐)
ES6的Class语法彻底规避了这个问题——Class声明不会被提升,必须先定义父类才能继承,而且语法更简洁:
class Foo1 { constructor() { this.message = 'Hello World'; } displayMessage() { console.log(this.message); } } class Foo2 extends Foo1 { constructor() { super(); // 对应ES5的Foo1.call(this) } } class Foo3 extends Foo2 { constructor() { super(); // 对应ES5的Foo2.call(this) } } const x = new Foo3(); x.displayMessage(); // 正常输出
如果你的运行环境支持ES6,优先用这种方式,代码可读性高,不容易踩原型链的坑。
内容的提问来源于stack exchange,提问作者Stefan Genov

