本篇文章會分別從ES5和ES6的角度上來學(xué)習(xí)JS的繼承。
由于js不像java那樣是真正面向?qū)ο蟮恼Z言,js是基于對象的,它沒有類的概念。所以,要想實現(xiàn)繼承,可以用js的原型prototype機(jī)制或者用apply和call方法去實現(xiàn)。
首先講一下對原型鏈的理解,原型鏈可以理解為我們?nèi)伺c人之間的繼承關(guān)系,上代碼:
var parent = {
money: 1000000,
};
var son = Object.create(parent);
var son_1 = Object.create(parent);
son.money // 1000000
son_1.money // 1000000
son === son_1 // false
son.money === parent.money // true
son_1.money === parent.money // true
son.__proto__ === parent // true
son_1.__proto__ === parent // true
上述代碼,新建了一個parent對象,并且使用create方法基于parent新建了一個son對象,這就實現(xiàn)了js中一個最簡單的js原型鏈繼承。
怎么說呢?create方法實現(xiàn)了,son和parent的血緣關(guān)系(原型繼承),為了方便理解,可以理解為son的proto指針指向了parent。
這個proto建立起來的繼承關(guān)系就是我們常說的原型鏈。
這個有什么魔性的作用呢?那就是讓我們訪問son某個屬性的時候,js會順著這個鏈子從對象本身一直往上走,直到找到為止。就像剛才那個例子,找money屬性,son沒有,就按著原型鏈一直往上走,找到了parent的money屬性。
同時,上面的代碼中,我基于parent還新建了另一個son_1對象,根據(jù)輸出結(jié)果我們可以發(fā)現(xiàn),
son和son_1共用了這層繼承關(guān)系,共用了它們原型的屬性。
在面向?qū)ο蟮恼Z言中,我們經(jīng)常使用類來創(chuàng)建一個自定義對象。然而js中所有事物都是對象,那么用什么辦法來創(chuàng)建自定義對象呢?我們可以通過構(gòu)造函數(shù)的方式模擬實現(xiàn)類的功能。
我們看下面這個函數(shù)
function Person(name){
this.name = name || 'huihui';
};
Person(); // window.name='huihui'
var p = new Person(); // {name:'huihui'}
這個函數(shù),如果直接調(diào)用(非嚴(yán)格模式下,如果嚴(yán)格模式下,會報錯),執(zhí)行結(jié)果則是將當(dāng)前作用域內(nèi)的this的屬性name賦值;如果是以new Person(‘’) 方式調(diào)用,則會返回一個全新的對象。好,那么問題來了,new Person(‘’)操作,我們會認(rèn)為是新建了某種類型的對象,這個過程到底發(fā)生了什么呢?
很簡單,發(fā)生了下面的過程:
1、var obj = {}; // 新建一個空對象
2、Object.setPrototypeOf?
Object.setPrototypeOf(obj,Person.prototype)
:obj.__proto__ = Person.prototype; // 為新對象綁定原型鏈關(guān)系
3、Person.call(obj,...arguments) // 綁定新對象的this作用域
這邊引出了一個新的東西【prototype】,在js中,任何一個函數(shù)對象都有這么個屬性。我們可以簡單的把prototype看做是一個模版,基于構(gòu)造函數(shù)新創(chuàng)建的自定義對象都是這個模版(prototype)的一個拷貝 。prototype是一個對象,不是一個函數(shù),它是一個constructor屬性指向?qū)?yīng)函數(shù)的的一個對象。
聊一聊這個prototype對象有個什么用。
首先,從上面代碼我們可以看出,基于構(gòu)造函數(shù)創(chuàng)建的對象的原型鏈(proto)始終指向這個構(gòu)造函數(shù)的prototype對象,好了,這時候我們可以發(fā)現(xiàn)prototype的其中一個重要作用就是存放某一類對象的公用特征,讓同一個構(gòu)造函數(shù)構(gòu)造出來的對象共同繼承某些公用方法或者屬性(放在這個構(gòu)造函數(shù)的prototype對象里);
其次,我們會經(jīng)常使用instanceof來判斷某個對象是否是某類型的實例,比如 p instanceof Person,這個過程其實是根據(jù)Person.prototype 是否在p 的原型鏈中來判斷的。
好了,剛才介紹了基于構(gòu)造函數(shù)創(chuàng)建對象的過程。那么,我們來看看這類對象之間的繼承怎么做。
看個例子:
function Animal(){
this.type = 'animal';
}
function Dog(){
this.name = 'wangwang';
}
Dog.prototype = Object.create(Animal.prototype);// 建立原型鏈關(guān)系
Dog.prototype.constructor = Dog;
var dog = new Dog();
dog.name // wangwang
dog.type // animal,繼承下來的屬性
上述代碼很清晰的建立起了兩個類(Animal\Dog)之間的繼承關(guān)系。
這部分的代碼其實就是純粹地基于原型鏈的繼承,但是,我們可以發(fā)現(xiàn)基于原型鏈繼承存在2個缺點:
一是字面量重寫原型會中斷關(guān)系,原型上的引用類型屬性一不小心就會被共享(原因見下面代碼);
function Animal(){
this.type = 'animal';
this.foods = ['c','b'];
}
Animal.prototype.addFood = function(food){
this.foods.push(food);
}
function Dog(){
this.name = 'wangwang';
}
Dog.prototype = Object.create(Animal.prototype);// 建立原型鏈關(guān)系
Dog.prototype.constructor = Dog;
var dog = new Dog();
dog.name // wangwang
dog.type // animal,繼承下來的屬性
dog.addFood('e');
dog.foods // ['c','b','e'];
var a = new Animal();
a.foods // ['c','b','e'];
dog.type = 'dog';
dog // {type:'dog',name:'wangwang',__proto:...}
上面代碼可以發(fā)現(xiàn),我們調(diào)用子類的實例對象dog的addFood方法,把公用的foods屬性都給改變了,這個是為啥呢?這是因為dog.addFood實際上就是執(zhí)行了dog.prototype.addFood.call(dog,...args);dog中找不到foods屬性,會直接原型鏈順著查找,找到了再修改掉了,因為原型對象的屬性是共享的,那么自然就影響到了所有的。
而這時候可能會有個疑問,那為啥簡單類型變量不會受到影響呢?因為簡單類型就是直接賦值的啊,就如dog.type = 'dog';這句語句只會直接就在當(dāng)前實例上加了對應(yīng)的屬性,不會上升到原型對象當(dāng)中去。(本質(zhì)上來講,其實就是如果不需要訪問這個屬性而直接賦值操作的,就不會影響到原型上的屬性,如果是需要訪問這個屬性然后再對這個屬性進(jìn)行操作的操作,就會影響到原型上對應(yīng)的這個屬性)。
二是子類型還無法給超類型傳遞參數(shù)。
這個時候,為了解決這個問題我們就聊到了構(gòu)造函數(shù)之間的類式繼承??创a:
function Animal(type){
this.type =type;
this.foods = ['c','b'];
}
function Dog(type){
Animal.call(this,type);
this.name = 'wangwang';
}
var d = new Dog('dog');
d // {foods:['c','b'],name:'wangwang',type:'dog'}
上面代碼通過超類函數(shù)的call調(diào)用,完美實現(xiàn)了繼承。但是,但是,我們發(fā)現(xiàn)這并沒有復(fù)用任何東西,沒有原型繼承,何談復(fù)用?
那么,這時候,我們便可以將這2種方式結(jié)合起來:
function Animal(type){
this.type =type;
this.foods = ['c','b'];
}
Animal.prototype.addFood = function(food){
this.foods.push(food);
}
function Dog(type){
Animal.call(this,type);
}
Dog.prototype = Object.create(Animal.prototype);// 建立原型鏈關(guān)系
Dog.prototype.constructor = Dog;
var d = new Dog('dog');
d.addFood('e');
d // {type:'dog',foods:['c','b','e'],__proto__}
這種方法叫做:組合式繼承,它是比較常用的一種繼承方法,其背后的思路是 使用原型鏈實現(xiàn)對原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承。這樣,既通過在原型上定義方法實現(xiàn)了函數(shù)復(fù)用,又保證每個實例都有它自己的屬性。
上面就是對ES5中的繼承實現(xiàn)方式做了自己的認(rèn)識與理解。接下來介紹一下ES6中對繼承做了哪些變化亦或是改善呢?
從上面可以看出,js當(dāng)中實現(xiàn)繼承最佳實踐就是組合式繼承的方法,為了迎合 java這類后端語言的寫法,ES6中引入了class 語法,可以通過extends關(guān)鍵字來實現(xiàn)類之間的繼承。下面我們來看看是咋回事兒。
class Animal {
constructor(type){
this.type = type;
}
addFood(food){
this.foods.push(food);
}
}
class Dog extends Animal{
constructor(type){
super(type);
this.name = 'dog';
}
}
typeof Animal //function
typeof Dog //function
下面是經(jīng)過babel轉(zhuǎn)譯后的代碼,我們仔細(xì)瞧一瞧:
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Animal = function () {
function Animal(type) {
_classCallCheck(this, Animal);
this.type = type;
}
_createClass(Animal, [{
key: 'addFood',
value: function addFood(food) {
this.foods.push(food);
}
}]);
return Animal;
}();
var Dog = function (_Animal) {
_inherits(Dog, _Animal);
function Dog(type) {
_classCallCheck(this, Dog);
var _this = _possibleConstructorReturn(this, (Dog.__proto__ || Object.getPrototypeOf(Dog)).call(this, type));
_this.name = 'dog';
return _this;
}
return Dog;
}(Animal);
首先,分析class ,我們發(fā)現(xiàn)class就是個函數(shù)語法糖,本質(zhì)上是一個立即執(zhí)行表達(dá)式,返回一個跟ES5一樣的構(gòu)造函數(shù)。然后我們觀察下_createClass這個方法,這個方法主要就是做了一件事情,就是把a(bǔ)ddFood 方法定義到Animal的prototype上。這部分與ES5無異。得出的結(jié)論就是ES6中的class本質(zhì)上跟前面提到的構(gòu)造函數(shù)一樣,在class里面定義的方法直接放在prototype上。
接下來,我們看下ES6中的繼承是怎么實現(xiàn)的。我們首先關(guān)注下_inherits這個方法,
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
可以看出這個方法里面做了兩件事情:1、建立subClass.prototype和superClass.prototype的繼承關(guān)系;2、建立subClass和superClass的繼承關(guān)系
然后,我們來看一下_possibleConstructorReturn這個方法,這個方法,其實就是對應(yīng)這super,效果等同于Animal.call(this,type);
不過這面可以注意個小細(xì)節(jié),子類中的this是通過_possibleConstructorReturn返回的,這里要銘記。與ES5中的差別再于,ES5中是通過call直接將父類的屬性繼承到子類來,this一直都是子類的this,而ES6中雖然也類似,但是子類中最終返回生成的this是通過_possibleConstructorReturn(super)返回的。
總結(jié)下:
JavaScript中實現(xiàn)繼承的方式靈活多變。
其中,ES5中最推薦使用組合式繼承方式來實現(xiàn),而ES6中的Class語法糖本質(zhì)上也是基于組合式繼承實現(xiàn)的。不過ES6當(dāng)中簡化了寫法,與傳統(tǒng)的面向?qū)ο笳Z言寫法更為接近。
好了,此文結(jié)束。
=============
補(bǔ)充:
1、object.create(_proto,props)怎么實現(xiàn)的?通過中間空函數(shù),f(),f.prototype=proto;return new f();
2、為什么要做constructor修正?為了在構(gòu)造函數(shù)外部,通過實例的constructor可以更改prototype;
3、 為什么es6 中class之間和class.prototype之間都要原型繼承綁定起來,因為static屬性也需要繼承,這是掛在class級別的。