搞懂 JavaScript 繼承原理

摘要: 理解JS繼承讨盒。

Fundebug經(jīng)授權(quán)轉(zhuǎn)載围辙,版權(quán)歸原作者所有峦剔。

在理解繼承之前翅阵,需要知道 js 的三個東西:

    1. 什么是 JS 原型鏈
    1. this 的值到底是什么
    1. JS 的 new 到底是干什么的

1. 什么是 JS 原型鏈赎败?

我們知道 JS 有對象秕衙,比如

var obj = { name: "obj" };

我們通過控制臺把 obj 打印出來:

我們會發(fā)現(xiàn) obj 已經(jīng)有幾個屬性(方法)了。那么問題來了:valueOf / toString / constructor 是怎么來僵刮?我們并沒有給 obj.valueOf 賦值呀据忘。

上面這個圖有點難懂,我手畫一個示意圖:

我們發(fā)現(xiàn)控制臺打出來的結(jié)果是:

  • obj 本身有一個屬性 name (這是我們給它加的)
  • obj 還有一個屬性叫做 proto(它是一個對象)
  • obj 還有一個屬性妓笙,包括 valueOf, toString, constructor 等
  • obj.proto其實也有一個叫做proto的屬性(console.log 沒有顯示)若河,值為 null

現(xiàn)在回到我們的問題:obj 為什么會擁有 valueOf / toString / constructor 這幾個屬性?

答案: 這跟 proto有關(guān) 寞宫。

當我們「讀取」 obj.toString 時萧福,JS 引擎會做下面的事情:

  • 看看 obj 對象本身有沒有 toString 屬性。沒有就走到下一步辈赋。
  • 看看 obj.proto 對象有沒有 toString 屬性鲫忍, 發(fā)現(xiàn) obj.proto 有 toString 屬性, 于是找到了钥屈,所以 obj.toString 實際就是第 2 步中找到的 obj.proto.toString悟民。
  • 如果 obj.proto沒有,那么瀏覽器會繼續(xù)查看 obj.proto.proto
  • 如果 obj.proto.proto也沒有篷就,那么瀏覽器會繼續(xù)查看 obj.proto.proto.proto
  • 直到找到 toString 或者 proto 為 null射亏。

上面的過程,就是「讀」屬性的「搜索過程」竭业。而這個「搜索過程」智润,是連著由 proto 組成的鏈子一直走的。這個鏈子未辆,就叫做「原型鏈」窟绷。

共享原型鏈

現(xiàn)在我們還有另一個對象

var obj2 = { name: "obj2" };

如圖:

那么 obj.toString 和 obj2.toString 其實是同一東西, 也就是 obj2.proto.toString咐柜。

說白了兼蜈,我們改其中的一個 proto.toString ,那么另外一個其實也會變!

差異化

如果我們想讓 obj.toString 和 obj2.toString 的行為不同怎么做呢拙友?
直接賦值就好了:

obj.toString = function() {
    return "新的 toString 方法";
};

小結(jié)

  • [讀]屬性時會沿著原型鏈搜索
  • [新增]屬性時不會去看原型鏈

2. this 的值到底是什么

你可能遇到過這樣的 JS 面試題:

var obj = {
    foo: function() {
        console.log(this);
    }
};

var bar = obj.foo;
obj.foo(); // 打印出的 this 是 obj
bar(); // 打印出的 this 是 window

請解釋最后兩行函數(shù)的值為什么不一樣为狸。

函數(shù)調(diào)用

JS(ES5)里面有三種函數(shù)調(diào)用形式:

func(p1, p2);
obj.child.method(p1, p2);
func.call(context, p1, p2); // 先不講 apply

一般,初學(xué)者都知道前兩種形式遗契,而且認為前兩種形式「優(yōu)于」第三種形式钥平。
我們方方老師大姥說了,你一定要記住,第三種調(diào)用形式涉瘾,才是正常調(diào)用形式:

func.call(context, p1, p2);

其他兩種都是語法糖,可以等價地變?yōu)?call 形式:

func(p1, p2)等價于 func.call(undefined, p1, p2);

obj.child.method(p1, p2) 等價于 obj.child.method.call(obj.child, p1, p2);

至此我們的函數(shù)調(diào)用只有一種形式:

func.call(context, p1, p2);

這樣捷兰,this 就好解釋了 this 就是上面 context立叛。

this 是你 call 一個函數(shù)時傳的 context,由于你從來不用 call 形式的函數(shù)調(diào)用贡茅,所以你一直不知道秘蛇。

先看 func(p1, p2) 中的 this 如何確定:

當你寫下面代碼時;

function func() {
    console.log(this);
}

func();
等價于;

function func() {
    console.log(this);
}

func.call(undefined); // 可以簡寫為 func.call()

按理說打印出來的 this 應(yīng)該就是 undefined 了吧,但是瀏覽器里有一條規(guī)則:

如果你傳的 context 就 null 或者 undefined顶考,那么 window 對象就是默認的 context(嚴格模式下默認 context 是 undefined)

因此上面的打印結(jié)果是 window赁还。如果你希望這里的 this 不是 window,很簡單:

func.call(obj); // 那么里面的 this 就是 obj 對象了

回到題目:

var obj = {
    foo: function() {
        console.log(this);
    }
};

var bar = obj.foo;
obj.foo(); // 轉(zhuǎn)換為 obj.foo.call(obj)驹沿,this 就是 obj
bar();
// 轉(zhuǎn)換為 bar.call()
// 由于沒有傳 context
// 所以 this 就是 undefined
// 最后瀏覽器給你一個默認的 this —— window 對象

[ ] 語法

function fn() {
    console.log(this);
}
var arr = [fn, fn2];
arr[0](); // 這里面的 this 又是什么呢艘策?

我們可以把 arr0 想象為 arr.0( ),雖然后者的語法錯了渊季,但是形式與轉(zhuǎn)換代碼里的 obj.child.method(p1, p2) 對應(yīng)上了朋蔫,于是就可以愉快的轉(zhuǎn)換了:

arr[0]();

假想為 arr.0()
然后轉(zhuǎn)換為 arr.0.call(arr)
那么里面的 this 就是 arr 了 :)

小結(jié):

  • this 就是你 call 一個函數(shù)時,傳入的第一個參數(shù)却汉。
  • 如果你的函數(shù)調(diào)用不是 call 形式驯妄, 請將其轉(zhuǎn)換為 call 形式

碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG合砂,花了大量的時間進行l(wèi)og 調(diào)試青扔,這邊順便給大家推薦一個好用的BUG監(jiān)控工具 Fundebug

3. JS 的 new 到底是干什么的翩伪?

我們聲明一個士兵微猖,具有如下屬性:

var 士兵 = {
    ID: 1, // 用于區(qū)分每個士兵
    兵種: "美國大兵",
    攻擊力: 5,
    生命值: 42,
    行走: function() {
        /*走倆步的代碼*/
    },
    奔跑: function() {
        /*狂奔的代碼*/
    },
    死亡: function() {
        /*Go die*/
    },
    攻擊: function() {
        /*糊他熊臉*/
    },
    防御: function() {
        /*護臉*/
    }
};

我們制造一個士兵, 只需要這樣:

兵營.制造(士兵);

如果需要制造 100 個士兵怎么辦呢幻工?

循環(huán) 100 次吧:

var 士兵們 = []
var 士兵
for(var i=0; i<100; i++){
  士兵 = {
    ID: i, // ID 不能重復(fù)
    兵種:"美國大兵",
    攻擊力:5,
    生命值:42,
    行走:function(){ /*走倆步的代碼*/}励两,
    奔跑:function(){ /*狂奔的代碼*/  },
    死亡:function(){ /*Go die*/    },
    攻擊:function(){ /*糊他熊臉*/   },
    防御:function(){ /*護臉*/       }
  }
  士兵們.push(士兵)
}

兵營.批量制造(士兵們)

哎呀,看起來好簡單

質(zhì)疑

上面的代碼存在一個問題:浪費了很多內(nèi)存

  • 行走囊颅、奔跑当悔、死亡、攻擊踢代、防御這五個動作對于每個士兵其實是一樣的盲憎,只需要各自引用同一個函數(shù)就可以了,沒必要重復(fù)創(chuàng)建 100 個行走胳挎、100 個奔跑……
  • 這些士兵的兵種和攻擊力都是一樣的饼疙,沒必要創(chuàng)建 100 次。
  • 只有 ID 和生命值需要創(chuàng)建 100 次慕爬,因為每個士兵有自己的 ID 和生命值窑眯。

改進

通過第一節(jié)可以知道 屏积,我們可以通過原型鏈來解決重復(fù)創(chuàng)建的問題:我們先創(chuàng)建一個「士兵原型」,然后讓「士兵」的 proto 指向「士兵原型」磅甩。

var 士兵原型 = {
  兵種:"美國大兵",
  攻擊力:5,
  行走:function(){ /*走倆步的代碼*/}炊林,
  奔跑:function(){ /*狂奔的代碼*/  },
  死亡:function(){ /*Go die*/    },
  攻擊:function(){ /*糊他熊臉*/   },
  防御:function(){ /*護臉*/       }
}

var 士兵們 = []
var 士兵
for(var i=0; i<100; i++){
  士兵 = {
    ID: i, // ID 不能重復(fù)
    生命值:42
  }

  /*實際工作中不要這樣寫,因為 __proto__ 不是標準屬性*/
  士兵.__proto__ = 士兵原型

  士兵們.push(士兵)
}

兵營.批量制造(士兵們)

優(yōu)雅卷要?

有人指出創(chuàng)建一個士兵的代碼分散在兩個地方很不優(yōu)雅渣聚,于是我們用一個函數(shù)把這兩部分聯(lián)系起來:

function 士兵(ID){
  var 臨時對象 = {};
  臨時對象.__proto__ = 士兵.原型;
  臨時對象.ID = ID;
  臨時對象.生命值 = 42;

  return 臨時對象;
}

士兵.原型 = {
  兵種:"美國大兵",
  攻擊力:5,
  行走:function(){ /*走倆步的代碼*/},
  奔跑:function(){ /*狂奔的代碼*/  },
  死亡:function(){ /*Go die*/    },
  攻擊:function(){ /*糊他熊臉*/   },
  防御:function(){ /*護臉*/       }
}

// 保存為文件:士兵.js

 然后就可以愉快地引用「士兵」來創(chuàng)建士兵了:

var 士兵們 = []
for(var i=0; i<100; i++){
  士兵們.push(士兵(i))
}

兵營.批量制造(士兵們)

JS 之父看到大家都這么搞僧叉,覺得何必呢奕枝,我給你們個糖吃,于是 JS 之父創(chuàng)建了 new 關(guān)鍵字瓶堕,可以讓我們少寫幾行代碼:

只要你在士兵前面使用 new 關(guān)鍵字隘道,那么可以少做四件事情:

  1. 不用創(chuàng)建臨時對象,因為 new 會幫你做(你使用「this」就可以訪問到臨時對象)捞烟;
  2. 不用綁定原型薄声,因為 new 會幫你做(new 為了知道原型在哪,所以指定原型的名字 prototype);
  3. 不用 return 臨時對象题画,因為 new 會幫你做默辨;
  4. 不要給原型想名字了,因為 new 指定名字為 prototype苍息。

這一次用 new 來寫

function 士兵(ID){
  this.ID = ID
  this.生命值 = 42
}

士兵.prototype = {
  兵種:"美國大兵",
  攻擊力:5,
  行走:function(){ /*走倆步的代碼*/},
  奔跑:function(){ /*狂奔的代碼*/  },
  死亡:function(){ /*Go die*/    },
  攻擊:function(){ /*糊他熊臉*/   },
  防御:function(){ /*護臉*/       }
}

// 保存為文件:士兵.js
然后是創(chuàng)建士兵(加了一個 new 關(guān)鍵字):

var 士兵們 = []
for(var i=0; i<100; i++){
  士兵們.push(new 士兵(i))
}

兵營.批量制造(士兵們)

new 的作用缩幸,就是省那么幾行代碼。(也就是所謂的語法糖)

注意 constructor 屬性

new 操作為了記錄「臨時對象是由哪個函數(shù)創(chuàng)建的」竞思,所以預(yù)先給「士兵.prototype」加了一個 constructor 屬性:

士兵.prototype = {
    constructor: 士兵
};

如果你重新對「士兵.prototype」賦值表谊,那么這個 constructor 屬性就沒了,所以你應(yīng)該這么寫:

士兵.prototype.兵種 = "美國大兵";
士兵.prototype.攻擊力 = 5;
士兵.prototype.行走 = function() {
    /*走倆步的代碼*/
};
士兵.prototype.奔跑 = function() {
    /*狂奔的代碼*/
};
士兵.prototype.死亡 = function() {
    /*Go die*/
};
士兵.prototype.攻擊 = function() {
    /*糊他熊臉*/
};
士兵.prototype.防御 = function() {
    /*護臉*/
};

或者你也可以自己給 constructor 重新賦值:

士兵.prototype = {
    constructor: 士兵,
    兵種: "美國大兵",
    攻擊力: 5,
    行走: function() {
        /*走倆步的代碼*/
    },
    奔跑: function() {
        /*狂奔的代碼*/
    },
    死亡: function() {
        /*Go die*/
    },
    攻擊: function() {
        /*糊他熊臉*/
    },
    防御: function() {
        /*護臉*/
    }
};

四盖喷、繼承

繼承的本質(zhì)就是上面的講的原型鏈

1)借助構(gòu)造函數(shù)實現(xiàn)繼承

function Parent1() {
    this.name = "parent1";
}

Parent1.prototype.say = function() {};

function Child1() {
    Parent1.call(this);
    this.type = "child";
}

console.log(new Child1());

打印結(jié)果:

這個主要是借用 call 來改變 this 的指向爆办,通過 call 調(diào)用 Parent ,此時 Parent 中的 this 是指 Child1课梳。有個缺點距辆,從打印結(jié)果看出 Child1 并沒有 say 方法,所以這種只能繼承父類的實例屬性和方法暮刃,不能繼承原型屬性/方法跨算。

2)借助原型鏈實現(xiàn)繼承

/**
 * 借助原型鏈實現(xiàn)繼承
 */
function Parent2() {
    this.name = "parent2";
    this.play = [1, 2, 3];
}

function Child2() {
    this.type = "child2";
}
Child2.prototype = new Parent2();

console.log(new Child2());

var s1 = new Child2();
var s2 = new Child2();

打印:

通過一講的椭懊,我們知道要共享莫些屬性诸蚕,需要 對象.proto = 父親對象的.prototype,但實際上我們是不能直接 操作proto,這時我們可以借用 new 來做早歇,所以
Child2.prototype = new Parent2(); <=> Child2.prototype.proto = Parent2.prototype; 這樣我們借助 new 這個語法糖揍庄,就可以實現(xiàn)原型鏈繼承王污。但這里有個總是硝桩,如打印結(jié)果,我們給 s1.play 新增一個值 椭豫,s2 也跟著改了捌朴。所以這個是原型鏈繼承的缺點津函,原因是 s1.pro 和 s2.pro指向同一個地址即 父類的 prototype蛉幸。

3)組合方式實現(xiàn)繼承

/**
 * 組合方式
 */

function Parent3() {
    this.name = "parent3";
    this.play = [1, 2, 3];
}

Parent3.prototype.say = function() {};

function Child3() {
    Parent3.call(this);
    this.type = "child3";
}

Child3.prototype = new Parent3();

var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(new Child3());
console.log(s3.play, s4.play);

打印:

將 1 和 2 兩種方式組合起來,就可以解決 1 和 2 存在問題丛晦,這種方式為組合繼承奕纫。這種方式有點缺點就是我實例一個對象的時, 父類 new 了兩次烫沙,一次是 var s3 = new Child3()對應(yīng) Child3.prototype = new Parent3()還要 new 一次匹层。

4)組合繼承的優(yōu)化 1

function Parent4() {
    this.name = "parent4";
    this.play = [1, 2, 3];
}

Parent4.prototype.say = function() {};

function Child4() {
    Parent4.call(this);
    this.type = "child4";
}

Child4.prototype = Parent4.prototype;

var s5 = new Child4();
var s6 = new Child4();

這邊主要為 Child4.prototype = Parent4.prototype, 因為我們通過構(gòu)造函數(shù)就可以拿到所有屬性和實例的方法锌蓄,那么現(xiàn)在我想繼承父類的原型對象升筏,所以你直接賦值給我就行,不用在去 new 一次父類瘸爽。其實這種方法還是有問題的您访,如果我在控制臺打印以下兩句:

從打印可以看出,此時我是沒有辦法區(qū)分一個對象 是直接 由它的子類實例化還是父類呢剪决?我們還有一個方法判斷來判斷對象是否是類的實例灵汪,那就是用 constructor,我在控制臺打印以下內(nèi)容:

咦,你會發(fā)現(xiàn)它指向的是父類 柑潦,這顯然不是我們想要的結(jié)果享言, 上面講過我們 prototype 里面有一個 constructor, 而我們此時子類的 prototype 指向是 父類的 prototye ,而父類 prototype 里面的 contructor 當然是父類自己的,這個就是產(chǎn)生該問題的原因渗鬼。

組合繼承的優(yōu)化 2

/**
 * 組合繼承的優(yōu)化2
 */

function Parent5() {
    this.name = "parent4";
    this.play = [1, 2, 3];
}

Parent5.prototype.say = function() {};

function Child5() {
    Parent5.call(this);
    this.type = "child4";
}

Child5.prototype = Object.create(Parent5.prototype);

這里主要使用Object.create()览露,它的作用是將對象繼承到proto屬性上。舉個例子:

var test = Object.create({ x: 123, y: 345 });
console.log(test); //{}
console.log(test.x); //123
console.log(test.__proto__.x); //3
console.log(test.__proto__.x === test.x); //true

那大家可能說這樣解決了嗎譬胎,其實沒有解決,因為這時 Child5.prototype 還是沒有自己的 constructor,它要找的話還是向自己的原型對象上找最后還是找到 Parent5.prototype, constructor 還是 Parent5 ,所以要給 Child5.prototype 寫自己的 constructor:

Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;

參考

關(guān)于Fundebug

Fundebug專注于JavaScript多糠、微信小程序、微信小游戲浩考、支付寶小程序夹孔、React Native、Node.js和Java線上應(yīng)用實時BUG監(jiān)控。 自從2016年雙十一正式上線搭伤,F(xiàn)undebug累計處理了10億+錯誤事件只怎,付費客戶有Google、360怜俐、金山軟件身堡、百姓網(wǎng)等眾多品牌企業(yè)。歡迎大家免費試用拍鲤!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贴谎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子季稳,更是在濱河造成了極大的恐慌擅这,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件景鼠,死亡現(xiàn)場離奇詭異仲翎,居然都是意外死亡,警方通過查閱死者的電腦和手機铛漓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門溯香,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人浓恶,你說我怎么就攤上這事玫坛。” “怎么了问顷?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵昂秃,是天一觀的道長。 經(jīng)常有香客問我杜窄,道長肠骆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任塞耕,我火速辦了婚禮蚀腿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扫外。我一直安慰自己莉钙,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布筛谚。 她就那樣靜靜地躺著磁玉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪驾讲。 梳的紋絲不亂的頭發(fā)上蚊伞,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天席赂,我揣著相機與錄音,去河邊找鬼时迫。 笑死颅停,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的掠拳。 我是一名探鬼主播癞揉,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼溺欧!你這毒婦竟也來了喊熟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤姐刁,失蹤者是張志新(化名)和其女友劉穎逊移,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體龙填,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年拐叉,在試婚紗的時候發(fā)現(xiàn)自己被綠了岩遗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡凤瘦,死狀恐怖宿礁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔬芥,我是刑警寧澤梆靖,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站笔诵,受9級特大地震影響返吻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乎婿,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一测僵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谢翎,春花似錦捍靠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至褒侧,卻和暖如春良风,著一層夾襖步出監(jiān)牢的瞬間谊迄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工拖吼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鳞上,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓吊档,卻偏偏與公主長得像篙议,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子怠硼,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

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