面向物件有一個特征是繼承,即重用某個已有類的代碼,在其基礎上建立新的類,而無需重新撰寫對應的屬性和方法,繼承之后拿來即用;
在其他的面向物件編程語言比如Java中,通常是指,子類繼承父類的屬性和方法;
我們現在來看看,JS是如何實作繼承這一個特征的;
要說明這個,我們首先要看看,每個物件都有的一個隱藏屬性[[Prototype]];
物件的隱藏屬性[[Prototype]]
在JS中,每個物件obj,都有這樣一個隱藏屬性[[Prototype]],它的值要么是null,要么是對另一個物件anotherObj的參考(不可以賦值為其他型別值),這另一個物件anotherObj,就叫做物件obj的原型;
通常說一個物件的原型,就是在說這個隱藏屬性[[Prototype]],也是在說它參考的那個物件,畢竟二者一致;
現在來創建一個非常簡單的字面量物件,來查看一下這個屬性:

可以看到,物件obj沒有自己的屬性和方法,但是它還有一個隱藏屬性[[Prototype]],資料型別是Object,說明它指向了一個物件(即原型),這個原型物件里面,有很多方法和一個屬性;
其他的暫且不論,我們先重點看一下,紅框的constructor()方法和__proto__屬性;
訪問器屬性(__proto__)
訪問[[Prototype]]
從紅框可以看到,屬性__proto__是一個訪問器屬性,有getter/setter特性(這個屬性名前后各兩個下劃線);
問題是,它是用來訪問哪個屬性的?
我們來呼叫一下看看:

可以看到,__proto__訪問器屬性,訪問的正是隱藏屬性[[Prototype]],或者說,它指向的正是原型物件;
值得一提的是,這是一個老式的訪問原型物件的方法,現代編程語言建議使用Object.getPrototypeOf/setPrototypeOf來訪問原型物件;
但是考慮兼容性,使用__proto__也是可以的;
請注意,__proto__不能代表[[Prototype]]本身,它只是其一個訪問器屬性;
設定[[Prototype]]
正因為它是訪問器屬性,也即具有getter和setter功能,我們現在可以控制物件的原型物件的指向了(并不建議這樣做):

如上圖,現在將其賦值為null,好了,現在obj物件沒有原型了;

如上圖,創建了兩個物件,并且讓obj1沒有了原型,讓obj2的原型是obj1;
看看,此時obj2.name讀取到obj1的屬性name了,首先obj2在自身屬性里找name沒有找到,于是去原型上去找,于是找到了obj1的name屬性了,換句話說,obj2繼承了obj1的屬性了;
這就是JS實作繼承的方式,通過原型這種機制;
讓我們看看下面的代碼:

正常的obj2.name = 'Jerry'的添加屬性的陳述句,會成為obj2物件自己的屬性,而不會去覆寫原型的同名屬性,這是再正常不過了,繼承得來的東西,只能讀取,不能修改(訪問器屬性__proto__除外);
現在的問題是,為什么obj2.__proto__是undefined?上面不是剛剛賦值為obj1了嗎?
原因就在于__proto__是訪問器屬性,我們讀取它實際上是在呼叫對應的getter/setter方法,而現在obj2的原型(即obj1)并沒有對應的getter/setter方法,自然是undefined了;
現在綜合一下,看下面代碼:

為什么最后obj2.__proto__輸出的是hello world,為什么__proto__成了obj2自己的屬性了?
關鍵就在于紅框的三句代碼:
第一句let obj2 = {},此時obj2有原型,有訪問器屬性__proto__,一切正常;
第二句obj2.__proto__ = obj1,這句呼叫__proto__的setter方法,將[[Prototype]]的參考指向了obj1;
這一句完成以后,obj2因為obj1這個原型而沒有訪問器屬性__proto__了;
所以第三句obj2.__proto__ = 'hello world'的__proto__已經不再是訪問器屬性了,而是一個普通的屬性名了,所以這句就是一個普通的添加屬性的陳述句了;
構造器(constructor)
在隱藏屬性[[Prottotype]]那里,看到其有一個constructor()方法,顧名思義,這就是構造器了;
類物件與函式物件
- 類物件
在其他編程語言比如Java中,構造方法通常是和類名同名的函式,里面定義了物件的一些初始化代碼;
當需要一個物件時,就通過new關鍵字去呼叫構造方法創建一個物件;
那在JS中,當我們let obj = {}去創建一個字面量物件的時候,發生了什么?
上面這句代碼,其實就是let obj = new Object()的簡寫,也是通過new關鍵字去呼叫一個和類名同名的構造方法去創建一個物件,在這里就是構造方法Object();
這種通過new className()呼叫構造方法創造的物件,稱為類物件;
- 函式物件
但是,再等一下,JS早期是沒有類的概念的,那個時候大家又是怎么去創建物件的呢?
想一下,創建物件是不是需要一個構造方法(即一個函式),本質上是不是new Function()的形式去創建物件?
對咯,早期就是new Function()去創建物件的,這個Function就叫做建構式;
這種通過new Function()呼叫建構式創造的物件,稱為函式物件;
建構式和普通函式又有什么區別呢?除了要求是用function關鍵字宣告的函式,并且命名建議大駝峰以外,幾乎是沒有區別的:

看,我們宣告了一個建構式Cat(),并通過new Cat()創造了一個物件tom;
列印tom發現,它有一個原型,這個原型和字面量物件的原型不一樣,它有一個方法一個屬性;
方法是constructor()構造器,指向的正是Cat()函式;
屬性是另一個隱藏屬性[[Prototype]],暫時不去探究它是誰;
也就是說,函式物件的原型,是由另一個原型和constructor()方法組成的物件;
我們可以用代碼來驗證一下,類物件和函式物件的原型的異同點:

如上所示,創建了一個函式物件tom和一個類物件obj;
可以看出:
函式物件的原型的方法constructor()指向建構式本身;
函式物件的原型的隱藏屬性[[Prototype]]和字面量物件(Object物件)的隱藏屬性,他們兩的參考相同,指向的是同一個物件,暫時不去探究這個物件是什么,就認為它是字面量物件的原型即可;
還可以看到,無論是類物件,還是函式物件,其原型都有constructor()構造器;
這個構造器在創建物件的程序中,具體起了什么樣的作用呢?
讓我們先看看函式物件tom的這個原型是怎么來的?我們之前一直都是在說物件有一個隱藏屬性[[Prototype]]指向原型物件,究竟是哪一步,讓這個隱藏屬性指向了原型物件呢?
函式的普通屬性prototype
事實上,每個函式都有一個屬性prototype,默認情況下,這個屬性prototype是一個物件,其中只含有一個方法constructor,而這個constructor指向函式本身(還有一個隱藏屬性[[Prototype]],指向字面量物件的原型);
可以用代碼佐證,如下所示:

注意,prototype要么是一個物件型別,要么是null,不可以是其他型別,這聽起來很像隱藏屬性[[Prototype]],不過prototype只是函式的一個普通屬性,物件是沒有這個屬性的;
來看下這個屬性的特性吧:

可以看到,它不是一個訪問器屬性,只是一個普通屬性,但是它不可配置不可列舉,只能修改值;
它的value值,眼熟嗎?正是建構式創建的函式物件的原型啊;
它居然還有一個特性[[Prototype]],不要把它和value值里面的屬性[[Prototype]]弄混,前者是prototype屬性的特性,后者是prototype屬性的一個隱藏屬性,雖然此刻他們都指向字面量物件的原型,但是前者始終指向字面量物件的原型,后者則始終指向原型(而原型是會變的);
這里也不再去追究為什么它會有這樣一個特性了,讓我們把重點放在prototype屬性本身;
new Function()的時候發生了什么
事實上,只有在呼叫new Function()作為建構式的時候,才會使用到這個prototype屬性;

我們來仔細分析一下上面代碼具體發生了什么:
let tom = new Cat()這句代碼的執行流程如下:
- 先呼叫
Cat.prototype屬性的特性[[Prototype]](我們知道它指向字面量物件的原型)里面的constructor()構造器,創建一個字面量空物件,當然此時這個物件的隱藏屬性[[Prototype]]也都已經存在了,將這個物件分配給this指標; - 然后回傳
this指標給tom,即tom參考了這個字面量空物件,同時this指向了tom; - 然后執行建構式
Cat()本身的陳述句,即this.name = "Tom",于是tom就有了一個屬性name; - 然后將
Cat.prototype屬性值value,復制(注意,這里是復制,不是賦值,這意味著這里不是傳參考,而是傳值)給tom的隱藏屬性[[Prototype]],即tom.__proto__ = Cat.prototype;
如果我們用代碼去描述上面整個程序,就類似于下面這樣:
// let tom = new Cat()的整個具體流程,類似于下面這樣
let tom = {}; //創建字面量物件,并賦值給變數tom
tom.name = "Tom"; // 執行Cat()函式
tom.__proto__ = Cat.prototype; // 將Cat的prototype的屬性值賦值給tom的隱藏屬性[[Prototype]]
現在已經說清楚了new Function()發生的具體程序,上面代碼的輸出結果也佐證了我們所說的:
函式物件tom的原型正是Cat函式的屬性prototype的值value,可以看到他們的constructor()構造器都指向Cat函式本身,并且tom.name的值Tom;
然后我們修改了Cat函式的prototype的值value,Cat.prototype = Dog.prototype陳述句將其設定成了Dog函式的prototype的值value;
讓我們順著剛剛說的流程,看看let newTom = new Cat()的執行程序:
- 先創建字面量空物件;
- 然后賦值給
newTom; - 然后呼叫
Cat()函式本身,即newTom.name = "Tom"; - 然后執行陳述句
newTom.__proto__ = Cat.prototype,而Cat.prototype = Dog.prototype,所以newTom.__proto__ = Dog.prototype;
輸出結果佐證了我們的執行程序,函式newTom的原型正是Dog函式的屬性prototype的值value,他們的constructor()構造器都指向了Dog函式本身,但是newTom.name的值依然是"Tom";
從上面前后兩個輸出結果也可以看出來,最后一步的tom.__proto__ = Cat.prototype確實是復制而不是賦值,否則在Cat.prototype = Dog.prototype陳述句之后,tom.__proto__ = Cat.prototype = Dog.prototype了,但是輸出結果表面并沒有改變;
現在我們已經明白了函式物件的原型為什么是這個樣子的,也明白了函式物件的constructor()構造器指向了建構式本身;
現在讓我們像下面這樣,使用一下函式物件的constructor()構造器吧:

看上面的代碼,我們現在已經知道let tom = new Cat()的時候都發生了什么,也知道此時tom的原型的constructor()構造器指向的是Dog函式;
所以let spike = new tom.constructor()這句代碼,當tom去自己的屬性里沒有找到constructor()方法的時候,就去原型里面去找,于是找到了指向Dog函式的constructor()構造器,所以這句代碼就等于let spike = new Dog();
通過這段代碼,好好體會一下函式物件的構造器吧,
建構式和普通函式的區別
其實從技術上來講,建構式和普通函式沒有區別;
只是默認建構式采用大駝峰命名法,并通過new運算子去創建一個函式物件;
-
new.target
我們怎樣去判斷一個函式的呼叫是普通呼叫,還是
new運算子呼叫的呢?
如上所示,通過
new.target,可以判斷該函式是被普通呼叫的還是通過new關鍵字呼叫的; -
建構式的回傳值
建構式從技術上說,就是一個普通函式,所以當然也可能有
return回傳值(通常建構式于情于理都是不會有return陳述句的);
之前說過
new Function()的時候的具體流程,我們來看一下:-
先創建一個字面量空物件;
-
將空物件賦值給
tom; -
執行
Cat()函式,讓tom有了屬性name;但是
Cat()函式有return陳述句,回傳了一個空物件{},由tom接收了,也就是說tom被覆寫賦值了; -
所以最后
tom指向的是return陳述句的空物件,而不是最開始創建的空物件;
-
字面量物件的原型
new Object()的時候發生了什么
我們剛剛說了new Function()創建函式物件的時候,具體發生了什么,現在來看看創建類物件的時候,具體發生了什么;
以Object為例,因為它是一個類,是JS其他所有類的祖先,這一點與Java類似;
我們先看一下Object的prototype屬性吧,是的,類和函式一樣,也有這個屬性(注意,是類有這個屬性,而不是類的實體即物件有這個屬性);

看上圖,是不是很眼熟,這不就是字面量物件的原型嗎?

是的,如上圖所示,就是它;
還記得原型鏈吧,那么這個原型物件還有原型嗎?

如上所示,沒有了,指向null了,看樣子我們已經走到了原型鏈的原點了,為了方便,我們就稱呼Object.prototype為原始原型吧;
看看它的特性吧:

和函式的prototype屬性的特性,如出一轍,但是注意,它的writable屬性是false了,這意味著我們再也無法對這個屬性做任何操作了;
這是當然,它可是所有類的祖先,怎么能隨意更改呢;
這下我們就能明白new ClassName()的時候大概流程是什么樣子了;
以let obj = {}為例(其實就是let obj = new Object()):
- 先呼叫
Objecet.prototype屬性的特性[[Prototype]]里面的constructor()構造器(不再繼續深究這個構造器了),創建一個字面量空物件,當然此時這個物件的隱藏屬性[[Prototype]]也都已經存在了; - 然后將這個物件賦值給
obj,即obj參考了這物件,同時this指標也就指向了obj; - 然后執行構造方法
Object()本身的陳述句,就不再進一步去研究這個構造方法了,總之此時obj已經是一個有著很多內置方法的字面量物件了; - 然后將
Object.prototype屬性值value,復制給obj的隱藏屬性[[Prototype]],即obj.__proto__ = Object.prototype;
注意,其實流程不完全是上面這樣子,與建構式的流程還有一點點區別,主要是第三步,還有一個構造器的執行,這和類的繼承有關系,詳細的在后面new className()的時候發生了什么里面具體說明;
更改原始原型
我們剛剛說了,Object.prototype屬性的所有特性都是false,意味著我們對這個屬性無法再做任何操作了;
這只是再說,我們不能對其本身做任何刪改的操作了,但是它本身依然是一個物件,這意味著我們可以正常的向其添加屬性和方法;

如上圖所示,我們向Object.prototype屬性物件里添加了hello()方法,并且由obj物件通過原型呼叫了這個方法;
類物件的原型
我們已經了解了函式物件的原型,和原始原型,再來看看類物件的原型;
我們把這三種放一起做個比較吧:

我們自定義了類classA,自定義了函式functionA,并創建了類物件clsA和函式物件funcA,以及字面量物件;
可以看出,類物件與函式物件的原型的形式,是一致的,只是各自原型里的constructor()指向各自的類/函式,即紅框部分不同;
而他們的原型的原型則是一致的,和字面量物件的原型一樣,都指向了原始原型,即綠框部分相同;
上面的輸出結果佐證了這一點;
從這也可以看出來,其他類都是繼承自原始類Object的,只是原型鏈的長短罷了,最終都可以溯源到原始類Object;
很顯然,類與建構式,很類似;
類與建構式的區別
盡管類物件和函式物件有相似的原型,但是不代表類與建構式就完全一樣了,他們之間的區別還是很大的:
-
型別不同,定義形式不同

類名后不需要括號,建構式名后需要加括號;
類的方法宣告形式和建構式的方法不一樣;
列印類和建構式,類前的型別是
class,建構式前的型別是f,即function;注意,不能使用
typeof運算子,它會認為類和建構式都是function -
prototype不一樣

如上所示,類的方法,會成為
prototype的方法,但是建構式的方法不會成為prototype的方法;也即建構式的
prototype始終由constructor()和原始原型組成,函式物件無法通過原型去呼叫在建構式里定義的方法;函式物件如果想要呼叫
method1()方法,就不能寫成let method1 = function(){},而是this.method1 = function(){},將其變為函式物件自己的方法; -
prototype的特性不一樣

類的
prototype是不可寫的,但是建構式的prototype是可寫的; -
方法的特性不一樣

由于函式物件不能通過原型繼承方法,這里只展示類的方法的特性,如上所示,類的方法,是不可列舉的,也即不會被
for-in語法遍歷到; -
模式不同
由于類是后來才有的概念,所以類總是使用嚴格模式,即不需要顯示使用
use strict,類總是在嚴格模式下執行;而建構式則不同,默認是普通模式,需要顯示使用
use strict才會在嚴格模式下執行; -
[[IsClassConstructor]]
類有隱藏屬性
[[IsClassConstructor]],其值為true;這要求必須使用
new關鍵字去呼叫它,像普通函式一樣呼叫會出錯:
但是很顯然,建構式本身就是一個函式,是可以像普通函式一樣去呼叫的;
-
構造器
constructor由于函式物件不能通過原型繼承方法,所以無法自定義構造器;
但是類物件可以繼承啊,所以可以自定義構造器并在
new的時候呼叫;
從圖上可以看出,我們是無法去自定義建構式的構造器的,它依然還是按照我們所說的流程去創建函式物件的;
我們現在看看,類自定義構造器,是怎么按照我們的流程去創建類物件的:
-
先呼叫
classA.prototype的特性[[Prototype]]里的構造器去創建一個字面量空物件; -
將空物件賦值給變數
clsA; -
然后執行構造方法
classA()本身的陳述句;首先添加了屬性
outterName;然后又遇到了
constructor()方法(注意該構造器與classA.prototype.constructor不是同一個東西),于是又執行了這個構造器的陳述句,添加了屬性innerName;
由此我們可以得出,類在創建類物件的時候,流程依然是我們所述的流程;
但是在遇到類里面的同名方法
constructor()時候,不會將其作為原型方法,而是會立即運行該構造器;另外,像
outterName這樣的屬性,不會成為prototype的屬性,也就是說,類只有定義的方法(除了constructor構造器)會進入prototype的屬性,成為原型被繼承; -
new className()的時候發生了什么
上面剛剛描述了類自定義構造器之后,創建物件是一個什么樣的流程;
現在來仔細理解一下類的構造器,事實上,如果我們不顯式自定義構造器,類也會默認提供一個下面這樣的構造器:
constructor() {
super();
}
這里的super()實際上就是在呼叫其父類的構造方法(注意不是指父類的構造器constructor(),而是指父類自身);
用代碼來驗證一下吧:

我們先來看一下let c = new classC()的時候,具體流程是什么樣的吧:
- 首先呼叫
classC.prototype屬性的特性[[Prototype]](它總是指向原始原型),創建一個字面量空物件; - 然后將其賦值給變數
c; - 然后執行構造方法
classC()的陳述句,通常會有添加物件的屬性和方法的陳述句,這里沒有; - 接著查看是否顯式宣告了
constructor()構造器(如果沒有就提供一個默認的構造器),這里有,于是立即執行這個構造器;- 首先是
super(),實際上就是執行建構式classA()的陳述句,于是添加了屬性nameA; - 然后是
this.nameB = 'C',于是添加了屬性nameC;
- 首先是
- 最后,將
classC.prototype的value值,復制給c的隱藏屬性[[Prototype]],即c.__proto__ = classC.prototype;
整個完整流程如上所示;
現在來試著對著流程看看let b = new classB()吧:
- 首先創建字面量空物件;
- 賦值給變數
b; - 執行
classB()的陳述句,添加了屬性nameB; - 沒有構造器,提供默認的構造器,執行
super()即執行classA()的陳述句,于是添加了屬性nameA; - 最后,復制
b的原型為classB.prototype的value值;
輸出結果也驗證了我們所說的;
操作原型的現代方法
之前已經說過,通過__proto__屬性去操作原型的方法,是歷史的過時的方法,實際上并不推薦;
現代JS有以下方法,供我們去操作原型:
-
Object.getPrototypeOf(obj)
此方法,回傳物件
obj的隱藏屬性[[Prototype]]; -
Object.setPrototypeOf(obj, proto)
此方法,將物件
obj的隱藏屬性[[Prototype]]指向新的物件proto; -
Object.create(proto, descriptors)
此方法,創建一個空物件,并將其隱藏屬性
[[Prototype]]指向proto;同時,可選引數
descriptors可以給空物件添加屬性,如下所示:
原型鏈與繼承
現在應該已經理解了原型是一個什么樣的概念,以及如何去訪問原型;
正如繼承有兒子繼承父親,父親繼承爺爺一樣,有這樣一個往上溯源的關系,原型也可以這樣往上溯源,這就是原型鏈的概念;
用代碼去理解一下吧:

我們定義了三個物件A/B/C,并且設定C的原型是B,B的原型是A;
讀取C.nameA的時候,首先在C自己的屬性里去找,沒有找到;
于是去原型B的屬性里去找,沒有找到;
再去B的原型A的屬性里去找,找到并輸出;
可以看C展開的一層層結構,可以很清晰的看到原型鏈的存在;
由此也可以看出,JS是單繼承的,同Java一致;
但是正常的繼承,肯定不是這樣手動去設定物件的原型的,而是自動去設定的;
在JS中,繼承的關鍵字也是extends,也是描述類的父子關系的;

上面代碼,classC繼承classB,而classB繼承classA;
所以classC的物件,繼承了他們的屬性,便有了三個屬性nameA/nameB/nameC,這也說明,屬性是不放在原型里的,而是會在創建物件的時候,直接成為classC的屬性;
classC的原型,有一個屬性一個方法,方法是constructor()構造器指向自己,屬性是另一個原型;
注意,列印出來的原型后面標注的classX,原型指的是物件,不是類,所以classC的原型不是指classB這個類本身,而是指其來源于classB;
紫色框:物件c的原型,即c.__proto__ == classC.prototype;
橘色框:classB.prototype,即物件c的原型的原型c.__proto__.__proto__ == classB.prototype;
綠色框:classA.prototype,即物件c的原型的原型的原型c.__proto__.__proto__.__proto__ == classA.prototype;
紅色框:Object.prototype,也即原始原型c.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
這是一條完整的原型鏈,從中也能看出繼承是什么樣的一個形式;
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/540646.html
標籤:其他
