對JavaScript繼承的理解

本篇文章會分別從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級別的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內(nèi)容