個人覺得很好理解的JavaScript原型和繼承

前幾天看了《再談js面向對象編程》,當時就請教哈大神,發現文章有的地方可能會造成誤導(或者說和ECMA有出入),後來自己翻一翻ECMA,總算找到“標準”的理解……

本文適合初學者,特別是對構造函數、原型和原型鏈概念比較模糊的,大牛請路過,好了,讓我們一步步來看看 js 的原型(鏈)到底有多神祕……

一、函數創建過程

在瞭解原型鏈之前我們先來看看一個函數在創建過程中做了哪些事情,舉一個空函數的例子:

1
function A() {};

當我們在代碼裏面聲明這麼一個空函數,js解析的本質是(膚淺理解有待深入):

1、創建一個對象(有constructor屬性及[[Prototype]]屬性),根據ECMA,其中[[Prototype]]屬性不可見、不可枚舉

2、創建一個函數(有name、prototype屬性),再通過prototype屬性 引用 剛纔創建的對象

3、創建變量A,同時把函數的 引用 賦值給變量A

如下圖所示:

JavaScript原型和繼承

(注意圖中都是“ 引用 ”類型)

每個函數的創建都經歷上述過程。

 

二、構造函數

那麼什麼是構造函數呢?

按照ECMA的定義

Constructor is a function that creates and initializes the newly created object.

構造函數是用來新建同時初始化一個新對象的函數。

什麼樣的函數可以用來創建同時初始化新對象呢?答案是:任何一個函數,包括空函數。

所以,結論是:任何一個函數都可以是構造函數。

 

三、原型

根據前面空函數的創建圖示,我們知道每個函數在創建的時候都自動添加了prototype屬性,這就是函數的原型,從圖中可知其實質就是對一個對象的引用(這個對象暫且取名原型對象)。

我們可以對函數的原型對象進行操作,和普通的對象無異!一起來證實一下。

圍繞剛纔創建的空函數,這次給空函數增加一些代碼:

1
2
3
4
5
6
7
8
9
function A() {
 this.width = 10;
 this.data = [1,2,3];
 this.key = "this is A";
 }
 A._objectNum = 0;//定義A的屬性
 A.prototype.say = function(){//給A的原型對象添加屬性
 alert("hello world")
 }

第7~9行代碼就是給函數的原型對象增加一個say屬性並引用一個匿名函數,根據“函數創建”過程,圖解如下:

JavaScript原型和繼承

(灰色背景就是在空函數基礎上增加的屬性)

簡單說原型就是函數的一個屬性,在函數的創建過程中由js編譯器自動添加

那麼原型有什麼用呢?

先了解下new運算符,如下:

1
2
var a1 = new A;
 var a2 = new A;

這是通過構造函數來創建對象的方式,那麼創建對象爲什麼要這樣創建而不是直接var a1 = {};呢?這就涉及new的具體步驟了,這裏的new操作可以分成三步(以a1的創建爲例):

1、新建一個對象並賦值給變量a1:var a1 = {};

2、把這個對象的[[Prototype]]屬性指向函數A的原型對象:a1.[[Prototype]] = A.prototype

3、調用函數A,同時把this指向1中創建的對象a1,對對象進行初始化:A.apply(a1,arguments)

其結構圖示如下:

JavaScript原型和繼承

從圖中看到,無論是對象a1還是a2,都有一個屬性保存了對函數A的原型對象的引用,對於這些對象來說,一些公用的方法可以在函數的原型中找到,節省了內存空間。

 

四、原型鏈

瞭解了new運算符以及原型的作用之後,一起來看看什麼是[[Prototype]]?以及對象如何沿着這個引用來進行屬性的查找?

在js的世界裏,每個對象默認都有一個[[Prototype]]屬性,其保存着的地址就構成了對象的原型鏈,它是由js編譯器在對象 被創建 的時候自動添加的,其取值由new運算符的右側參數決定:當我們var object1 = {};的時候,object1[[Prototype]]就指向Object構造函數的原型對象,因爲var object1 = {};實質上等於var object = new Object();(原因可參照上述對new A的分析過程)。

對象在查找某個屬性的時候,會首先遍歷自身的屬性,如果沒有則會繼續查找[[Prototype]]引用的對象,如果再沒有則繼續查找[[Prototype]].[[Prototype]]引用的對象,依次類推,直到[[Prototype]].….[[Prototype]]undefinedObject[[Prototype]]就是undefined

如上圖所示:

1
2
3
4
5
6
7
8
9
//我們想要獲取a1.fGetName
  alert(a1.fGetName);//輸出undefined
  //1、遍歷a1對象本身
  //結果a1對象本身沒有fGetName屬性
  //2、找到a1的[[Prototype]],也就是其對應的對象A.prototype,同時進行遍歷
  //結果A.prototype也沒有這個屬性
  //3、找到A.prototype對象的[[Prototype]],指向其對應的對象Object.prototype
  //結果Object.prototype也沒有fGetName
  //4、試圖尋找Object.prototype的[[Prototype]]屬性,結果返回undefined,這就是a1.fGetName的值

簡單說就是通過對象的[[Prototype]]保存對另一個對象的引用,通過這個引用往上進行屬性的查找,這就是原型鏈。

 

五、繼承

有了原型鏈的概念,就可以進行繼承。

1
function B() {};

這個時候產生了B的原型B.prototype

原型本身就是一個Object對象,我們可以看看裏面放着哪些數據

B.prototype 實際上就是 {constructor : B , [[Prototype]] : Object.prototype}

因爲prototype本身是一個Object對象的實例,所以其原型鏈指向的是Object的原型

1
2
B.prototype = A.prototype;//相當於把B的prototype指向了A的prototype;這樣只是繼承了A的prototype方法,A中的自定義方法則不繼承
  B.prototype.thisisb = "this is constructor B";//這樣也會改變a的prototype

但是我們只想把B的原型鏈指向A,如何實現?

第一種是通過改變原型鏈引用地址

1
B.prototype.__proto__ = A.prototype;

ECMA中並沒有__proto__這個方法,這個是ff、chrome等js解釋器添加的,等同於EMCA的[[Prototype]],這不是標準方法,那麼如何運用標準方法呢?

我們知道new操作的時候,實際上只是把實例對象的原型鏈指向了構造函數的prototype地址塊,那麼我們可以這樣操作

1
B.prototype = new A();

這樣產生的結果是:

產生一個A的實例,同時賦值給B的原型,也即B.prototype 相當於對象 {width :10 , data : [1,2,3] , key : "this is A" , [[Prototype]] : A.prototype}

這樣就把A的原型通過B.prototype.[[Prototype]]這個對象屬性保存起來,構成了原型的鏈接

但是注意,這樣B產生的對象的構造函數發生了改變,因爲在B中沒有constructor屬性,只能從原型鏈找到A.prototype,讀出constructor:A

1
2
var b = new B;
  console.log(b.constructor);//output A

所以我們還要人爲設回B本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
B.prototype.constructor = B;
  //現在B的原型就變成了{width :10 , data : [1,2,3] , key : "this is A" , [[Prototype]] : A.prototype , constructor : B}
  console.log(b.constructor);//output B
  //同時B直接通過原型繼承了A的自定義屬性width和name
  console.log(b.data);//output [1,2,3]
  //這樣的壞處就是
  b.data.push(4);//直接改變了prototype的data數組(引用)
  var c = new B;
  alert(c.data);//output [1,2,3,4]
  //其實我們想要的只是原型鏈,A的自定義屬性我們想在B中進行定義(而不是在prototype)
  //該如何進行繼承?
  //既然我們不想要A中自定義的屬性,那麼可以想辦法把其過濾掉
  //可以新建一個空函數
  function F(){}
  //把空函數的原型指向構造函數A的原型
  F.prototype = A.prototype;
  //這個時候再通過new操作把B.prototype的原型鏈指向F的原型
  B.prototype = new F;
  //這個時候B的原型變成了{[[Prototype]] : F.prototype}
  //這裏F.prototype其實只是一個地址的引用
  //但是由B創建的實例其constructor指向了A,所以這裏要顯示設置一下B.prototype的constructor屬性
  B.prototype.constructor = B;
  //這個時候B的原型變成了{constructor : B , [[Prototype]] : F.prototype}
  //這樣就實現了B對A的原型繼承

圖示如下,其中紅色部分代表原型鏈:

JavaScript原型和繼承

作爲初學者淺陋的理解,本文目的在於更具象地去理解js的面向對象,疏漏之處請指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章