首頁 > webfront > ECMAS > js > > 正文

js原型鏈之大話西游—簡單粗暴地理解JavaScript原型鏈

發布人:[email protected]    點擊:

隨便拉一個碼農估計都會寫JS,大部分也知道JS是基于原型的語言,但是如果問及JS原生對象(Object,Function,Array,Date等)的這個原型鏈長什

隨便拉一個碼農估計都會寫JS,大部分也知道JS是基于原型的語言,但是如果問及JS原生對象(Object,Function,Array,Date等)的這個原型鏈長什么樣子?估計能回答出來的人就少了,特別是沒有學過java的小鳥們,繼承是啥?

繼承是OO語言中的一個最為人津津樂道的概念。ECMAScript只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。

 不像JAVA是完全OOP的語言:class是一類具有共同特點的物體的抽象,object是某個class下具體的一個實現,Object類是所有類的頂層父類,對Java的認知是從類與對象開始的(javascript萬物皆對象);Javascript則不然,它是從類型(type)開始,在各類語言中遇到的number,string, boolean, object, function,array等都屬于類型。這里總結為兩大類:原生類型與對象類型

原生類型包括:number,string, bool, null, undefined;

非原生類型對象都屬于對象類型,包括:object, array, function等

那這里的object專指具有屬性(attribute)的對象,在判斷某個值是什么大的類型沒有意義,往往需要判斷它是什么原生類型或者對象類型:

判斷原生類型,可以使用typeof關鍵字;

判斷對象類型,可以使用toString()方法;

這個是茄子寫:簡單粗暴地理解JavaScript原型鏈處 感覺寫的特別好,拿過來來排版整理下,覺得能更快理解!

function Person (name) { this.name = name; }
function Mother () { }
Mother.prototype = {    //Mother的原型
    age: 18,
    home: ['Beijing', 'Shanghai']
};
Person.prototype = new Mother(); //Person的原型為Mother

//用chrome調試工具查看,提供了__proto__接口查看原型,這里有兩層原型,各位還是直接看chrome好一點。
var p1 = new Person('Jack'); //p1:'Jack'; __proto__:{__proto__:18,['Beijing','Shanghai']}
var p2 = new Person('Mark'); //p2:'Mark'; __proto__:{__proto__:18,['Beijing','Shanghai']}

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__:{__proto__:18,['Shenzhen','Shanghai']}
 * p2:'Mark';    __proto__:{__proto__:18,['Shenzhen','Shanghai']}
 */

p1.home = ['Hangzhou', 'Guangzhou']; 
/* 其實跟p1.age=20一樣的操作。換成這個理解: var o={}; o.home=['big','house']
 * p1:'Jack',20,['Hangzhou','Guangzhou']; __proto__:{__proto__:18,['Shenzhen','Shanghai']}
 * p2:'Mark';                             __proto__:{__proto__:18,['Shenzhen','Shanghai']}
 */

delete p1.age;    
/* 刪除實例的屬性之后,原本被覆蓋的原型值就重見天日了。正如你剃了光頭,遺傳的迷人小卷發就長出來了。
 * 這就是向上搜索機制,先搜你,然后你媽,再你媽他媽,所以你媽的改動會動態影響你。
 * p1:'Jack',['Hangzhou','Guangzhou']; __proto__:{__proto__:18,['Shenzhen','Shanghai']}
 * p2:'Mark';                          __proto__:{__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.prototype,p1p2會變,但上面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的原型已經完全變成一個新的對象了!人他媽換了個后媽!
 * 由于上面Person與Mother已經斷開聯系了,這時候Mother怎么變已經不影響Person了。
 * p4:'Tony';__proto__: 28 {country: 'USA', city: 'Washington'}
 */
 
Person.prototype = new Mother(); //再次綁定
var p5 = new Person('Luffy');
// 這個時候如果需要應用這些改動的話,那就要重新將Person的原型綁到mother上了
// p5:'Luffy';__proto__:{__proto__: 2, ['run','walk']}

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

看完基本能理解了吧?

現在再來說說 p1.age = 20、p1.home = ['Hangzhou', 'Guangzhou'] 和  p1.home[0] =  ‘Shenzhen’ 的區別。 p1.home[0] = ‘Shenzhen’;  總結一下是  p1.object.method,p1.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 啊。

實例在分析:

//父類構造函數
function Parent() {
  this.name = "baba";
}
//父類原型方法
Parent.prototype.getName = function () {
  return this.name;
}
//子類構造函數
function Child() {
  this.name = "cc";
}
//類式繼承
classInherit(Parent, Child);
//實例
var child = new Child();
alert(child.getName()) //“baba”

下面我們來看看這個繼承的關鍵方法:classInherit(Parent,Child)

var classInherit = (function () {
  var F = function () { }
  return function (P, C) {
    F.prototype = P.prototype;
    C.prototype = new F();
    C.prototype.constructor = C;
  }
}());

分析一下這個方法:

首先創建一個空的構造函數F(),用其實例的_proto_屬性來構建父類與子類的原型鏈。起到一個代理的作用,目的是為了防止C.prototype = P.prototype,這樣會在子類實例化后修改屬性或方法時候,連同父類一起修改。整體采用即時函數并且在閉包中存儲F(),防止多次繼承時候創建大量的空的構造函數,從而減少消耗內存。最后一行的意思是,由于原型鏈的關系,C的實例對象的constructor會指向P,所以重新設置。

js原型鏈式圖解


3)復制繼承

//復制繼承
function copyInherit(p, c) {
  var i,
    toStr = Object.prototype.toString,
    astr = "[object Array]";
  c = c || {};
  for (i in p) {
    if (p.hasOwnProperty(i)) {
      if (typeof p[i] === "object") {
        c[i] = toStr.call(p[i]) == astr ? [] : {};
        c[i] = copy(p[i], c[i]);
      }
      else {
        c[i] = p[i];
      }
    }
  }
  return c;
}
//重寫Parent
function Parent() {
  this.name = "pp";
  this.obj= {a:1,b:2};
  this.arr= [1, 2]
}
//實例
var child = new Child();
var parent = new Parent();
copyInherit(parent, child);
alert(child.name) //"baba"
alert(child.arr) //1,2
alert(child.obj.a) //1

分析下copyInherit(p,c)

當一個值賦予一個變量時候,分為傳值和傳引用兩種方式,當你父對象內屬性包含數組類型或是對象類型時候,  c[i] = toStr.call(p[i]) == astr ? [] : {};這一句會避免修改子對象屬性而引起的父對象屬性被篡改。


小結

說了這么多,其實核心只有一個:屬性共享和獨立的控制,當你的對象實例需要獨立的屬性,所有做法的本質都是在對象實例里面創建屬性。若不考慮太多, 你大可以在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);
};

顯然,當我們要創建批量的person1、person2……時,每次都要敲很多代碼,資深copypaster都吃不消!然后就有了批量生產的工廠模式。

2)工廠模式

/2.工廠模式,定義一個函數創建對象
function creatPerson (name, age) {
    var person = new Object(); 
    person.name = name;
    person.age = age;
    person.sayName = function () {
        alert(this.name);
    };
    return person; 
}

工廠模式就是批量化生產,簡單調用就可以進入造人模式(啪啪啪……)。指定姓名年齡就可以造一堆小寶寶啦,解放雙手。但是由于是工廠暗箱操作的,所 以你不能識別這個對象到底是什么類型、是人還是狗傻傻分不清(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 以節省內存。

構造函數、原型和實例之間的關系

①+Object

②+Function+Object+Array

來個繞口令:

function Foo1() {
  this.name1 = '1';
}
function Foo2() {
  this.name2 = '2';
}
Foo2.prototype = new Foo1();
function Foo3() {
  this.name = '3';
}
Foo3.prototype = new Foo2();
var foo3 = new Foo3();
console.dir(foo3);



存在的問題:可以解決原型繼承當中傳參問題,但是父類型當中的原型對象上的成員(屬性和方法)不能被繼承到


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

做法是將需要獨立的屬性方法放入構造函數中,而可以共享的部分則放入原型中,這樣做可以最大限度節省內存而又保留對象實例的獨立性。


總結:

1.原型和原型鏈是JS實現繼承的一種模型。

2.原型鏈的形成是真正是靠__proto__ 而非prototype

要深入理解這句話,我們再舉個例子,看看前面你真的理解了嗎?

var animal = function(){};
  var dog = function(){};

  animal.price = 2000;//
  dog.prototype = animal;
  var tidy = new dog();


  console.log(dog.price) //undefined
  console.log(tidy.price) // 2000

為什么呢?畫一下內存圖:

這說明什么問題呢,執行dog.price的時候,發現沒有price這個屬性,雖然prototype指向的animal有這個屬性,但它并沒有去沿著這個“鏈”去尋找。同樣,執行tidy.price的時候,也沒有這個屬性,但是__proto__指向了animal,它會沿著這個鏈去尋找,animal中有price屬性,所以tidy.price輸出2000。由此得出,原型鏈的真正形成是靠的__proro__,而不是prototype。

因此,如果在這樣指定dog.__proto__ = animal。那dog.price = 2000。

最后打個比喻,雖然不是很確切,但可能對原型的理解有些幫助。



  父親(函數對象),先生了一個大兒子(prototype),也就是你大哥,父親給你大哥買了好多的玩具,當你出生的時候,你們之間的親情紐帶(__proto__)會讓你自然而然的擁有了你大哥的玩具。同樣,你也先生個大兒子,又給他買了好多的玩具,當你再生兒子的時候,你的小兒子會自然擁有你大兒子的所有玩具。至于他們會不會打架,這不是我們的事了。

所以說,你是從你大哥那繼承的,印證了那句“長兄如父”啊!



不要糾結于那一堆術語了,那除了讓你腦筋擰成麻花,真的不能幫你什么。簡單粗暴點看原型鏈吧,想點與代碼無關的事,比如人、妖以及人妖。

1)人是人他媽生的,妖是妖他媽生的。人和妖都是對象實例,而人他媽和妖他媽就是原型。原型也是對象,叫原型對象。

2)人他媽和人他爸啪啪啪能生出一堆人寶寶、妖他媽和妖他爸啪啪啪能生出一堆妖寶寶,啪啪啪就是構造函數,俗稱造人。

3)人他媽會記錄啪啪啪的信息,所以可以通過人他媽找到啪啪啪的信息,也就是說能通過原型對象找到構造函數。

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

5)人他媽也是由人他媽他媽生的,通過人他媽找到人他媽他媽,再通過人他媽他媽找到人他媽他媽……,這個關系叫做原型鏈。

6)原型鏈并不是無限的,當你通過人他媽一直往上找,最后發現你會發現人他媽他媽他媽……的他媽都不是人,也就是原型鏈最終指向null。

7)人他媽生的人會有人的樣子,妖他媽生的妖會有妖的丑陋,這叫繼承。

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

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

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

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

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

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

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

尼瑪!你特么也是夠了!!

參考文章:

http://2660311.blog.51cto.com/2650311/1358226/

http://www.108js.com/article/article1/10201.html?id=1092

http://www.cnblogs.com/DF-fzh/p/5619319.html