1. 程式人生 > >JS高階---原型鏈(一看就懂,但18歲以下請繞道)

JS高階---原型鏈(一看就懂,但18歲以下請繞道)

    1)小明是小明媽生的,小狗是小狗媽生的。小明和小狗都是物件例項,而小明媽和小狗媽就是原型。原型也是物件,原型物件。

     2)小明媽和小明爸啪啪啪能生出一堆小明明、小狗媽和小狗爸啪啪啪能生出一堆小狗狗,啪啪啪就是建構函式,俗稱造人或造狗。

     3)小明媽會記錄啪啪啪的資訊,所以可以通過小明媽找到啪啪啪的資訊,也就是說能通過原型物件找到建構函式

    4)小明媽可以生很多寶寶,但這些寶寶只有一個媽媽,這就是原型的唯一性

     5)小明媽也是由小明媽的媽媽生的,通過小明媽找到小明媽的媽媽,再通過小明媽的媽媽找到媽媽……,這個關係叫做原型鏈

    6)原型鏈並不是無限的,當你通過小明的媽媽一直往上找,最後發現你會發現小明媽媽的媽媽……的媽媽都不是人,也就是

原型鏈最終指向null

    7)人的媽媽生的人會有人的樣子,狗的媽媽生的狗會有狗的醜陋,這叫繼承

     8)你繼承了你媽媽的膚色,你媽媽繼承了你媽媽的媽媽的膚色,你媽媽的媽……,這就是原型鏈的繼承

    9)你沒有家,那你家指的就是你媽媽的家;你媽也沒有家,那你家指的就是你媽媽的媽媽家……這就是原型鏈的向上搜尋。

  10)你會繼承你媽的樣子,但是你也可以去染髮洗剪吹,就是說物件的屬性可以自定義,會覆蓋繼承得到的屬性。

==== =以下 網上說的不多,感覺一些公司也不會經常用----理解就好  =====

   11)雖然你洗剪吹了染成黃毛了,但你不能改變你媽的樣子,你媽生的弟弟妹妹跟你的黃毛洗剪吹沒一點關係,就是說物件例項不能改動原型的屬性。

   12)但是你家被你玩火燒了的話,那就是說你家你媽家你弟們家都被燒了,這就是原型屬性的共享。

  13)你媽外號阿珍,鄰居大娘都叫你阿珍兒,但你媽頭髮從飄柔做成了金毛獅王后,隔壁大嬸都改口叫你金毛獅王子,這叫原型的動態性。

   14)你媽愛美,又跑到韓國整形,整到你媽他媽都認不出來,即使你媽頭髮換回飄柔了,但隔壁鄰居還是叫你金毛獅王子。因為沒人認出你媽,整形後的你媽已經回爐再造了,這就是原型的整體重寫。

1.建構函式建立物件 (請注意單詞的大小寫,意思完全不同)

     function Person(name){

        this.name = name;

     }

     function Mother(){ }

     Mother.prototype = {      //Mother的原型

         age: 18,

         home: ['Beijing']

     }

Person.prototype = new Mother();   //Person的原型為Mother


在這個例子中,Person就是一個建構函式,我們使用new建立了一個例項物件person

//chrome除錯工具檢視,提供了__proto__介面檢視原型

var p1 = new Person('Jack');    //p1:'Jack'; __proto__:18,['Beijing']

var p2 = new Person('Mark');   //p2:'Mark'; __proto__:18,['Beijing']

p1.age = 20; 


/* 例項不能改變原型的基本值屬性,正如你洗剪吹染黃毛跟你媽無關

*p1例項下增加一個age屬性的普通操作,與原型無關。跟var o={}; o.age=20一樣。

* p1:下面多了個屬性age,而__proto__Mother.prototype一樣,age=18

* p2:只有屬性name__proto__Mother.prototype一樣

*/

p1.home[0] = 'Shenzhen';

/* 原型中引用型別屬性的共享,正如你燒了你家,就是燒了你全家的家

 * 這個先過,下文再仔細嘮叨一下可好?

 * p1'Jack',20; __proto__:18,['Shenzhen','Shanghai']

 * p2'Mark';    __proto__:18,['Shenzhen','Shanghai']

 */

p1.home = ['Hangzhou', 'Guangzhou'];

/* 其實跟p1.age=20一樣的操作。換成這個理解: var o={}; o.home=['big','house']

 * p1'Jack',20,['Hangzhou','Guangzhou']; __proto__:18,['Shenzhen','Shanghai']

 * p2'Mark';                             __proto__:18,['Shenzhen','Shanghai']

 */

delete p1.age;    

/* 刪除例項的屬性之後,原本被覆蓋的原型值就重見天日了。正如你剃了光頭,遺傳的迷人小卷發就長出來了。

 * 這就是向上搜尋機制,先搜你,然後你媽,再你媽他媽,所以你媽的改動會動態影響你。

  * p1'Jack',['Hangzhou','Guangzhou']; __proto__:18,['Shenzhen','Shanghai']

 * p2'Mark';                           __proto__:18,['Shenzhen','Shanghai']

 */

Person.prototype.lastName = 'Jin';

/* 改寫原型,動態反應到例項中。正如你媽變新潮了,鄰居提起你都說是潮婦的兒子

 * 注意,這裡我們改寫的是Person的原型,就是往Mother里加一個lastName屬性,等同於Mother.lastName='Jin'

 * 這裡並不是改Mother.prototype,改動不同的層次,效果往往會有很大的差異。

 * p1'Jack',['Hangzhou','Guangzhou']; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']

 * p2'Mark';                          __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']

 */

Person.prototype = {

    age: 28,

    address: { country: 'USA', city: 'Washington' }

};

var p3 = new Person('Obama');

/* 重寫原型!這個時候Person的原型已經完全變成一個新的物件了,也就是說Person換了個媽,叫後媽。

 * 換成這樣理解:var a=10; b=a; a=20; c=a。所以b不變,變得是c,所以p3跟著後媽變化,與親媽無關。

 * p1'Jack',['Hangzhou','Guangzhou']; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']

 * p2'Mark';                          __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']

 * p3:'Obama';__proto__: 28 {country: 'USA', city: 'Washington'}

 */

Mother.prototype.no = 9527;

/* 改寫原型的原型,動態反應到例項中。正如你媽他媽變新潮了,鄰居提起你都說你丫外婆真潮。

 * 注意,這裡我們改寫的是Mother.prototypep1p2會變,但上面p3跟親媽已經了無瓜葛了,不影響他。

 * p1'Jack',['Hangzhou','Guangzhou']; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai'],9527

 * p2'Mark';                          __proto__:'jin';__proto__:18,['Shenzhen','Shanghai'],9527

 * p3:'Obama';__proto__: 28 {country: 'USA', city: 'Washington'}

 */

Mother.prototype = {

    car: 2,

    hobby: ['run','walk']

};

var p4 = new Person('Tony');

/* 重寫原型的原型!這個時候Mother的原型已經完全變成一個新的物件了!人他媽換了個後媽!

 * 由於上面PersonMother已經斷開聯絡了,這時候Mother怎麼變已經不影響Person了。

 * p4:'Tony';__proto__: 28 {country: 'USA', city: 'Washington'}

 */

Person.prototype = new Mother(); //再次繫結

var p5 = new Person('Luffy');

// 這個時候如果需要應用這些改動的話,那就要重新將Person的原型綁到mother上了

// p5:'Luffy';__proto__: 2, ['run','walk']

p1.__proto__.__proto__.__proto__.__proto__ //null,你說原型鏈的終點不是null

Mother.__proto__.__proto__.__proto__    //null,你說原型鏈的終點不是null

看完基本能理解了吧?

現在再來說說 p1.age = 20p1.home = ['Hangzhou', 'Guangzhou'] 和  p1.home[0] = Shenzhen’ 的區別。 p1.home[0] = Shenzhen;  總結一下是 p1.object.methodp1.object.property 這樣的形式。

p1.age = 20;  p1.home = ['Hangzhou', 'Guangzhou'];這兩句還是比較好理解的,先忘掉原型吧,想想我們是怎麼為一個普通物件增加屬性的:

var obj = new Object();

obj.name='xxx';

obj.num = [100, 200];

這樣是不是就理解了呢?一樣一樣的呀。

那為什麼 p1.home[0] = Shenzhen’ 不會在 p1 下建立一個 home 陣列屬性,然後將其首位設為 ’Shenzhen’呢? 我們還是先忘了這個,想想上面的obj物件,如果寫成這樣: var obj.name = xxx, obj.num = [100, 200],能得到你要的結果嗎? 顯然,除了報錯你什麼都得不到。因為obj還未定義,又怎麼能往裡面加入東西呢?同理,p1.home[0]中的 home p1 下並未被定義,所以也不能直接一步定義 home[0] 了。如果要在p1下建立一個 home 陣列,當然是這麼寫了:

p1.home = [];

p1.home[0] = 'Shenzhen';

這不就是我們最常用的辦法嗎?

而之所以 p1.home[0] = Shenzhen’ 不直接報錯,是因為在原型鏈中有一個搜尋機制。當我們輸入 p1.object 的時候,原型鏈的搜尋機制是先在例項中搜索相應的值,找不到就在原型中找,還找不到就再往上一級原型中搜索……一直到了原型鏈的終點,就是到null還沒找到的話,就返回一個 undefined。當我們輸入 p1.home[0] 的時候,也是同樣的搜尋機制,先搜尋 p1 看有沒有名為 home 的屬性和方法,然後逐級向上查詢。最後我們在Mother的原型裡面找到了,所以修改他就相當於修改了 Mother 的原型啊。

一句話概括:p1.home[0] = Shenzhen’  等同於  Mother.prototype.home[0] = Shenzhen’。

由上面的分析可以知道,原型鏈繼承的主要問題在於屬性的共享,很多時候我們只想共享方法而並不想要共享屬性,理想中每個例項應該有獨立的屬性。因此,原型繼承就有了下面的兩種改良方式:

1)組合繼承

function Mother (age) {

    this.age = age;

    this.hobby = ['running','football']

}

Mother.prototype.showAge = function () {

    console.log(this.age);

};

function Person (name, age) {

    Mother.call(this, age);//第二次執行

    this.name = name;

}

Person.prototype = new Mother(); //第一次執行

Person.prototype.constructor = Person;

Person.prototype.showName = function () {

    console.log(this.name);

}


var p1 = new Person('Jack', 20);

p1.hobby.push('basketball');  //p1:'Jack'; __proto__:20,['running','football']

var p2 = new Person('Mark', 18);  //p2:'Mark'; __proto__:18,['running','football']

 

結果是醬紫的:

這裡第一次執行的時候,得到 :

   Person.prototype.age = undefined,

   Person.prototype.hobby = ['running','football'],

第二次執行也就是 var p1 = new Person(‘Jack’, 20) 的時候,得到

   p1.age =20,

   p1.hobby = ['running','football'],


push後就變成了 p1.hobby = ['running','football', 'basketball']。其實分辨好 this 的變化,理解起來也是比較簡單的,把 this 簡單替換一下就能得到這個結果了。 如果感覺理解起來比較繞的話,試著把腦子裡面的概念扔掉吧,把自己當瀏覽器從上到下執行一遍程式碼,結果是不是就出來了呢?

通過第二次執行原型的建構函式 Mother(),我們在物件例項中複製了一份原型的屬性,這樣就做到了與原型屬性的分離獨立。細心的你會發現,我們第一次呼叫 Mother(),好像什麼用都沒有呢,能不呼叫他嗎?可以,就有了下面的寄生組合式繼承。

2)寄生組合式繼承

function object(o){

      function F(){}
 
     F.prototype = o;

     return new F();

}


function inheritPrototype(Person, Mother){

      var prototype = object(Mother.prototype);
   
      prototype.constructor = Person;    
    
      Person.prototype = prototype;    

}

function Mother (age) {

      this.age = age;
 
      this.hobby = ['running','football']

}

Mother.prototype.showAge = function () {

     console.log(this.age);

};


function Person (name, age) {

    Mother.call(this, age);

    this.name = name;

}

inheritPrototype(Person, Mother);

Person.prototype.showName = function () {

    console.log(this.name);

}


 

var p1 = new Person('Jack', 20);

p1.hobby.push('basketball');//p1:'Jack'; __proto__:20,['running','football']

var p2 = new Person('Mark', 18); //p2:'Mark'; __proto__:18,['running','football']

 


結果是醬紫的:

原型中不再有 age hobby 屬性了,只有兩個方法,正是我們想要的結果!

關鍵點在於 object(o) 裡面,這裡借用了一個臨時物件來巧妙避免了呼叫new Mother(),然後將原型為 o 的新物件例項返回,從而完成了原型鏈的設定。很繞,對吧,那是因為我們不能直接設定 Person.prototype = Mother.prototype 啊。

小結

說了這麼多,其實核心只有一個:屬性共享和獨立的控制,當你的物件例項需要獨立的屬性,所有做法的本質都是在物件例項裡面建立屬性。若不考慮太多,你大可以在Person裡面直接定義你所需要獨立的屬性來覆蓋掉原型的屬性。總之,使用原型繼承的時候,要對於原型中的屬性要特別注意,因為他們都是牽一髮而動全身的存在。

下面簡單羅列下js中建立物件的各種方法,現在最常用的方法是組合模式,熟悉的同學可以跳過到文章末尾點讚了。

1)原始模式

//1.原始模式,物件字面量方式

var person = {

    name: 'Jack',

    age: 18,

    sayName: function () { alert(this.name); }

};


//1.原始模式,Object建構函式方式

var person = new Object();

person.name = 'Jack';

person.age = 18;

person.sayName = function () {

    alert(this.name);

};


顯然,當我們要建立批量的person1person2……時,每次都要敲很多程式碼,資深copypaster都吃不消!然後就有了批量生產的工廠模式。

2)工廠模式

//2.工廠模式,定義一個函式建立物件

function creatPerson (name, age) {

    var temp = new Object();

    person.name = name;

    person.age = age;

    person.sayName = function () {

        alert(this.name);

    };

    return temp;

}

工廠模式就是批量化生產,簡單呼叫就可以進入造人模式(啪啪啪……)。指定姓名年齡就可以造一堆小寶寶啦,解放雙手。但是由於是工廠暗箱操作的,所以你不能識別這個物件到底是什麼型別、是人還是狗傻傻分不清(instanceof 測試為 Object),另外每次造人時都要建立一個獨立的temp物件,程式碼臃腫,雅蠛蝶啊。

3)建構函式

//3.建構函式模式,為物件定義一個建構函式

function Person (name, age) {

    this.name = name;

    this.age = age;

    this.sayName = function () {

        alert(this.name);

    };    

}

var p1 = new Person('Jack', 18); //建立一個p1物件

Person('Jack', 18);    //屬性方法都給window物件,window.name='Jack',window.sayName()會輸出Jack

建構函式與C++JAVA中類的建構函式類似,易於理解,另外Person可以作為型別識別(instanceof 測試為 Person Object)。但是所有例項依然是獨立的,不同例項的方法其實是不同的函式。這裡把函式兩個字忘了吧,把sayName當做一個物件就好理解了,就是說張三的 sayName 和李四的 sayName是不同的存在,但顯然我們期望的是共用一個 sayName 以節省記憶體。

4)原型模式

//4.原型模式,直接定義prototype屬性

function Person () {}

Person.prototype.name = 'Jack';

Person.prototype.age = 18;

Person.prototype.sayName = function () { alert(this.name); };

//4.原型模式,字面量定義方式

function Person () {}

Person.prototype = {

    name: 'Jack',

    age: 18,

    sayName: function () { alert(this.name); }

};

var p1 = new Person(); //name='Jack'

var p2 = new Person(); //name='Jack'

這裡需要注意的是原型屬性和方法的共享,即所有例項中都只是引用原型中的屬性方法,任何一個地方產生的改動會引起其他例項的變化。

5)混合模式(構造+原型)

//5. 原型構造組合模式,

function Person (name, age) {

    this.name = name;

    this.age = age;

}

Person.prototype = {

    hobby: ['running','football'];

    sayName: function () { alert(this.name); },

    sayAge: function () { alert(this.age); }

};

var p1 = new Person('Jack', 20);

//p1:'Jack',20; __proto__: ['running','football'],sayName,sayAge

var p2 = new Person('Mark', 18);

//p1:'Mark',18;__proto__: ['running','football'],sayName,sayAge


做法是將需要獨立的屬性方法放入建構函式中,而可以共享的部分則放入原型中,這樣做可以最大限度節省記憶體而又保留物件例項的獨立性。