原型、原型鏈、閉包、繼承


一、原型、原型鏈

原型對象
   在JavaScript 中,每當定義一個對象(函數)時候,對象中都會包含一些預定義的屬性。其中函數對象的一個屬性就是原型對象 prototype。注:普通對象沒有prototype,但有__proto__屬性。

  原型對象其實就是普通對象(Function.prototype除外,它是函數對象,但它很特殊,他沒有prototype屬性(前面說道函數對象都有prototype屬性))。看下面的例子:
 function f1(){};
 console.log(f1.prototype) //f1{}
 console.log(typeof f1. prototype) //Object
 console.log(typeof Function.prototype) // Function,這個特殊
 console.log(typeof Object.prototype) // Object
 console.log(typeof Function.prototype.prototype) //undefined

 從這句console.log(f1.prototype) //f1 {} 的輸出就結果可以看出,f1.prototype就是f1的一個實例對象。就是在f1創建的時候,創建了一個它的實例對象並賦值給它的prototype,基本過程如下:
 var temp = new f1();
 f1. prototype = temp;

  所以,Function.prototype為什么是函數對象就迎刃而解了,上文提到凡是new Function ()產生的對象都是函數對象,所以temp1是函數對象。
 var temp1 = new Function ();
 Function.prototype = temp1;

那原型對象是用來做什么的呢?主要作用是用於繼承。舉了例子:
  var person = function(name){
   this.name = name
  };
  person.prototype.getName = function(){
     return this.name; 
  }
  var zjh = new person(‘zhangjiahao’);
  zjh.getName(); //zhangjiahao

   從這個例子可以看出,通過給person.prototype設置了一個函數對象的屬性,那有person實例(例中:zjh)出來的普通對象就繼承了這個屬性。具體是怎么實現的繼承,就要講到下面的原型鏈了。

三.原型鏈
   JS在創建對象(不論是普通對象還是函數對象)的時候,都有一個叫做__proto__的內置屬性,用於指向創建它的函數對象的原型對象prototype。以上面的例子為例:

  console.log(zjh.__proto__ === person.prototype) //true

同樣,person.prototype對象也有__proto__屬性,它指向創建它的函數對象(Object)的prototype

  console.log(person.prototype.__proto__ === Object.prototype) //true

繼續,Object.prototype對象也有__proto__屬性,但它比較特殊,為null

  console.log(Object.prototype.__proto__) //null

我們把這個有__proto__串起來的直到Object.prototype.__proto__為null的鏈叫做原型鏈。如下圖:

四.內存結構圖
為了更加深入和直觀的進行理解,下面我們畫一下上面的內存結構圖:


畫圖約定:


疑點解釋:
1.Object.__proto__ === Function.prototype // true
  Object是函數對象,是通過new Function()創建,所以Object.__proto__指向Function.prototype。

2.Function.__proto__ === Function.prototype // true
  Function 也是對象函數,也是通過new Function()創建,所以Function.__proto__指向Function.prototype。

自己是由自己創建的,好像不符合邏輯,但仔細想想,現實世界也有些類似,你是怎么來的,你媽生的,你媽怎么來的,你姥姥生的,……類人猿進化來的,那類人猿從哪來,一直追溯下去……,就是無,(NULL生萬物)
正如《道德經》里所說“無,名天地之始”。

3.Function.prototype.__proto__ === Object.prototype //true
其實這一點我也有點困惑,不過也可以試着解釋一下。
Function.prototype是個函數對象,理論上他的__proto__應該指向 Function.prototype,就是他自己,自己指向自己,沒有意義。
JS一直強調萬物皆對象,函數對象也是對象,給他認個祖宗,指向Object.prototype。Object.prototype.__proto__ === null,保證原型鏈能夠正常結束。

五.constructor
  原型對象prototype中都有個預定義的constructor屬性,用來引用它的函數對象。這是一種循環引用
  person.prototype.constructor === person //true
  Function.prototype.constructor === Function //true
  Object.prototype.constructor === Object //true

完善下上面的內存結構圖:


有兩點需要注意:
(1)注意Object.constructor===Function;//true 本身Object就是Function函數構造出來的
(2)如何查找一個對象的constructor,就是在該對象的原型鏈上尋找碰到的第一個constructor屬性所指向的對象

六.總結
1.原型和原型鏈是JS實現繼承的一種模型。
2.原型鏈的形成是真正是靠__proto__ 而非prototype

二、閉包

一、什么是閉包?

官方”的解釋是:閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
相信很少有人能直接看懂這句話,因為他描述的太學術。其實這句話通俗的來說就是:JavaScript中所有的function都是一個閉包。不過一般來說,嵌套的function所產生的閉包更為強大,也是大部分時候我們所謂的“閉包”。看下面這段代碼:

function a() { 
var i = 0;
function b() { alert(++i); }
return b;
}
var c = a();
c();

這段代碼有兩個特點:

1、函數b嵌套在函數a內部;

2、函數a返回函數b。

引用關系如圖:

  這樣在執行完var c=a()后,變量c實際上是指向了函數b,再執行c()后就會彈出一個窗口顯示i的值(第一次為1)。這段代碼其實就創建了一個閉包,為什么?因為函數a外的變量c引用了函數a內的函數b,就是說:

  當函數a的內部函數b被函數a外的一個變量引用的時候,就創建了一個閉包。

  讓我們說的更透徹一些。所謂“閉包”,就是在構造函數體內定義另外的函數作為目標對象的方法函數,而這個對象的方法函數反過來引用外層函數體中的臨時變量。這使得只要目標 對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變量值。盡管最開始的構造函數調用已經結束,臨時變量的名稱也都消失了,但在目 標對象的方法內卻始終能引用到該變量的值,而且該值只能通這種方法來訪問。即使再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變量只是對應新 的值,和上次那次調用的是各自獨立的。

二、閉包有什么作用?

  簡而言之,閉包的作用就是在a執行完並返回后,閉包使得Javascript的垃圾回收機制GC不會收回a所占用的資源,因為a的內部函數b的執行需要依賴a中的變量。這是對閉包作用的非常直白的描述,不專業也不嚴謹,但大概意思就是這樣,理解閉包需要循序漸進的過程。

在上面的例子中,由於閉包的存在使得函數a返回后,a中的i始終存在,這樣每次執行c(),i都是自加1后alert出i的值。

  那 么我們來想象另一種情況,如果a返回的不是函數b,情況就完全不同了。因為a執行完后,b沒有被返回給a的外界,只是被a所引用,而此時a也只會被b引 用,因此函數a和b互相引用但又不被外界打擾(被外界引用),函數a和b就會被GC回收。(關於Javascript的垃圾回收機制將在后面詳細介紹)

三、閉包內的微觀世界

  如果要更加深入的了解閉包以及函數a和嵌套函數b的關系,我們需要引入另外幾個概念:函數的執行環境(excution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數a從定義到執行的過程為例闡述這幾個概念。

  1. 定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)設置為定義a時a所在的“環境”,如果a是一個全局函數,則scope chain中只有window對象。
  2. 執行函數a的時候,a會進入相應的執行環境(excution context)
  3. 在創建執行環境的過程中,首先會為a添加一個scope屬性,即a的作用域,其值就為第1步中的scope chain。即a.scope=a的作用域鏈。
  4. 然后執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型而且不能通過JavaScript代碼直接訪問。創建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。
  5. 下一步是在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
  6. 最后把所有函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,因此如同第3步,函數b的作用域鏈被設置為b所被定義的環境,即a的作用域。

到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的作用域鏈包含了對函數a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數。函數b被c引用,函數b又依賴函數a,因此函數a在返回后不會被GC回收。

當函數b執行的時候亦會像以上步驟一樣。因此,執行時b的作用域鏈包含了3個對象:b的活動對象、a的活動對象和window對象,如下圖所示:

如圖所示,當在函數b中訪問一個變量的時候,搜索順序是:

  1. 先搜索自身的活動對象,如果存在則返回,如果不存在將繼續搜索函數a的活動對象,依次查找,直到找到為止。
  2. 如果函數b存在prototype原型對象,則在查找完自身的活動對象后先查找自身的原型對象,再繼續查找。這就是Javascript中的變量查找機制。
  3. 如果整個作用域鏈上都無法找到,則返回undefined。

小結,本段中提到了兩個重要的詞語:函數的定義執行。文中提到函數的作用域是在定義函數時候就已經確定,而不是在執行的時候確定(參看步驟1和3)。用一段代碼來說明這個問題:

function f(x) { 
var g = function () { return x; }
return g;
}
var h = f(1);
alert(h()); 

這段代碼中變量h指向了f中的那個匿名函數(由g返回)。

  • 假設函數h的作用域是在執行alert(h())確定的,那么此時h的作用域鏈是:h的活動對象->alert的活動對象->window對象。
  • 假設函數h的作用域是在定義時確定的,就是說h指向的那個匿名函數在定義的時候就已經確定了作用域。那么在執行的時候,h的作用域鏈為:h的活動對象->f的活動對象->window對象。

如果第一種假設成立,那輸出值就是undefined;如果第二種假設成立,輸出值則為1。

運行結果證明了第2個假設是正確的,說明函數的作用域確實是在定義這個函數的時候就已經確定了。

四、閉包的應用場景
保護函數內的變量安全。以最開始的例子為例,函數a中i只有函數b才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全性。

  1. 在內存中維持一個變量。依然如前例,由於閉包,函數a中i的一直存在於內存中,因此每次執行c(),都會給i自加1。
  2. 通過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)
    私有屬性和方法在Constructor外是無法被訪問的

    function Constructor(...) {  
      var that = this;  
      var membername = value; 
      function membername(...) {...}
    }

以上3點是閉包最基本的應用場景,很多經典案例都源於此。

三、繼承

 

JS繼承的實現方式

 

既然要實現繼承,那么首先我們得有一個父類,代碼如下:

 

// 定義一個動物類
function Animal (name) {
  // 屬性
  this.name = name || 'Animal';
  // 實例方法
  this.sleep = function(){
    console.log(this.name + '正在睡覺!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

 

1、原型鏈繼承

 

核心: 將父類的實例作為子類的原型

 

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

 

特點:

 

  1. 非常純粹的繼承關系,實例是子類的實例,也是父類的實例
  2. 父類新增原型方法/原型屬性,子類都能訪問到
  3. 簡單,易於實現

 

缺點:

 

  1. 要想為子類新增屬性和方法,必須要在new Animal()這樣的語句之后執行,不能放到構造器中
  2. 無法實現多繼承
  3. 來自原型對象的引用屬性是所有實例共享的(詳細請看附錄代碼: 示例1)
  4. 創建子類實例時,無法向父類構造函數傳參

 

2、構造繼承

 

核心:使用父類的構造函數來增強子類實例,等於是復制父類的實例屬性給子類(沒用到原型)

 

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

 

特點:

 

  1. 解決了1中,子類實例共享父類引用屬性的問題
  2. 創建子類實例時,可以向父類傳遞參數
  3. 可以實現多繼承(call多個父類對象)

 

缺點:

 

  1. 實例並不是父類的實例,只是子類的實例
  2. 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
  3. 無法實現函數復用,每個子類都有父類實例函數的副本,影響性能

 

3、實例繼承

 

核心:為父類實例添加新特性,作為子類實例返回

 

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

 

特點:

 

  1. 不限制調用方式,不管是new 子類()還是子類(),返回的對象具有相同的效果

 

缺點:

 

  1. 實例是父類的實例,不是子類的實例
  2. 不支持多繼承

 

4、拷貝繼承

 

function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  Cat.prototype.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

 

特點:

 

  1. 支持多繼承

 

缺點:

 

  1. 效率較低,內存占用高(因為要拷貝父類的屬性)
  2. 無法獲取父類不可枚舉的方法(不可枚舉方法,不能使用for in 訪問到)

 

5、組合繼承

 

核心:通過調用父類構造,繼承父類的屬性並保留傳參的優點,然后通過將父類實例作為子類原型,實現函數復用

 

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

 

特點:

 

  1. 彌補了方式2的缺陷,可以繼承實例屬性/方法,也可以繼承原型屬性/方法
  2. 既是子類的實例,也是父類的實例
  3. 不存在引用屬性共享問題
  4. 可傳參
  5. 函數可復用

 

缺點:

 

  1. 調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)

 

6、寄生組合繼承

 

核心:通過寄生方式,砍掉父類的實例屬性,這樣,在調用兩次父類的構造的時候,就不會初始化兩次實例方法/屬性,避免的組合繼承的缺點

 

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 創建一個沒有實例方法的類
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //將實例作為子類的原型
  Cat.prototype = new Super();
})();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

 

特點:

 

  1. 堪稱完美

 

缺點:

 

  1. 實現較為復雜

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM