原文: [一篇文章理解 JS 繼承](https://mp.weixin.qq.com/s/Hjzt0DUd6aXIH84vrf0poQ)
說實在話,以前我只需要知道“寄生組合繼承”是最好的,有個祖傳代碼模版用就行。最近因為一些事情,幾個星期以來一直心心念念想整理出來。本文以《JavaScript高級程序設計》上的內容為骨架,補充了ES6 Class的相關內容,從我認為更容易理解的角度將繼承這件事敘述出來,希望大家能有所收獲。
### 1. 繼承分類
先來個整體印象。如圖所示,JS中繼承可以按照是否使用object函數(在下文中會提到),將繼承分成兩部分(Object.create是ES5新增的方法,用來規范化這個函數)。
其中,原型鏈繼承和原型式繼承有一樣的優缺點,構造函數繼承與寄生式繼承也相互對應。寄生組合繼承基于Object.create, 同時優化了組合繼承,成為了完美的繼承方式。ES6 Class Extends的結果與寄生組合繼承基本一致,但是實現方案又略有不同。
下面馬上進入正題。

### 2. 繼承方式
> 上圖上半區的原型鏈繼承,構造函數繼承,組合繼承,網上內容比較多,本文不作詳細描述,只指出重點。這里給出了我認為最容易理解的一篇[《JS中的繼承(上)》](https://segmentfault.com/a/1190000014476341)。如果對上半區的內容不熟悉,可以先看這篇文章,再回來繼續閱讀;如果已經比較熟悉,這部分可以快速略過。另,上半區大量借用了yq前端的一篇繼承文章1。
#### 2.1 原型式繼承
核心:將父類的實例作為子類的原型。
~~~
SubType.prototype = new SuperType()
// 所有涉及到原型鏈繼承的繼承方式都要修改子類構造函數的指向,
// 否則子類實例的構造函數會指向SuperType。
SubType.prototype.constructor = SubType;
~~~
優點:父類方法可以復用。
缺點:
* 父類的引用屬性會被所有子類實例共享
* 子類構建實例時不能向父類傳遞參數
#### 2.2 構造函數繼承
核心:將父類構造函數的內容復制給了子類的構造函數。這是所有繼承中唯一一個不涉及到prototype的繼承。
`SuperType.call(SubType);`
優點: 和原型鏈繼承完全反過來
* 父類的引用屬性不會被共享
* 子類構建實例時可以向父類傳遞參數
缺點:父類的方法不能復用,子類實例的方法每次都是單獨創建的。
#### 2.3 組合繼承
核心:原型式繼承和構造函數繼承的組合,兼具了二者的優點。
~~~
function SuperType() {
this.name = 'parent';
this.arr = [1, 2, 3];
}
SuperType.prototype.say = function() {
console.log('this is parent')
}
function SubType() {
SuperType.call(this)
// 第二次調用SuperType
}
SubType.prototype = new SuperType()
// 第一次調用SuperType
~~~
優點:
* 父類的方法可以被復用
* 父類的引用屬性不會被共享
* 子類構建實例時可以向父類傳遞參數
缺點:調用了兩次父類的構造函數,第一次給子類的原型添加了父類的name, arr屬性,第二次又給子類的構造函數添加了父類的name, arr屬性,從而覆蓋了子類原型中的同名參數。這種被覆蓋的情況造成了性能上的浪費。
#### 2.4 原型式繼承
核心:原型式繼承的object方法本質上是對參數對象的一個淺復制。
優點:父類方法可以復用。
缺點:
* 父類的引用屬性會被所有子類實例共享
* 子類構建實例時不能向父類傳遞參數
~~~
functionobject(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object (person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);
//"Shelby,Court,Van,Rob,Barbie"
~~~
> ECMAScript 5 通過新增 Object.create()方法規范化了原型式繼承。這個方法接收兩個參數:一 個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下, Object.create()與 object()方法的行為相同。——《JAVASCript高級編程》
所以上文中代碼可以轉變為:
~~~
var yetAnotherPerson = object(person);
=> var yetAnotherPerson = Object.create(person);
~~~
#### 2.5 寄生式繼承
核心:使用原型式繼承獲得一個目標對象的淺復制,然后增強這個淺復制的能力。
優缺點:僅提供一種思路,沒什么優點。
~~~
function createAnother(original){
var clone = object(original);
//通過調用函數創建一個新對象
clone.sayHi = function(){
//以某種方式來增強這個對象
alert("hi");
};
return clone;
//返回這個對象
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();
//"hi"
~~~
#### 2.6 寄生組合繼承
剛才說到組合繼承有一個會兩次調用父類的構造函數造成浪費的缺點,寄生組合繼承就可以解決這個問題。
~~~
function inheritPrototype(subType, superType){
var prototype = object (superType.prototype);
// 創建了父類原型的淺復制
prototype.constructor = subType;
// 修正原型的構造函數
subType.prototype = prototype;
// 將子類的原型替換為這個原型
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 核心:因為是對父類原型的復制,所以不包含父類的構造函數,也就不會調用兩次父類的構造函數造成浪費
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
}
~~~
優缺點:這是一種完美的繼承方式。
### 2.7 ES6 Class extends
核心: ES6繼承的結果和寄生組合繼承相似,本質上,ES6繼承是一種語法糖。但是,寄生組合繼承是先創建子類實例this對象,然后再對其增強;而ES6先將父類實例對象的屬性和方法,加到this上面(所以必須先調用super方法),然后再用子類的構造函數修改this。
~~~
class A {}
class B extends A {
constructor() { super();
}
}
~~~
ES6實現繼承的具體原理:
~~~
class A {}
class B {}
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 繼承 A 的靜態屬性
Object.setPrototypeOf(B, A);
~~~
ES6繼承與ES5繼承的異同:
相同點:本質上ES6繼承是ES5繼承的語法糖。
不同點:
* ES6繼承中子類的構造函數的原型鏈指向父類的構造函數,ES5中使用的是構造函數復制,沒有原型鏈指向。
* ES6子類實例的構建,基于父類實例,ES5中不是。
### 3. 總結
* ES6 Class extends是ES5繼承的語法糖
* JS的繼承除了構造函數繼承之外都基于原型鏈構建的
* 可以用寄生組合繼承實現ES6 Class extends,但是還是會有細微的差別
### 參考文章:
[《js繼承、構造函數繼承、原型鏈繼承、組合繼承、組合繼承優化、寄生組合繼承》](https://segmentfault.com/a/1190000015216289)
《JavaScript高級編程》
- js
- js繼承
- keyCode
- 好的網站
- 零散知識點-js
- This
- 對象深拷貝和淺拷貝
- 數組方法
- 數組的深拷貝和淺拷貝
- JS 引擎的執行機制
- js中的new
- 常用正則
- 函數柯里化
- 會修改當前數組的方法
- 不會修改當前數組的方法
- 函數式編程
- 循環遍歷
- 基礎知識
- 異步
- js知識總結
- fileReader
- HTML
- 零散知識點
- html5新特性
- viewport
- CSS
- cursor
- css3新特性
- 水平居中
- 垂直居中
- display解析
- 塊級元素和行內元素
- css技巧和方法
- 清除浮動
- Less
- Sass
- 綜合
- 微信小程序
- 前端面試
- CSS-面試
- JS-面試
- js-web-api
- js知識
- MVC-面試
- jQuery與框架的區別
- 閉包
- promise
- http狀態碼
- cdn
- 離線存儲
- 事件
- web安全
- 性能優化
- 響應式
- 服務器渲染和本地渲染
- 模板是什么?
- VUE流程
- 瀏覽器渲染過程
- this的指向
- new的使用
- HTML-面試
- title和alt區別
- html5元素
- h5新特性
- 圖片格式
- 零散面試總結
- react
- 生命周期-react
- state
- props
- 組件通信
- 虛擬DOM
- 源碼分析
- webstorm-template
- element與component區別
- 組件的理解
- JXS
- vue與react區別
- 16.8版本
- vue
- 生命周期-vue
- 實現流程
- webpack
- 概念
- 入口起點
- 出口
- loader
- 模式
- 插件
- manifest
- redux
- 介紹
- 核心概念
- 三大原則
- 基礎
- action
- reducer
- store
- 數據流
- 高級
- 異步action
- 異步數據流
- middleware
- ES6阮一峰
- ...
- let
- es6箭頭函數
- const
- 塊級作用域
- 頂層對象的屬性
- global 對象
- 變量的解構賦值
- 字符串的擴展
- promise對象
- 正則的擴展
- 數值的擴展
- Math對象的擴展
- 函數的擴展
- 數組的擴展
- 對象的擴展
- symbol
- async函數
- class的基本用法
- Class 的繼承
- Set 和 Map 數據結構
- 開發工具
- 好用的軟件
- chrome插件
- 其他實用工具
- 微信公眾號-前端早讀課
- 【第1352期】map和reduce,處理數據結構的利器
- 微信公眾號-前端大全
- JS 的執行機制
- 一篇文章理解 JS 繼承
- 瀏覽器
- 緩存
- 《Webkit技術內幕》之頁面渲染過程
- 跨域
- 安全
- XSS
- 設計模式
- 發布訂閱模式
- 工廠模式
- MV*模式
- 觀察者模式
- react-router
- 一些小技巧
- js一些小算法
- 1.已知一個數組中的值,在另外一個數組中查找該值
- 累加器
- 數組隨機
- 數組扁平化并去重排序
- Immutable
- 常用命令
- hybrid
- schema封裝
- typescript