對Javascript 類、原型鏈、繼承的理解

一、序言

對Javascript 類、原型鏈、繼承的理解

和其他面向對象的語言(如Java)不同,Javascript語言對類的實現和繼承的實現沒有標準的定義,而是將這些交給了程序員,讓程序員更加靈活地(當然剛開始也更加頭疼)去定義類,實現繼承。(以下不討論ES6中利用class、extends關鍵字來實現類和繼承;實質上,ES6中的class、extends關鍵字是利用語法糖實現的)

Javascript靈活到甚至可以實現接口的封裝(類似Java中的Interface和implements)。

二、類的實現

1.我對類的理解

首先,我先說說我對類的理解:類是包含了一系列【屬性/方法】的集合,可以通過類的構造函數創建一個實例對象(例如人類是一個類,而每一個人就是一個實例對象),而這個實例對象中會包含兩方面內容:

a.類的所有非靜態【屬性/方法】

非靜態【屬性/方法】就是每一個實例所特有的,屬於個性。(例如每個人的名字都不相同,而名字這個屬性就是一個非靜態屬性)

b.類的所有靜態【屬性/方法】

靜態【屬性/方法】就是每一個實例所共享的,屬於共性。(例如每個人都要吃飯,而吃飯這個方法就是一個非靜態方法)

2.Javascript對類的實現

a.利用函數創建類,利用new關鍵字生成實例對象

(話不多說,先上代碼,以下沒有特別說明的話,我都會先上代碼,然後進行解釋說明)

// 代碼2.2.afunction Human() { console.log('create human here')}var fakeperson = Human() // undefinedvar person = new Human() // {}

這裡Human既是一個普通函數,也是一個類的構造函數,當調用Human()的時候,它作為一個普通函數會被執行,會輸出create human here,但是沒有返回值(即返回undefined);而當調用new Human()時,也會輸出create human here並且返回一個對象。因為我們用Human這個函數來構造對象,所以我們也把Human稱作構造函數。所以通過定義構造函數,就相當於定義了一個類,通過new關鍵字,即可生成一個實例化的對象。

b.利用構造函數實現非靜態【屬性/方法】

// 代碼2.2.bfunction Human(name) { this.name = name}var person_1 = new Human('Jack')var person_2 = new Human('Rose')console.log(person_1.name) // Jackconsole.log(person_2.name) // Rose

這裡的Human構造函數中多了一個參數並且函數體中多了一句this.name = name,這句話的中的this指針指向new關鍵字返回的實例化對象,所以根據構造函數參數的不同,其生成的對象中的具有的屬性name的值也會不同。而這裡的name就是這個類的 非靜態【屬性/方法】

c.利用prototype實現靜態【屬性/方法】

這裡因為要用到原型鏈的知識,所以放到原型鏈後面說。

三、原型鏈

1.類的prototype是什麼?

在Javascript中,每當我們定義一個構造函數,Javascript引擎就會自動為這個類中添加一個prototype(也被稱作原型)

2.對象的__proto__是什麼?

在Javascript中,每當我們使用new創建一個對象時,Javascript引擎就會自動為這個對象中添加一個__proto__屬性,並讓其指向其類的prototype

// 代碼3.2function Human(name) { this.name = name}console.log(Human.prototype)var person_test1 = new Human('Test1')var person_test2 = new Human('Test2')console.log(person_test1.__proto__)console.log(person_test2.__proto__)console.log(Human.prototype === person_test1.__proto__) // trueconsole.log(Human.prototype === person_test2.__proto__) // true

我們會發現Human.prototype是一個對象,Human類的實例化對象person_test1、person_test2下都有一個屬性__proto__也是對象,並且它們都等於Human.prototype,我們知道在Javascript中引用類型的相等意味著他們所指向的是同一個對象。所以我們可以得到結論,任何一個實例化對象的__proto__屬性都指向其類的prototype。

3.對象的__proto__有什麼作用?

// 代碼3.3var Pproto = { name:'jack'}var person = { __proto__:Pproto}console.log(person.name) // jackperson.name = 'joker'console.log(person.name) // joker

我們發現最開始我們並沒有給person定義name屬性,為什麼console出來jack呢?這就是Javascript著名的原型鏈的結果啦。話不多說,先上圖:

對Javascript 類、原型鏈、繼承的理解

當我們訪問person.name時,發生了什麼呢?

首先它會訪問person對象本身的屬性,如果本身沒有定義name屬性的話,它會去尋找它的__proto__屬性對象,在這個例子中person的__proto__屬性對應的是Pproto對象,所以person的__proto__指向了Pproto,然後我們發現Pproto對象是具有name屬性的,那麼person.name就到此為止,返回了jack,但是如果我們又給person加上了一個自身的屬性name呢?這時,再次person.name就不會再尋找__proto__了,因為person本身已經具有了name屬性,而且其值為joker,所以這裡會返回joker.

我們注意到上圖中Pproto的__proto__指向了Object,這是因為每一個通過字面量的方式創建出來的對象它們都默認是Object類的對象,所以它們的__proto__自然指向Object.prototype。

4.利用prototype實現靜態【屬性/方法】

// 代碼3.4function Human(name) { this.name = name}Human.prototype.eat = function () { console.log('I eat!')}var person_1 = new Human('Jack')var person_2 = new Human('Rose')person_1.eat() // I eat!person_2.eat() // I eat!console.log(person_1.eat === person_2.eat) // true

這裡我們在構造函數外多寫了一句:Human.prototype.eat = function() {...} 這樣以後每個通過Human實例化的對象的__proto__都會指向Human.prototype,並且根據上述原型鏈知識,我們可以知道只要構造函數中沒有定義同名的非靜態【屬性/方法】,那麼每個對象訪問say方法時,訪問的其實都是Human.prototype.say方法,這樣我們就利用prototype實現了類的靜態【屬性/方法】,所有的對象實現了共有的特性,那就是eat

四、繼承的實現

1.我對繼承的理解

假如有n(n>=2)個類,他們的一些【屬性/方法】不一樣,但是也有一些【屬性/方法】是相同的,所以我們每次定義它們的時候都要重複的去定義這些相同的【屬性/方法】,那樣豈不是很煩?所以一些牛逼的程序員想到,能不能像兒子繼承父親的基因一樣,讓這些類也像“兒子們”一樣去“繼承”他們的“父親”(而這裡的父親就是包含他們所具有的相同的【屬性/方法】)。這樣我們就可以多定義一個類,把它叫做父類,在它的裡面包含所有的這些子類所具有的相同的【屬性/方法】,然後通過繼承的方式,讓所有的子類都可以訪問這些【屬性/方法】,而不用每次都在子類的定義中去定義這些【屬性/方法】了。

2.原型鏈實現繼承(讓子類繼承了父類的靜態【屬性/方法】)

// 代碼4.1function Father() {}Father.prototype.say = function() { console.log('I am talking...')}function Son() {}var sonObj_1 = new Son()console.log(sonObj_1.say) // undefined// 原型鏈實現繼承的關鍵代碼Son.prototype = new Father()var sonObj_2 = new Son()console.log(sonObj_2.say) // function() {...}

看到這句Son.prototype = new Father()你可能有點蒙圈,沒關係,我先上個原型鏈的圖,你分分鐘就能明白了

對Javascript 類、原型鏈、繼承的理解

對著圖我們想一想,首先,一開始Son、Father兩個類沒有什麼關係,所以在訪問say的時候肯定是undefined,但是當我們使用了Son.prototype = new Father()後,我們知道通過new Son()生成的對象都會有__proto__屬性,而這個屬性指向Son.prototype,而這裡我們又讓它等於了一個Father的對象,而Father類又定義了靜態方法say,所以這裡我們的sonObj_2通過沿著原型鏈尋找,尋找到了say方法,於是就可以訪問到Father類的靜態方法say了。這樣就實現了子類繼承了父類的靜態【屬性/方法】,那麼如何讓子類繼承父類的非靜態【屬性/方法】呢?

3.構造函數實現繼承(讓子類繼承了父類的非靜態【屬性/方法】)

// 代碼4.3function Father(name) { this.name = name}function Son() { Father.apply(this, arguments) this.sing = function() { console.log(this.name + ' is singing...') }}var sonObj_1 = new Son('jack')var sonObj_2 = new Son('rose')sonObj_1.sing() // jack is singing...sonObj_2.sing() // rose is singing...

在這個例子中,通過在Son的構造函數中利用apply函數,執行了Father的構造函數,所以每一個Son對象實例化的過程中都會執行Father的構造函數,從而得到name屬性,這樣,每一個Son實例化的Son對象都會有不同的name屬性值,於是就實現了子類繼承了父類的非靜態【屬性/方法】

4.組合方式實現繼承(組合 原型鏈繼承 + 構造函數繼承)

顧名思義,就是結合上述兩種方法,然後同時實現對父類的靜態及非靜態【屬性/方法】的繼承,代碼如下:

// 代碼4.4function Father(name) { this.name = name}Father.prototype.sayName = function() { console.log('My name is ' + this.name)}function Son() { Father.apply(this, arguments)}Son.prototype = new Father('father')var sonObj_1 = new Son('jack')var sonObj_2 = new Son('rose')sonObj_1.sayName() // My name is jacksonObj_2.sayName() // My name is rose

這裡子類Son沒有一個自己的方法,它的sayName方法繼承自父類的靜態方法sayName,構造函數中繼承了父類的構造函數方法,所以得到了非靜態的name屬性,因此它的實例對象都可以調用靜態方法sayName,但是因為它們各自的name不同,所以打印出來的name的值也不同。看到這裡,大家可能認為這已經是一種完美無缺的Javascript的繼承方式了,但是還差一丟丟,因為原型鏈繼承不是一種純粹的繼承原型的方式,它有副作用,為什麼呢?因為在我們調用Son.prototype = new Father()的時候,不僅僅使Son的原型指向了一個Father的實例對象,而且還讓Father的構造函數執行了一遍,這樣就會執行this.name = name;所以這個Father對象就不純粹了,它具有了name屬性,並且值為father,那為什麼之後我們訪問的時候訪問不到這個值呢?這又是因為原型鏈的原因啦,話不多說先上圖:

對Javascript 類、原型鏈、繼承的理解

對Javascript 類、原型鏈、繼承的理解

所以這裡父類的構造函數在進行原型鏈繼承的時候也執行了一次,並且在原型鏈上生成了一個我們永遠也不需要訪問的name屬性,而這肯定是佔內存的(想象一下name不是一個字符串,而是一個對象),那麼我們怎麼能讓原型鏈繼承更純粹一點呢?讓它只繼承原型(靜態【屬性/方法】)呢?

5.寄生組合方式實現繼承

為了讓原型鏈繼承的更純粹,這裡我們引入一個Super函數,讓Father的原型寄生在Super的原型上,然後讓Son去繼承Super,最後我們把這個過程放到一個閉包內,這樣Super就不會汙染全局變量啦,話不多說上代碼:

// 代碼4.4function Father(name) { this.name = name}Father.prototype.sayName = function() { console.log('My name is ' + this.name)}function Son() { Father.apply(this, arguments)}(function () { function Super(){} Super.prototype = Father.prototype Son.prototype = new Super()}())var sonObj_1 = new Son('jack')

這個時候再去打印sonObj1就會發現,它的原型中已經沒有name屬性啦,如下所示:

對Javascript 類、原型鏈、繼承的理解


分享到:


相關文章: