
昨天我们聊了原型链知道了JS对象之间是怎么“攀亲戚”的。今天咱们来聊聊继承——也就是怎么让一个对象“认祖归宗”继承另一个对象的属性和方法。从最原始的手动操作到ES6优雅的class语法这中间有好几种姿势每种都有自己的脾气。今天一次性给你盘清楚。前言继承在JS里就像“房产继承”——你想把老爹的房子传给孩子但又不想直接把房子拆了重新盖。不同的继承方式就像是不同的“过户”手段有的简单粗暴有的精细巧妙有的会留下后遗症。今天我们就来盘点JS里实现继承的七种方式从最基础的到最完善的让你在面试官问“JS继承有哪些方式”时能从容应对还能说出各自的优缺点。一、原型链继承最原始的“血脉相连”这是JS里最基础的继承方式核心就是让子类的原型指向父类的实例。functionAnimal(){this.name动物;this.colors[黑色,白色];}Animal.prototype.eatfunction(){console.log(吃东西);};functionDog(){}// 关键一步让Dog的原型指向Animal的实例Dog.prototypenewAnimal();constdog1newDog();constdog2newDog();dog1.colors.push(金色);console.log(dog2.colors);// [黑色, 白色, 金色] —— 哎呀被改了优点实现了方法的继承写起来简单。缺点引用类型的属性会被所有实例共享一个改了大家都改。无法向父类构造函数传参。子类实例的构造函数被“篡改”成了Animal。这种继承就像家族企业祖宗留下的财产比如房产证大家共用一个孙子把房子卖了其他孙子都没了。二、构造函数继承借鸡生蛋为了解决引用共享和传参问题诞生了“借用构造函数”的方式在子类构造函数里调用父类构造函数。functionAnimal(name){this.namename;this.colors[黑色,白色];}Animal.prototype.eatfunction(){console.log(吃东西);};functionDog(name){Animal.call(this,name);// 借调父类构造函数}constdog1newDog(旺财);constdog2newDog(来福);dog1.colors.push(金色);console.log(dog1.colors);// [黑色, 白色, 金色]console.log(dog2.colors);// [黑色, 白色] —— 没被影响console.log(dog1.eat);// undefined —— 父类原型上的方法没继承到优点解决了引用共享问题每个实例有自己的属性副本。可以向父类传递参数。缺点只能继承父类实例属性继承不到父类原型上的方法。方法都在构造函数里定义每次创建实例都会创建新方法浪费内存。这就像“借钱不借地”你把老爹的现金拿来了但老爹的祖宅原型上的方法没拿到。三、组合继承取长补短把原型链继承和构造函数继承结合起来各取所长。functionAnimal(name){this.namename;this.colors[黑色,白色];}Animal.prototype.eatfunction(){console.log(吃东西);};functionDog(name){Animal.call(this,name);// 第二次调用父类}Dog.prototypenewAnimal();// 第一次调用父类Dog.prototype.constructorDog;// 修正构造函数指向constdog1newDog(旺财);constdog2newDog(来福);dog1.colors.push(金色);console.log(dog1.colors);// [黑色, 白色, 金色]console.log(dog2.colors);// [黑色, 白色]dog1.eat();// 吃东西 —— 方法也继承到了优点解决了引用共享问题。可以向父类传参。继承了父类原型上的方法。缺点父类构造函数被调用了两次造成了一定的性能浪费和属性冗余实例上有原型上也有。组合继承是JS里最常用的继承方式虽然有小瑕疵但足够好用。ES6的class本质上就是它的语法糖。四、原型式继承Object.create的雏形这种方式不涉及构造函数直接通过一个对象创建另一个对象。constanimal{name:动物,colors:[黑色,白色],eat(){console.log(吃东西);}};functioncreateObject(obj){functionF(){}F.prototypeobj;returnnewF();}constdog1createObject(animal);constdog2createObject(animal);dog1.colors.push(金色);console.log(dog2.colors);// [黑色, 白色, 金色] —— 又被共享了ES5提供了Object.create()方法就是干这个的。constdog1Object.create(animal);constdog2Object.create(animal);优点不需要构造函数直接基于已有对象创建新对象。缺点引用类型属性还是会被共享。这种继承就像“克隆人”克隆体共享同一个原型一个改了大家都改。五、寄生式继承给继承加个包装在原型式继承的基础上给新对象添加方法。functioncreateDog(original){constcloneObject.create(original);clone.barkfunction(){console.log(汪汪汪);};returnclone;}constanimal{name:动物,eat(){console.log(吃东西);}};constdogcreateDog(animal);dog.bark();// 汪汪汪优点可以在不修改原始对象的情况下添加新功能。缺点和原型式继承一样引用共享问题依然存在而且方法每次创建都会重新生成没法复用。六、寄生组合继承最完美的姿势组合继承的缺点是调用了两次父类构造函数。寄生组合继承解决了这个问题它被认为是JS继承的“最佳实践”。functionAnimal(name){this.namename;this.colors[黑色,白色];}Animal.prototype.eatfunction(){console.log(吃东西);};functionDog(name){Animal.call(this,name);// 只调用一次父类}// 核心用Object.create代替new Animal()Dog.prototypeObject.create(Animal.prototype);Dog.prototype.constructorDog;Dog.prototype.barkfunction(){console.log(汪汪汪);};constdog1newDog(旺财);constdog2newDog(来福);dog1.colors.push(金色);console.log(dog2.colors);// [黑色, 白色] —— 不受影响dog1.eat();// 吃东西dog1.bark();// 汪汪汪优点父类构造函数只调用一次。原型链干干净净没有冗余属性。既有自己的属性副本又继承了原型方法。寄生组合继承是目前最理想的继承实现方式也是ES6class背后做的事情。七、ES6 Class语法糖的终极形态ES6引入了class关键字让继承写起来像其他语言一样优雅。classAnimal{constructor(name){this.namename;this.colors[黑色,白色];}eat(){console.log(吃东西);}}classDogextendsAnimal{constructor(name,breed){super(name);// 调用父类构造函数this.breedbreed;}bark(){console.log(汪汪汪);}}constdog1newDog(旺财,土狗);constdog2newDog(来福,金毛);dog1.colors.push(金色);console.log(dog2.colors);// [黑色, 白色] —— 完美dog1.eat();// 吃东西dog1.bark();// 汪汪汪优点语法清晰易读易写。底层就是寄生组合继承性能好。支持super关键字方便调用父类方法。缺点本质还是原型那一套但语法糖已经够甜了。八、各种继承方式对比总结继承方式优点缺点适用场景原型链继承简单引用共享、不能传参基本不用构造函数继承解决引用共享、能传参不能继承原型方法基本不用组合继承两者优点都有调用两次父类以前常用原型式继承基于已有对象创建引用共享简单对象复用寄生式继承可添加新功能引用共享临时增强对象寄生组合继承完美写法稍复杂ES6之前的首选ES6 class语法优雅、标准需要转译老环境现代开发首选九、实际开发中怎么选无脑选ES6 class。除非你还要兼容IE这种古董否则直接用class和extends就完事了。不仅代码量少而且不容易踩坑。如果你好奇class底层干了啥或者要写一些高阶的继承场景比如混入multiple inheritance那寄生组合继承的手写实现还是值得掌握的。functioninherit(child,parent){child.prototypeObject.create(parent.prototype);child.prototype.constructorchild;}这个工具函数就是寄生组合继承的核心。十、总结从“手动挡”到“自动挡”JS的继承演进史其实就是一部从“手动挡”到“自动挡”的发展史原型链继承是手动挡操作复杂容易出事。组合继承是自动挡但油耗性能稍高。寄生组合继承是CVT平顺又高效。ES6 class是智能驾驶你只管踩油门剩下的交给它。无论哪种方式底层都是原型链那一套。掌握了原型继承的七种姿势不过是排列组合。以后面试官再问“JS继承有哪些方式”你可以从容地把这七种娓娓道来顺便告诉他“但实际开发我选ES6 class。”明天我们将进入JavaScript的另一个核心领域——异步编程从回调地狱到Promise再到async/await带你彻底理清JS的异步世界。如果你觉得今天的文章对你有帮助点个赞让更多人看到。有疑问评论区见我们明天见