和其餘面向對象的語言(如Java)不一樣,Javascript語言對類的實現和繼承的實現沒有標準的定義,而是將這些交給了程序員,讓程序員更加靈活地(固然剛開始也更加頭疼)去定義類,實現繼承。(如下不討論ES6中利用class、extends關鍵字來實現類和繼承;實質上,ES6中的class、extends關鍵字是利用語法糖實現的)php
首先,我先說說我對類的理解:類是包含了一系列【屬性/方法】的集合,能夠經過類的構造函數建立一個實例對象(例如人類是一個類,而每個人就是一個實例對象),而這個實例對象中會包含兩方面內容:前端
a. 類的屬性程序員
屬性就是每個實例所特有的,屬於個性。(例如每一個人的名字都不相同)web
b. 類的方法瀏覽器
方法就是每個實例所共享的,屬於共性。(例如每一個人都要吃飯)閉包
a.利用函數建立類,利用new關鍵字生成實例對象(話很少說,先上代碼,如下沒有特別說明的話,我都會先上代碼,而後進行解釋說明)app
function Human() {異步
console.log('create human here')函數
}var fakeperson = Human() // undefinedvar person = new Human() // {}ui
這裏Human既是一個普通函數,也是一個類的構造函數,當調用Human()的時候,它做爲一個普通函數會被執行,會輸出create human here,可是沒有返回值(即返回undefined);而當調用new Human()時,也會輸出create human here而且返回一個對象。由於咱們用Human這個函數來構造對象,因此咱們也把Human稱做構造函數。因此經過定義構造函數,就至關於定義了一個類,經過new關鍵字,便可生成一個實例化的對象。
b.利用構造函數實現類的屬性
function 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就是這個類的屬性
在Javascript中,每當咱們定義一個構造函數,Javascript引擎就會自動爲這個類中添加一個prototype(也被稱做原型)
在Javascript中,每當咱們使用new建立一個對象時,Javascript引擎就會自動爲這個對象中添加一個__proto__屬性,並讓其指向其類的prototype
function 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。
var Pproto = {
name:'jack'
}var person = {
__proto__:Pproto
}console.log(person.name) // jackperson.name = 'joker'console.log(person.name) // joker
咱們發現最開始咱們並無給person定義name屬性,爲何console出來jack呢?這就是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。
function 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
假若有n(n>=2)個類,他們的一些【屬性/方法】不同,可是也有一些【屬性/方法】是相同的,因此咱們每次定義它們的時候都要重複的去定義這些相同的【屬性/方法】,那樣豈不是很煩?因此一些牛逼的程序員想到,能不能像兒子繼承父親的基因同樣,讓這些類也像「兒子們」同樣去「繼承」他們的「父親」(而這裏的父親就是包含他們所具備的相同的【屬性/方法】)。這樣咱們就能夠多定義一個類,把它叫作父類,在它的裏面包含全部的這些子類所具備的相同的【屬性/方法】,而後經過繼承的方式,讓全部的子類均可以訪問這些【屬性/方法】,而不用每次都在子類的定義中去定義這些【屬性/方法】了。
function 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()你可能有點蒙圈
看下圖
對着圖咱們想想,首先,一開始Son、Father兩個類沒有什麼關係,因此在訪問say的時候確定是undefined,可是當咱們使用了Son.prototype = new Father()後,咱們知道經過new Son()生成的對象都會有__proto__屬性,而這個屬性指向Son.prototype,而這裏咱們又讓它等於了一個Father的對象,而Father類又定義了靜態方法say,因此這裏咱們的sonObj_2經過沿着原型鏈尋找,尋找到了say方法,因而就能夠訪問到Father類的靜態方法say了。這樣就實現了子類繼承了父類的方法,那麼如何讓子類繼承父類的屬性呢?
function 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屬性值,因而就實現了子類繼承了父類的屬性
顧名思義,就是結合上述兩種方法,而後同時實現對父類的【屬性/方法】的繼承,代碼以下:
function 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,那爲何以後咱們訪問的時候訪問不到這個值呢?
這裏父類的構造函數在進行原型鏈繼承的時候也執行了一次,而且在原型鏈上生成了一個咱們永遠也不須要訪問的name屬性,而這確定是佔內存的(想象一下name不是一個字符串,而是一個對象)
爲了讓原型鏈繼承的更純粹,這裏咱們引入一個Super函數,讓Father的原型寄生在Super的原型上,而後讓Son去繼承Super,最後咱們把這個過程放到一個閉包內,這樣Super就不會污染全局變量啦,話很少說上代碼:
function 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單線程的背後有瀏覽器的其餘線程爲其完成異步服務,這些異步任務爲了和主線程通訊,經過將回調函數推入到任務隊列等待執行。主線程所作的就是執行完同步任務後,經過事件循環,不斷地檢查並執行任務隊列中回調函數。
(php開發,web前端,ui設計,vr開發專業培訓機構技術分享,部分摘自Github開源社區!!!)