天天看點

js原型、繼承

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

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

一、函數建立過程

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

function

A() {};

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

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

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

3、建立變量A,同時把函數的 引用 指派給變量A

如下圖所示:

js原型、繼承

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

每個函數的建立都經曆上述過程。

二、構造函數

那麼什麼是構造函數呢?

按照ECMA的定義

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

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

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

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

三、原型

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

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

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

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屬性并引用一個匿名函數,根據“函數建立”過程,圖解如下:

js原型、繼承

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

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

那麼原型有什麼用呢?

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

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)

其結構圖示如下:

js原型、繼承

從圖中看到,無論是對象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]]

undefined

Object

[[Prototype]]

就是

undefined

如上圖所示:

//我們想要擷取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]]儲存對另一個對象的引用,通過這個引用往上進行屬性的查找,這就是原型鍊。

五、繼承

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

function

B() {};

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

原型本身就是一個Object對象,我們可以看看裡面放着哪些資料

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

因為prototype本身是一個Object對象的執行個體,是以其原型鍊指向的是Object的原型

B.prototype = A.prototype;

//相當于把B的prototype指向了A的prototype;這樣隻是繼承了A的prototype方法,A中的自定義方法則不繼承

B.prototype.thisisb = 

"this is constructor B"

;

//這樣也會改變a的prototype

但是我們隻想把B的原型鍊指向A,如何實作?

第一種是通過改變原型鍊引用位址

B.prototype.__proto__ = A.prototype;

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

我們知道new操作的時候,實際上隻是把執行個體對象的原型鍊指向了構造函數的prototype位址塊,那麼我們可以這樣操作

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

var

b = 

new

B;

console.log(b.constructor);

//output A

是以我們還要人為設回B本身

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的原型繼承

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

js原型、繼承

繼續閱讀