JavaScript原型鏈深入理解

RosellaQfn 8年前發布 | 8K 次閱讀 JavaScript開發 JavaScript

在JS中,原型鏈是一個重要的概念,不管是繼承還是屬性值的查找中,都用到了原型鏈的基本知識,有些朋友經常問我一些關于原型鏈的問題,今天整理一下自己對原型鏈的理解,下次我就不用在去解釋了,直接看文章。

首先,大家都知道在JS中有六種 基本數據 類型和一種 復雜類型

六種基本數據類型:

  1. String
  2. Number
  3. Boolean
  4. null
  5. undefined
  6. Symbol(ES2015新加入,解決屬性名的沖突問題。

另外一種復雜類型自然而然就是 Object ,有的人也說JS中 一切皆是對象 。上面的六種基本數據類型中,除了null和undefined沒有構造函數外,其他4種都對應有其構造函數對象,有時利用這些構造函數可以強制轉換數據類型。

要想講清楚原型鏈的一些問題,還有一個特殊的Object對象必須事先說清楚,那就是 Function 對象。它也是對象,只不過比其他普通對象復雜點罷了。

組合繼承

首先我們先寫一個大家熟悉的組合繼承(原型鏈+構造函數),然后根據這個組合繼承去了解原型鏈到底是怎樣連接起來的。

//父類
function Person(name,age){
  if(this instanceof Person){
    this.name = name;
    this.age = age;
  }else {
    Person.call(this,name,age);
  }
}

Person.prototype.say = function(){
  console.log('我叫' + this.name +',今年' + this.age + '歲了!')
}

//子類
function Student(name,age,gride){
  if(this instanceof Student){
    Person.call(this,name,age);
    this.gride = gride;
  }else {
    Student.call(this,name,age,gride);
  }
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

Student.prototype.say = function(){
  console.log('我叫' + this.name +',今年' + this.age + '歲了,考試考了' + this.gride + '分!');
}

var stu = new Student('chping',23,100);
stu.say();

此時會輸出 我叫chping,今年23歲了,考試考了100分! ,這說明我們的組合繼承就實現了,下面可以根據這個例子逐句解釋一下原型鏈的相關問題,順便講解這個組合繼承了。

第一個問題:安全使用構造函數

首先可能你對 PersonStudent 里面的那個判斷有些疑問,你可能是下面這樣寫構造函數:

function Person(name,age){
   this.name = name;
   this.age = age;
}

不這樣寫,主要是 防止構造函數被執行 ,因為一旦構造函數執行,其內的屬性值會被掛載到window上面去了(當構造函數執行的時候,里面的 this 是指向window的)。

好的,這個問題解決了,在看第二個問題。

第二個問題:prototype對象

接著我們來體會一下下面這句話:

每一個函數對象都有一個prototype屬性,該屬性指向其prototype對象。

這句話相信你已經聽了很多遍了,可能并不是這樣說的,但是就是這么個意思。那么這句話應該怎么理解呢?

其實也不難,先看前半部分 每一個函數對象都有一個prototype屬性 。這半句就是說 函數對象 默認有一個屬性,這個屬性叫 prototype 。另外,函數對象就是指用 function 聲明的對象(補充一下:在ES2015之前,有兩種聲明變量的方式, function 用來聲明函數變量, var 用來聲明普通變量。但是在ES2015中新加了 letconst )。

再看一下后半部分 該屬性指向其prototype對象 。后半句的意思就是說 prototype 屬性指向的是一個也叫 prototype 的對象,該對象是隨著函數對象而產生的。也就是說,只要通過 function 定義一個函數對象,就會生成一個 prototype對象 ,并在函數上生成一個 prototype屬性 來指向該 prototype屬性 。用圖來表示一下就是下面的樣子:

構造函數與其prototype對象

從圖中(此圖很丑,歡迎投稿:))你可以看到還有一個知識點,就是 prototype對象 中還有一個 constructor 對象,該對象又指向了構造函數,這也是一個需要注意的知識點,后面我們在展開來說,這里先記一下。

通過上面的這句話,我們可以聯想到,Object好像也是一個構造函數,Function好像也是一個函數,他們是不是也是這樣的呢?回答是肯定的,他們也是這樣的。如下圖:

Object的構造函數與其prototype對象

;

Function的構造函數與其prototype對象

第三個問題:__proto__ 屬性

先說明一下, __proto__ 的寫法是前后各兩個英文輸入法下的下劃線,不是一個。

然后我們再來看這樣一句話:

每一個對象都有一個__protto__屬性,該屬性指向創建這個對象的構造函數的prototype對象。

這句話稍微有點繞,我再來解釋一下這句話。這句話的前半部分比較好理解,就是說JS中的每一個對象都有一個屬性,這個屬性的名字叫做 __proto__ ,還要再說的話,就是注意 JS中一切皆是對象 這句話。

這句話后半部分有點繞,我們把它分成兩句話去理解:

  1. 創建這個對象的構造函數
  2. 的prototype對象

這樣就明白了,但是創建這個對象的構造函數怎么確定呢?這是個問題,不好解釋,我也解釋不好。就總結一下:

  1. function定義的函數對象的 __proto__ 屬性指向Function對象的prototype對象 。
  2. 非function定義的對象的 __proto__ 屬性指向創建它的構造函數的prototype對象 。(就是都指向Object的prototype對象)
  3. Object的prototype對象的 __proto__ 指向null

還是看圖吧:

對象的 proto 屬性

相信通過圖你已經看懂了 __proto__ 屬性的指向問題了。

思考組合繼承

//父類
function Person(name,age){
  if(this instanceof Person){
    this.name = name;
    this.age = age;
  }else {
    Person.call(this,name,age);
  }
}
//上面通過function聲明了一個函數對象,那么該對象的肯定有一個prototype屬性,
//并且指向其prototype對象。我們可以打印驗證一下

console.log(Person.prototype);
/*打印結果如下
{
  constructor: function Person(name,age)
    arguments:null
    caller:null
    length:2
    name:"Person"
    prototype:Object
    __proto__:function()  
  ,
  __proto__: Object
}
*/

這說明 prototype對象 就是一個空對象添加了一個 constructor屬性

另外,也看到了 prototype 對象有一個 __proto__ 屬性,指向Object,先記住。

此外,我們也看到了 constructor 指向的Person函數確實存在prototype屬性和 __proto__ 屬性,以及其指向問題,我們同時也可以打印驗證一下:

console.log(Person.prototype.constructor === Person);//true
console.log(Person.prototype.__proto__=== Object.prototype);//true
console.log(Person.__proto__=== Function.prototype);//true

我們再來驗證一下 ObjectFunction 的prototype屬性,constructor屬性還有 __proto__ 屬性

console.log(Object.prototype);
console.log(Object.prototype.constructor === Object);//true
console.log(Object.prototype.__proto__ === null);//true
console.log(Object.__proto__ === Function.prototype);//true

console.log(Function.prototype);
console.log(Function.prototype.constructor === Function);//true
console.log(Function.prototype.__proto__ === Object.prototype);//true
console.log(Function.__proto__ === Function.prototype);//true

OK,相信大家對原型鏈有了一定了解了。我們接著往下看。

Person.prototype.say = function(){
  console.log('我叫' + this.name +',今年' + this.age + '歲了!')
}
`

在Person的 prototype對象 上添加了一個say方法,和給普通對象添加方法并沒有區別,只不過在稍后使用的時候才會展現出它的與眾不同。

接下來是Student類:

function Student(name,age,gride){
  if(this instanceof Student){
    Person.call(this,name,age);
    this.gride = gride;
  }else {
    Student.call(this,name,age,gride);
  }
}

在此時,Student和Person以 Person.call(this,name,age); 這一句代碼產生了聯系,此時Student僅僅是通過構造函數繼承的方式調用了Person,這并不是本文重點,我們此時可以認為原型鏈上Student和Person并沒有任何聯系,讓它們在原型鏈上產生聯系的是下面這條語句。

Student.prototype = new Person();
Student.prototype.constructor = Student;

我們來想想這兩條語句都干了啥?

首先我們知道,Student函數對象在被 function 聲明的時候已經生成了其 prototype 對象,并且通過 prototype屬性 建立了聯系。

這里的第一條語句居然是, 改變了Student函數對象的prototype屬性指向,不再指向function聲明時自動生成的prototype對象,而是指向Person函數對象的一個實例對象。 讓我們用圖展示一下,就成了下面這樣:

原型鏈繼承關鍵步驟圖解

OK,原型鏈繼承就這樣實現了。但是由于我們讓 Student 函數對象的 prototype 屬性重新指向了一個 Person 函數對象的實例,而這個實例對象里面是不可能有 constructor 屬性的,自然也不會指向 Student (為什么沒有呢?前面已經說了,因為只有用 function 聲明函數對象的時候,自動生成的 prototype對象 中才默認有 constructor屬性 ,其他對象不會有)。

接下來就是在Student函數對象新指向的prototype對象上添加say方法:

Student.prototype.say = function(){
  console.log('我叫' + this.name +',今年' + this.age + '歲了,考試考了' + this.gride + '分!');
}

這個就沒有說的必要了,就是在對象上加了一個方法,只不過這個對象有些特別罷了。然后就是通過Student構造函數來生成實例:

var stu = new Student('chping',23,100);
stu.say();

此時,我們還是要看看這兩句干了啥?

先看第一句, new Student('chping',23,100); ,其中我們必須先得知道關鍵字 new 做了什么:

var obj = {}
Student.call(this,'chping',23,100);
obj.__proto__ = Student.prototype;
return obj;

這樣相信你就明白,第一句干的活了:

  1. 首先創建一個空對象。
  2. 將屬性掛載到該空對象上。
  3. 將空對象的 __proto__ 屬性連接到Student函數對象的prototype對象上,來生成原型鏈。
  4. 返回該對象給stu變量。

接下來就是第二句 stu.say() ,這句話的意思就是 stu實例 對象調用 say方法 ,但是在查找的時候發現, stu實例 對象上并沒有這個方法,于是 原型鏈 就來了。此時他會根據其 __proto__ 屬性來查找 Student 函數對象的 prototype對象 上有沒有 say方法 ,然后它發現正好有一個 say方法 ,于是就可以執行該方法了。

此時又產生一個問題,在執行Student函數對象的prototype上的say方法時,里面的 this 指向誰呢?

可以想一下,此時 say 方法是被誰調用的,很明顯是 stu 實例對象,所以 this 指向 stu ,所以, this.name 、 this.age 、 this.gride 就是實例對象 stu 上面的 chping 、 23 、 100 了。

下面我們再來看一下完整的原型鏈繼承的圖解,如果你能完全看懂這張圖,那么你對原型鏈的理解也就差不多了。

原型鏈繼承圖解

結語

原型鏈的基礎知識差不多通過上面這個例子就介紹完了,我們來總結一下:

  1. 每一個函數對象都有一個prototype屬性,該屬性指向其prototype對象 。
  2. 每一個對象都有一個 __protto__ 屬性,該屬性指向創建這個對象的構造函數的prototype對象 。
    1. function定義的函數對象的 proto 屬性指向Function對象的prototype對象 。
    2. 非function定義的對象的 proto 屬性指向創建它的構造函數的prototype對 。(就是都指向Object的prototype對象)
    3. Object的prototype對象的 proto 指向null

然后在看看下面幾個常見的原型鏈的小題目,相信你對原型鏈會有一個新的認識了。

第一題

function Person(){
  this.name = 1;
}
var person1 = new Person();
Person.prototype.name =2;
console.log(person1.name); 
console.log(person1.__proto__.name); 

Person.prototype = {
  name:3
}
console.log(person1.name); 
console.log(person1.__proto__.constructor);
console.log(person1.__proto__.name);

var person2 = new Person();
console.log(person2.__proto__.name); 
console.log(person2.__proto__.constructor == Object);
console.log(person2.name);

上面的 console.log 會打印什么?

這個題考察的是對prototype對象的理解。

第二題

function Outer() {
  this.a = 1;
}

function Inner() {}

var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();

inner.a = inner.a + 1;

console.log(inner);
console.log(outer);

猜猜上面會是什么結果?

這個題考察的是對實例對象上屬性的理解。

第三題

var animal = function(){}
var dog =function(){}
animal.price = 2000;
dog.prototype = animal;
var dd = new dog();

console.log(dog.price);
console.log(dd.price);

在分析一下這個題目的輸出結果?

這個題目考察的是 __proto__ 屬性的理解。

第四題

下面放大招了,這個題目可能不完全是原型鏈的問題,對JS基礎知識的一個綜合考察,可以試一試:

var a = new Object();
a.param = 123;

function foo(){
  get = function(){
    console.log(1);
  };
  return this;
}

foo.get = function(){
  console.log(2);
};

foo.prototype.get = function(){
  console.log(3);
};

var get = function(){
  console.log(4);
};

function get(){
  console.log(5);
}

foo.get();
get();
foo().get();
get();
new foo.get();
new foo().get();
new new foo().get();

 

來自:http://www.jianshu.com/p/b745c5481fab

 

 本文由用戶 RosellaQfn 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!