導讀
封裝,繼承和多型是物件導向的三大特性。
繼承是JavaScript老生常談的問題,也基礎中的基礎。最基本的就是知道JS中繼承種類,優缺點並能夠手寫例子。下面我引用掘金Oliveryoung老兄一篇文章的思維導圖,他的這篇文章寫的很棒。可以點選。
繼承相關的文章實在是過多,糾結了很久還是落筆,主要原因是:
- 繼承也是JavaScript進階系列的組成部分,同時也是自己對於知識點的梳理;
- 對於剛好看到這篇文章的小夥伴,可以溫故知新;
- 把自己的理解寫出來,有錯誤的能夠指出,一起進步;
下面我以思維導圖為依據,根據不同思路,給出不同的繼承方式,掃盲這塊知識點。
原型鏈繼承
利用JavaScript原型鏈的,把父物件鏈到子物件的原型中一些特性,我舉如下的例子,並給出程式碼:
//父類物件
var Animal = function(){
this.compose = ["head","body","legs"]; //小動物的組成部分
this.statistics = {count:0}; //小動物的數量
this.category = "animal";
}
//子類物件
var Cat = function(name){
this.category = "cat";
this.name = name; //小動物名稱
}
Cat.prototype = new Animal(); //原型鏈繼承
var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
複製程式碼
列印結果如下:
下面是物件之間的關聯關係。
tom
貓的_proto_
屬性指向Cat.prototype
,而Cat.prototype = new Animal()
將Animal物件的"一切"都繼承了下來。
我們接著執行如下程式碼:
var jemmy = new Cat("jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);
複製程式碼
列印結果:
公有引用屬性(statistics
)沒有問題,但是私有引用屬性(compose
)被汙染,所以原型鏈繼承缺點:
- 汙染私有引用屬性
- 無法向父物件傳參
構造器繼承
既然私有引用屬(compose
)屬性不能掛載在Cat.prototype
,那我們把它掛載在自雷物件上,使用call
或者apply
來改變context
,不熟悉call
和apply
請猛戳這裡。
//父類
var Animal = function (category) {
this.compose = ["head", "body", "legs"];
this.statistics = { count: 0 };
this.category = category || "animal";
}
//子類
var Cat = function (name) {
Animal.call(this,"cat");
this.name = name;
}
var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);
複製程式碼
列印結果如下:
物件之間的關係圖如下:構造器繼承解決了原型鏈繼承的兩大問題,但是又暴露了公有引用屬性不能共享的問題。矯枉過正!
我們把公有引用屬性使用原型鏈繼承,私有引用屬性使用構造器繼承,引出組合繼承(構造器+原型組合繼承)
組合繼承
我們將compose
屬性掛載在物件屬性上,statistics
屬性掛載在原型上,結合前兩種繼承:
//父類
var Animal = function (category) {
//私有屬性
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
}
Animal.prototype.statistics = {count: 0}; //公有屬性放在父物件的原型上
//子類
var Cat = function (name) {
Animal.call(this,"cat");//將非共享的屬性通過call加入到子物件中
this.name = name;
}
Cat.prototype = Animal.prototype; //掛載到子物件的原型中
// console.log(Cat.prototype.constructor == Animal) //true
Cat.prototype.constructor = Cat;//糾正子物件的建構函式
var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);
複製程式碼
列印結果:
物件之間的關係圖如下: 融合原型鏈繼承和建構函式繼承的優點,也是常見的繼承策略。原型繼承
原型繼承主要利用ES5出現的Object.create()函式,這裡改寫一下上述的例子:
var Animal = function (category) {
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
this.statistics = {count: 0};
}
var animal = new Animal();
var tom = Object.create(animal);
tom.name = "Tom";
tom.category = "cat";
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = Object.create(animal);
jemmy.name = "Jemmy";
jemmy.category = "cat";
jemmy.compose.push("black eyes");
jemmy.statistics.count++;
console.log(jemmy);
複製程式碼
物件結構如下:
原型繼承和原型鏈繼承一樣,只是使用Object.create()的方式進行繼承。寄生式繼承
鑑於原型式繼承的封裝性不是很好,寄生式繼承主要用於解決這個問題。
//父類
var Animal = function (category) {
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
this.statistics = { count: 0 };
}
//把原型式繼承封裝成一個create函式
function create(parent, name, category, eyes) {
let obj = Object.create(parent);
obj.name = name;
obj.category = category;
obj.compose.push(eyes);
obj.statistics.count++;
return obj;
}
var animal = new Animal();
var tom = create(animal, "Tom", "cat", "blue eyes");
console.log(tom);
var jemmy = create(animal, "Jemmy", "cat", "black eyes");
console.log(jemmy);
複製程式碼
這種方式比原型式繼承封裝性更好。但是缺點還是沒解決的。在寄生式基礎上,結合構造器繼承,就是寄生組合式繼承。
寄生組合式繼承
寄生組合式繼承,更多了還是利用組合式繼承的思想。
//父類
var Animal = function (category) {
//私有屬性
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
}
Animal.prototype = {
//公有屬性,還是放在原型上
statistics : { count: 0 }
}
var Cat = function(name){
//把私有屬性通過call繼承過來
Animal.call(this,"cat");
this.name = name;
}
function proto(Son,Parent) {
//其實就是把組合繼承的原型部分封裝了一下
Son.prototype = Object.create(Parent.prototype);
Son.prototype.constructor = Son;
}
proto(Cat,Animal);
var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);
複製程式碼
列印結果和組合式繼承一致:
總結一下,JavaScript的繼承的思想主要由下面兩條構成:
- 將私有屬性通過
call/apply
在子物件建構函式中呼叫,直接繼承- 將公有屬性通過原型鏈繼承
組合式繼承和寄生組合式繼承只是實現方式的不同,思想是一致的。
ES6
class
和extends
的出現,使繼承變得簡單!
class Animal{
constructor(category){
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
}
}
Animal.prototype.statistics = { count: 0 }
class Cat extends Animal{
constructor(name){
super("cat");
this.name = name;
}
}
var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);
複製程式碼
列印結果如下:
我們使用babel轉成ES5語法,快速轉換地址這裡: 轉換結果如下:"use strict";
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf :
function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
//子類的原型指向一個以(subClass為建構函式,superClass.prototype中的物件屬性)的物件
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true } });
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf ||
function _setPrototypeOf(o, p)
{ o.__proto__ = p; return o; };
return _setPrototypeOf(o, p);
}
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Animal = function Animal(category) {
_classCallCheck(this, Animal);
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
};
Animal.prototype.statistics = {
count: 0
};
var Cat =
/*#__PURE__*/
function (_Animal) {
_inherits(Cat, _Animal);
function Cat(name) {
var _this;
_classCallCheck(this, Cat);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Cat).call(this, "cat"));
_this.name = name;
return _this;
}
return Cat;
}(Animal);
複製程式碼
大家可以仔細看看這段程式碼,寫的挺有意思的,看懂了基本上也就理解繼承了。在這裡
_inherits(Cat, _Animal)
跟寄生組合式繼承中的proto(Cat,Animal)
一樣,_getPrototypeOf(Cat).call(this, "cat")
和Animal.call(this,"cat");
也一樣,值得一提的是ES6中的super(xxx)
就是將父類的建構函式使用call
進行傳參。