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

1、原型、原型鏈javascript

原型對象
   在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
java

2、閉包
安全

1、什麼是閉包? 閉包

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

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

這段代碼有兩個特色:性能

一、函數b嵌套在函數a內部;this

二、函數a返回函數b。spa

引用關係如圖:prototype

  這樣在執行完var c=a()後,變量c其實是指向了函數b,再執行c()後就會彈出一個窗口顯示i的值(第一次爲1)。這段代碼其實就建立了一個閉包,爲何?由於函數a外的變量c引用了函數a內的函數b,就是說:code

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

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

2、閉包有什麼做用?

  簡而言之,閉包的做用就是在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的垃圾回收機制將在後面詳細介紹)

3、閉包內的微觀世界

  若是要更加深刻的瞭解閉包以及函數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個假設是正確的,說明函數的做用域確實是在定義這個函數的時候就已經肯定了。

4、閉包的應用場景
保護函數內的變量安全。以最開始的例子爲例,函數a中i只有函數b才能訪問,而沒法經過其餘途徑訪問到,所以保護了i的安全性。

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

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

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

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);
};

 

一、原型鏈繼承

 

核心: 將父類的實例做爲子類的原型

 

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. 建立子類實例時,沒法向父類構造函數傳參

 

二、構造繼承

 

核心:使用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類(沒用到原型)

 

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. 沒法實現函數複用,每一個子類都有父類實例函數的副本,影響性能

 

三、實例繼承

 

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

 

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. 不支持多繼承

 

四、拷貝繼承

 

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 訪問到)

 

五、組合繼承

 

核心:經過調用父類構造,繼承父類的屬性並保留傳參的優勢,而後經過將父類實例做爲子類原型,實現函數複用

 

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. 調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)

 

六、寄生組合繼承

 

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

 

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. 實現較爲複雜
相關文章
相關標籤/搜索