JS繼承的一些見解
# JS繼承的一些見解
> js在es6之前的繼承是五花八門的。而且要在項目中靈活運用面向對象寫法也是有點彆扭,更多的時候還是覺得面向過程的寫法更爲簡單,效率也高。久而久之對js的繼承每隔一段時間就會理解出現困難。所以這次我要把對對象的理解寫下來,這樣應該就深刻一點了。
我們先來看看一個對象是怎麼生成的
```javascript
// 三種創建對象的方法
var obj = {}
var obj2 = new Object()
var obj3 = Object.create(null)
// 創建一個空字符串對象
var obj4 = new String()
obj.constructor === obj2.constructor // true
obj.__proto__ === obj2.__proto__ === Object.prototype // true
obj4.__proto__ === String.prototype // true
obj4.__proto__.__proto__ === Object.prototype // ture
```
> 這三種方法,前面兩種是一樣的。它們創建的空對象都具有原型,而第三種方式創建的是一個真正意義上的空對象,它不繼承任何屬性;而obj4呢是一個字符串對象。看下圖的對比:
下面對象除了obj3都有個__proto__的隱性屬性,這個屬性指向的是該創建該實例的構造函數的原型。
![js對象圖1](http://p0639a4mt.bkt.clouddn.com/1523691451000-012.jpg "js對象圖1")
這裏obj和obj2雖然是空對象,不過它們具有Object構造函數的屬性的方法,而obj4除了有obj擁有的屬性和方法,它還具備了String擁有的屬性和方法;而obj3是真正的空對象,它的__proto__指向的是null。
##### 我們再將obj4全部展開看看:
![js對象圖2](http://p0639a4mt.bkt.clouddn.com/1523692671000-427.jpg "js對象圖2")
##### 再來看看它們之間的關係:
![js對象圖3](http://p0639a4mt.bkt.clouddn.com/1523695882000-212.png "js對象圖3")
> 如上圖,我舉得例子是Object和String這兩個原生具有的構造器。它們的關係和我們平時寫的父類和子類之間的關係是一樣的,所有的類最終都會指向Object。
在es5的時代,我們用到的繼承最簡單的就是原型鏈繼承了
```javascript
// 我們先創建一個父類
function Super (name) {
this.name = name
this.type = '我是父類'
}
Super.prototype.move = function () {
console.log(this.name + '在跑')
}
// 子類繼承父類
function Child (name) {
this.name = name
}
Child.prototype = new Super()
// 原型鏈繼承,子類原型的私有方法和屬性要在繼承之後添加,不然會被覆蓋
Child.prototype.constructor = Child // 修復對構造函數的指向
Child.prototype.eat = function () {
// 做點啥
}
```
> 原型鏈繼承就是將子類的原型等於父類的實例,然後再添加子類原型的屬性和方法。這樣的缺點就是隻能繼承一個父類。而且在創建子類的實例的時候,不能傳參給父類。在做單一繼承而且父類不需要傳參的時候,這種寫法是最好的選擇了。
下面在介紹一種我個人覺得已經很不錯的繼承方式(組合繼承)
```javascript
// 子類繼承父類
function Child (name) {
Super.call(this)
this.name = name
}
Child.prototype = new Super()
Child.prototype.constructor = Child // 修復對構造函數的指向
Child.prototype.eat = function () {
// 做點啥
}
```
>- 這種繼承方式就是粗暴的將父類Super構造函數利用對象冒充的方式將父類的實例屬性複製到子類裏。在用原型繼承的方式繼承父類原型的屬性和方法。
>- 這樣的繼承方式,你會發現子類的實例的屬性和__proto__下都有父類的屬性,而子類實例的把子類原型的覆蓋了。
>- 這樣的方式可以實現對多個父類(非原型部分)的繼承。因爲原型的繼承是鏈式的,所以只能繼承一個。
一般來說這種繼承方式已經很好了,代碼量少。繼承性好,弊端也沒有影響。這就是ES6之前的繼承。下面再來看看ES6是怎麼操作的。
### ES6的class
>- ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作爲對象的模板。通過class關鍵字,可以定義類。
>- 基本上,ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用 ES6 的class改寫,就是下面這樣。
上面這兩句話是引用阮一峯對ES6的class的簡介(Class 的基本語法)。阮大神的ECMAScript 6 入門真的是對es6初學者居家必備的好東西。我在這裏就談談我的理解好了。
```javascript
// 寫一個class
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3) // new一個實例
```
下面看下point內部是怎樣的,可以看出constructor,toString都是在“__proto__”內的。
![js對象圖4](http://p0639a4mt.bkt.clouddn.com/1523869463000-173.png "js對象圖4")
在這裏會等價於構造函數的什麼寫法呢
```javascript
// 寫一個class
function Point (x, y) {
this.x = x
this.y = y
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
}
var point = new Point(2, 3) // new一個實例
```
在不考慮class靜態屬性的情況下,可以簡單理解爲constructor內this下的新屬性和方法都屬於構造函數內的,而constructor外的方法都是prototype下的。
>- 在看看class的繼承
```javascript
class ChildPoint extends Point {
constructor (x, y, z) {
super(x, y)
this.z = z
}
toString () {
return '(' + this.x + ', ' + this.y + ','+ this.z +')';
}
changeX (x) {
this.x = x
}
}
var childPoint = new ChildPoint(2, 3, 4) // new一個ChildPoint的實例
console.log(childPoint.x) // 2
console.log(childPoint.y) // 3
console.log(childPoint.z) // 4
console.log(childPoint.toString()) // 2,3,4
childPoint.changeX(5)
console.log(childPoint.x) // 5
console.log(childPoint.__proto__ === ChildPoint.prototype) // true
console.log(childPoint.__proto__.__proto__ === Point.prototype) // true
console.log(childPoint.constructor === ChildPoint) // true
```
這裏class是通過extends繼承父類的,而子類的constructor方法內需要調用super()方法,這相當與調用了父類的constructor()方法。
下面看下childPoint的內部情況:
![js對象圖5](http://p0639a4mt.bkt.clouddn.com/1523875338000-822.png "js對象圖5")
從圖片和上面打印的情況可以看出,es6的繼承和組合繼承很是相似。唯一的不同點就是es6的繼承沒有組合繼承產生的多餘的一份實例屬性在原型裏。看起來很順眼。不過相對的es6的不能多繼承(雖然組合繼承只能多繼承多個構造函數,並不能繼承多個原型)。
其實組合繼承還可以進一步升級成es6繼承那個樣子的(寄生組合繼承):
```javascript
// 我們先創建一個父類
function Super (name) {
this.name = name
this.type = '我是父類'
}
Super.prototype.move = function () {
console.log(this.name + '在跑')
}
// 子類繼承父類
function Child (name) {
Super.call(this)
this.name = name
}
(function() {
// 創建一個沒有Super實例的中間類
var MiddleSuper = function() {}
MiddleSuper.prototype = Super.prototyoe // 跟Super共享原型
// 在這裏MiddleSuper和Super的區別就是有沒有實例了。
// 然後在正常進行組合繼承的操作
Child.prototype = new MiddleSuper()
})()
Child.prototype.constructor = Child // 修復對構造函數的指向
Child.prototype.eat = function () {
// 做點啥
}
```
![js對象圖6](http://p0639a4mt.bkt.clouddn.com/1523934606000-937.png "js對象圖6")
經過這一系列複雜的操作後我們就得到了一份和es6一樣的結構了。不過es6只需要一個extends,而es6之前我們就得長篇大論的寫才能實現,而且還特別繞。所以一句話,快去學es6吧。
>- 該文純屬個人見解,有什麼問題請指出。
本文參考了:
>- 幻天芒的JS繼承的實現方式
>- 阮一峯的ECMAScript 6 入門
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.