js 原型鏈和繼承(轉)

在理解繼承以前,須要知道 js 的三個東西:javascript

    1. 什麼是 JS 原型鏈
    1. this 的值究竟是什麼
    1. JS 的 new 究竟是幹什麼的

1. 什麼是 JS 原型鏈?

咱們知道 JS 有對象,好比java

var obj = { name: "obj" };

咱們經過控制檯把 obj 打印出來:面試

咱們會發現 obj 已經有幾個屬性(方法)了。那麼問題來了:valueOf / toString / constructor 是怎麼來?咱們並無給 obj.valueOf 賦值呀。segmentfault

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

咱們發現控制檯打出來的結果是:app

  • obj 自己有一個屬性 name (這是咱們給它加的)
  • obj 還有一個屬性叫作 proto(它是一個對象)
  • obj 還有一個屬性,包括 valueOf, toString, constructor 等
  • obj.proto其實也有一個叫作proto的屬性(console.log 沒有顯示),值爲 null

如今回到咱們的問題:obj 爲何會擁有 valueOf / toString / constructor 這幾個屬性?函數

答案: 這跟 proto有關 。工具

當咱們「讀取」 obj.toString 時,JS 引擎會作下面的事情:優化

  • 看看 obj 對象自己有沒有 toString 屬性。沒有就走到下一步。
  • 看看 obj.proto 對象有沒有 toString 屬性, 發現 obj.proto 有 toString 屬性, 因而找到了,因此 obj.toString 實際就是第 2 步中找到的 obj.proto.toString。
  • 若是 obj.proto沒有,那麼瀏覽器會繼續查看 obj.proto.proto
  • 若是 obj.proto.proto也沒有,那麼瀏覽器會繼續查看 obj.proto.proto.proto
  • 直到找到 toString 或者 proto 爲 null。

上面的過程,就是「讀」屬性的「搜索過程」。而這個「搜索過程」,是連着由 proto 組成的鏈子一直走的。這個鏈子,就叫作「原型鏈」。ui

共享原型鏈

如今咱們還有另外一個對象

var obj2 = { name: "obj2" };

如圖:

那麼 obj.toString 和 obj2.toString 實際上是同一東西, 也就是 obj2.proto.toString。

說白了,咱們改其中的一個 proto.toString ,那麼另一個其實也會變!

差別化

若是咱們想讓 obj.toString 和 obj2.toString 的行爲不一樣怎麼作呢?
直接賦值就行了:

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

小結

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

2. this 的值究竟是什麼

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

var obj = { foo: function() { console.log(this); } }; var bar = obj.foo; obj.foo(); // 打印出的 this 是 obj bar(); // 打印出的 this 是 window

請解釋最後兩行函數的值爲何不同。

函數調用

JS(ES5)裏面有三種函數調用形式:

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

通常,初學者都知道前兩種形式,並且認爲前兩種形式「優於」第三種形式。
咱們方方老師大姥說了,你必定要記住,第三種調用形式,纔是正常調用形式:

func.call(context, p1, p2);

其餘兩種都是語法糖,能夠等價地變爲 call 形式:

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

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

至此咱們的函數調用只有一種形式:

func.call(context, p1, p2);

這樣,this 就好解釋了 this 就是上面 context。

this 是你 call 一個函數時傳的 context,因爲你歷來不用 call 形式的函數調用,因此你一直不知道。

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

當你寫下面代碼時;

function func() { console.log(this); } func(); 等價於; function func() { console.log(this); } func.call(undefined); // 能夠簡寫爲 func.call()

按理說打印出來的 this 應該就是 undefined 了吧,可是瀏覽器裏有一條規則:

若是你傳的 context 就 null 或者 undefined,那麼 window 對象就是默認的 context(嚴格模式下默認 context 是 undefined)

所以上面的打印結果是 window。若是你但願這裏的 this 不是 window,很簡單:

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

回到題目:

var obj = { foo: function() { console.log(this); } }; var bar = obj.foo; obj.foo(); // 轉換爲 obj.foo.call(obj),this 就是 obj bar(); // 轉換爲 bar.call() // 因爲沒有傳 context // 因此 this 就是 undefined // 最後瀏覽器給你一個默認的 this —— window 對象

[ ] 語法

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

咱們能夠把 arr0 想象爲 arr.0( ),雖而後者的語法錯了,可是形式與轉換代碼裏的 obj.child.method(p1, p2) 對應上了,因而就能夠愉快的轉換了:

arr[0]();

假想爲 arr.0()
而後轉換爲 arr.0.call(arr)
那麼裏面的 this 就是 arr 了 :)

小結:

  • this 就是你 call 一個函數時,傳入的第一個參數。
  • 若是你的函數調用不是 call 形式, 請將其轉換爲 call 形式

碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

3. JS 的 new 究竟是幹什麼的?

咱們聲明一個士兵,具備以下屬性:

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

咱們製造一個士兵, 只須要這樣:

兵營.製造(士兵);

若是須要製造 100 個士兵怎麼辦呢?

循環 100 次吧: var 士兵們 = [] var 士兵 for(var i=0; i<100; i++){ 士兵 = { ID: i, // ID 不能重複 兵種:"美國大兵", 攻擊力:5, 生命值:42, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防護:function(){ /*護臉*/ } } 士兵們.push(士兵) } 兵營.批量製造(士兵們)

哎呀,看起來好簡單

質疑

上面的代碼存在一個問題:浪費了不少內存

  • 行走、奔跑、死亡、攻擊、防護這五個動做對於每一個士兵實際上是同樣的,只須要各自引用同一個函數就能夠了,不必重複建立 100 個行走、100 個奔跑……
  • 這些士兵的兵種和攻擊力都是同樣的,不必建立 100 次。
  • 只有 ID 和生命值須要建立 100 次,由於每一個士兵有本身的 ID 和生命值。

改進

經過第一節能夠知道 ,咱們能夠經過原型鏈來解決重複建立的問題:咱們先建立一個「士兵原型」,而後讓「士兵」的 proto 指向「士兵原型」。

var 士兵原型 = { 兵種:"美國大兵", 攻擊力:5, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防護:function(){ /*護臉*/ } } var 士兵們 = [] var 士兵 for(var i=0; i<100; i++){ 士兵 = { ID: i, // ID 不能重複 生命值:42 } /*實際工做中不要這樣寫,由於 __proto__ 不是標準屬性*/ 士兵.__proto__ = 士兵原型 士兵們.push(士兵) } 兵營.批量製造(士兵們)

優雅?

有人指出建立一個士兵的代碼分散在兩個地方很不優雅,因而咱們用一個函數把這兩部分聯繫起來:

function 士兵(ID){ var 臨時對象 = {}; 臨時對象.__proto__ = 士兵.原型; 臨時對象.ID = ID; 臨時對象.生命值 = 42; return 臨時對象; } 士兵.原型 = { 兵種:"美國大兵", 攻擊力:5, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防護:function(){ /*護臉*/ } } // 保存爲文件:士兵.js 而後就能夠愉快地引用「士兵」來建立士兵了: var 士兵們 = [] for(var i=0; i<100; i++){ 士兵們.push(士兵(i)) } 兵營.批量製造(士兵們)

JS 之父看到你們都這麼搞,以爲何須呢,我給大家個糖吃,因而 JS 之父建立了 new 關鍵字,可讓咱們少寫幾行代碼:

只要你在士兵前面使用 new 關鍵字,那麼能夠少作四件事情:

  1. 不用建立臨時對象,由於 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 而後是建立士兵(加了一個 new 關鍵字): var 士兵們 = [] for(var i=0; i<100; i++){ 士兵們.push(new 士兵(i)) } 兵營.批量製造(士兵們)

new 的做用,就是省那麼幾行代碼。(也就是所謂的語法糖)

注意 constructor 屬性

new 操做爲了記錄「臨時對象是由哪一個函數建立的」,因此預先給「士兵.prototype」加了一個 constructor 屬性:

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

若是你從新對「士兵.prototype」賦值,那麼這個 constructor 屬性就沒了,因此你應該這麼寫:

士兵.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() { /*護臉*/ } };

4、繼承

繼承的本質就是上面的講的原型鏈

1)藉助構造函數實現繼承

function Parent1() { this.name = "parent1"; } Parent1.prototype.say = function() {}; function Child1() { Parent1.call(this); this.type = "child"; } console.log(new Child1());

打印結果:

這個主要是借用 call 來改變 this 的指向,經過 call 調用 Parent ,此時 Parent 中的 this 是指 Child1。有個缺點,從打印結果看出 Child1 並無 say 方法,因此這種只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法。

2)藉助原型鏈實現繼承

/** * 藉助原型鏈實現繼承 */ 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 這個語法糖,就能夠實現原型鏈繼承。但這裏有個老是,如打印結果,咱們給 s1.play 新增一個值 ,s2 也跟着改了。因此這個是原型鏈繼承的缺點,緣由是 s1.pro 和 s2.pro指向同一個地址即 父類的 prototype。

3)組合方式實現繼承

/** * 組合方式 */ 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()對應 Child3.prototype = new Parent3()還要 new 一次。

4)組合繼承的優化 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, 由於咱們經過構造函數就能夠拿到全部屬性和實例的方法,那麼如今我想繼承父類的原型對象,因此你直接賦值給我就行,不用在去 new 一次父類。其實這種方法仍是有問題的,若是我在控制檯打印如下兩句:

從打印能夠看出,此時我是沒有辦法區分一個對象 是直接 由它的子類實例化仍是父類呢?咱們還有一個方法判斷來判斷對象是不是類的實例,那就是用 constructor,我在控制檯打印如下內容:

咦,你會發現它指向的是父類 ,這顯然不是咱們想要的結果, 上面講過咱們 prototype 裏面有一個 constructor, 而咱們此時子類的 prototype 指向是 父類的 prototye ,而父類 prototype 裏面的 contructor 固然是父類本身的,這個就是產生該問題的緣由。

組合繼承的優化 2

/** * 組合繼承的優化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;

參考

相關文章
相關標籤/搜索