前端入門13-JavaScript進階之原型

聲明

本系列文章內容所有梳理自如下幾個來源:javascript

做爲一個前端小白,入門跟着這幾個來源學習,感謝做者的分享,在其基礎上,經過本身的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,若有發現,歡迎指點下。前端

PS:梳理的內容以《JavaScript權威指南》這本書中的內容爲主,所以接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來說講。java

正文-原型

JavaScript 中並無 Java 裏的類,但它有構造函數,也有繼承,只是它是動態的基於原型的繼承。因此,原型有點相似於 Java 中父類的概念。git

可是,JavaScript 中的關於實例、繼承等這些跟 Java 仍是有很大的區別。github

先來講說在 Java 裏面:web

類是靜態的,類是可繼承的,是對象的抽象模型的表現,每一個具體的對象都是從類上實例化出來的,一個類中定義了這類對象的屬性和行爲,一旦定義完了運行時就沒法改變了。編程

但對於 JavaScript 來講,它並無類的存在,在 JavaScript 裏,除了原始類型外,其他皆是對象。數組

它是動態的基於原型的繼承機制,原型本質上也是對象,也就是說對象是繼承自對象而來的。瀏覽器

而對象這個概念是實例化後的每個具體個體表明,它是運行期動態生成的,再加上 JavaScript 裏對象的特性,如可動態添加屬性,這就讓 JavaScript 裏的繼承機制很是強大,由於這樣一來,它是可動態繼承的,原型對象上發生的變化可以同步讓繼承它的子對象都跟隨着變化。函數

原型概念

函數和構造函數的區別就在於,全部的函數,當和 new 關鍵字一塊兒使用時,此時稱它爲構造函數。相似的關係,全部的對象,當它被設置爲某個構造函數的 prototype 屬性值時,此時稱它爲原型。

也就是說,任何對象均可以當作其餘對象的原型。

在 Java 中,對象通常經過 super 關鍵字指向它的父類,而在 JavaScript 中,對象可經過 __proto__ 來指向它的原型對象,或者經過構造函數的 prototype 指向對象的原型。

prototype & __proto__

這兩個雖然指向的是同一個原型對象,但它們的宿主卻不同,須要區分一下,prototype 是構造函數的屬性,__proto__ 是經過構造函數建立出來的對象的屬性。

__proto__ 屬性並不在 ES5 標準規範中,但基本大部分瀏覽器都爲引用類型實現了這麼一個屬性,用於查看當前對象所繼承的原型,它的值等於該對象的構造函數的 prototype 屬性值。

prototype 是每一個函數對象的一個屬性,其餘對象並無這個屬性,由於基本全部的對象其實都是經過構造函數建立出來的,因此也只有函數才能來實現繼承的機制。這個屬性值表示着從這個構造函數建立的對象的原型是什麼。

對象一節學習過,建立一個對象的三種方式:

//對象直接量:
var a = {};//實際上是 var a = new Object(); 的語法糖
var a = [];//實際上是 var a = new Array(); 的語法糖

//構造函數
var a = new Array();

//Object.crate()
var a = Object.crate(null);

因此,對象直接量的方式本質上也是經過構造函數的方式建立對象。

這也是爲何會在對象一節中說,全部經過直接量方式建立的對象都繼承自 Object.prototype 的理由。

而經過 Object.create() 方式建立的對象,其原型就是參數指定的對象,可手動傳入 null,表示建立的對象沒有原型。

因此,在 JavaScript 中,絕大部分的對象都有原型,即便不手動指定,也會有默認的內置原型對象。之因此說絕大部分,是由於原型鏈頂部的 Object.prototype 對象的原型是 null,或者經過 Object.create() 建立對象時手動指定 null。

默認的繼承結構

若是不手動指定繼承關係,默認的幾種引用類型的繼承關係(原型鏈)以下:

  • 聲明的每一個函數 -> Function.prototype –> Object.prototype -> null
  • 數組對象 -> Array.prototype -> Object.prototype -> null
  • 對象直接量建立的對象 -> Object.prototype -> null
  • 自定義構造函數建立的對象 -> {} -> Object.prototype -> null

全部對象繼承的頂層原型是 Object.prototype。

這也是爲何函數對象、數組對象、普通對象均可以使用一些內置的方法,由於建立這些對象的時候,默認就會有一些繼承關係,跟 Java 中全部的類都繼承自 Object 的機制相似。

構造函數和原型的關係

構造函數自己是一個函數對象,它的屬性 prototype 指向的是另外一個對象,因此這兩個概念自己就是兩個不一樣的東西。

經過一個構造函數建立一個新的對象,不能說,這個對象繼承自構造函數,而是應該說,這對象繼承自構造函數的屬性 prototype 指向的對象。

因此,能夠通俗的理解,構造函數只是做爲第三方相似於工具的角色,用來建立一個新對象,而後讓這個新對象繼承自 prototype 屬性指向的對象。

不過構造函數和原型之間是相互引用的關聯關係,構造函數有個屬性 prototype 指向原型,而原型也有一個屬性 constructor 指向構造函數。

因此,全部從這個構造函數建立的新對象,都繼承了原型的屬性,那麼這些新對象也就能夠經過繼承而來的 constructor 的屬性訪問構造函數。

若是不手動破壞原型鏈,那麼經過構造函數建立新對象時,三者間的關係:

三者關係

而更多的時候,咱們須要藉助原型來讓對象繼承一些公有行爲,有兩種作法,一種是經過直接在原型對象上動態添加相關屬性,這種方式不破壞原型鏈,比較推薦。

還有一種,定義一個新的原型對象,而後從新賦值構造函數的 prototype 屬性值,將它指向新的原型對象。但這種方式會破壞默認的原型鏈,同時也會破壞構造函數、原型、實例化對象三者間的默認關聯關係。

舉個例子:

function A(){}   //定義構造函數A
A.prototype.c = 1;
var b = new A(); //經過構造函數建立對象b

經過構造函數建立一個新對象b,且在構造函數的 prototype 上手動添加新的屬性c,會被 b 繼承,因爲這種方式是沒有破壞原型鏈的,因此三者間關係以下:

構造函數示例

b.__proto__ 表示 b 的原型,原型對象的 constructor 屬性指向構造函數 A,name 是函數對象的屬性,用於輸出函數名。

並且對象 b 因爲繼承自原型 A.prototype,因此也繼承它的 constructor 屬性,因此也指向構造函數 A。

此時對象 b 的繼承關係:b -> {} -> Object.prototype

以上是默認的不破壞原型鏈下三者的關係,但若是手動破壞了原型鏈呢:

function A(){}   //定義構造函數A
A.prototype.c = 1;
var a = [];      //建立數組對象a
a.c = 0;
A.prototype = a; //手動修改構造函數A的prototype,讓其指向 a
var b = new A(); //經過構造函數建立對象b,b繼承自原型a

上面的代碼手動修改了 A.prototype 的屬性值,讓 b 是繼承自手動建立的對象 a,因此這裏就破壞了默認的原型鏈,同時,三者間的關係也被破壞了:

修改原型示例

首先,c 屬性驗證了 b 是繼承自對象 a了。

而咱們說過,b.__proto__ 指向 b 的原型,在這裏,b 的原型就是對象 a 了。而對象 a 是手動建立的,因此它的 constructor 屬性是繼承自它的原型對象。數組直接量建立的數組對象,本質上是經過 new Array(),因此a的構造函數是 Array(),對象 a 繼承自 Array.prototype。

對於對象 a,咱們建立它的方式並無手動去修改它的原型鏈,因此按默認的三者間的關係,Array.prototype 的 constructor 屬性指向構造函數 Array(),這就是爲何 b.__proto__.constructor.name 的值會是 Array 了。

而,對象 b 繼承自對象 a,因此 b.constructor 的值也纔會是 Array。

此時,對象 b 的繼承關係: b-> a -> Array.prototype -> Object.prototype

因此,在這個例子中,雖然對象 b 是從構造函數 A 建立的,但它的 constructor 其實並不指向 A,這點也能夠稍微說明,構造函數的做用其實更相似於做爲第三方協調原型和實例對象二者的角色。

一般是不建議經過這種方式來實現繼承,由於這樣會破壞默認的三者間的聯繫,除非手動修復,手動對 a 的 constructor 屬性賦值爲 A,這樣能夠手動修復三者間默認的關聯。

來稍微小結一下

由於原型本質上也是對象,因此它也具備對象的特性,同時它也有本身的一些特性,總結下:

  • 全部的引用類型(數組、對象、函數),都具備對象特性,均可以自由擴展屬性,null除外。
  • 全部的引用類型(數組、對象、函數),都有一個 __proto__ 屬性,屬性值的數據類型是對象,含義是隱式原型,指向這個對象的原型。
  • 全部的函數(不包括數組、對象),都有一個 prototype 屬性,屬性值的數據類型是對象,含義是顯式原型。由於函數均可以當作構造函數來使用,當被用於構造函數建立新對象時,新對象的原型就是指向構造函數的 prototype 值。
  • 全部的內置構造函數(Array、Function、Object…),它的 prototype 屬性值都是定義好的內置原型對象,因此從這些內置構造函數建立的對象都默認繼承自內置原型,可以使用內置的屬性。
  • 全部的自定義函數,它的 prototype 屬性值都是 new Object(),因此全部從自定義構造函數建立的對象,默認的原型鏈爲 (空對象){} ---- Object.prototype。
  • 全部的引用類型(數組、對象、函數),__proto__ 屬性指向它的構造函數的prototype值,不手動破壞構造函數、原型之間的默認關係時
  • 全部的引用類型(數組、對象、函數),若是不手動破壞原型鏈,構造函數、原型、實例對象三者之間有默認的關聯。

對象的標識

在 Java 中,因爲對象都是從對應的類實例化出來的,所以類自己就能夠作爲對象的標識,用於區分不一樣對象是否同屬一個類的實例。運算符是 instanceof。

在 JavaScript 中,雖然也有 instanceof 運算符,但因爲並無類的概念,雖然有相似的構造函數、原型的概念存在,但因爲這些本質上也都是對象,因此很難有某個惟一的標識能夠來區分 JavaScript 的對象。

下面從多種思路着手,講解如何區分對象:

instanceof

在 Java 中,能夠經過 instanceof 運算符來判斷某個對象是不是從指定類實例化出來的,也能夠用於判斷一羣對象是否屬於同一個類的實例。

在 JavaScript 中有些區別,但也有些相似:

var b = {}
function A() {}
A.prototype = b;
var a = new A();
if (a instanceof A) { //符合,由於 a 是從A實例化的,繼承自A.prototype即b
    console.log("true"); 
}

function B() {}
B.prototype = b;
var c = new B();
if (c instanceof A) {//符合,雖然c是從B實例化的,但c也一樣繼承自b,而A.prototype指向b,因此知足
    console.log("true");
}
if (c instanceof Object) {//符合,雖然 c 是繼承自 b,但 b 繼承自 Object.prototype,因此c的原型鏈中有 Object.prototype
    console.log("true");
}

在 JavaScript 中,instanceof 運算符的左側是對象,右側是構造函數。但他們的判斷是,只要左側對象的原型鏈中包括右側構造函數的 prototype 指向的原型,那麼條件就知足,即便左側對象不是從右側構造函數實例化的對象。

也就是說,在 JavaScript 中,判斷某些對象是否屬於同一個類的實例,不是根據他們是不是從同一個構造函數實例化的,而是根據他們的構造函數的 prototype 指向是否是相同的。

經過這種方式來區分對象有點侷限是:在瀏覽器中多個窗口裏,每一個窗口的上下文都是相互獨立的,沒法相互比較。

isPrototypeOf

instanceof 是判斷的對象和構造函數二者間的關係,但本質上是判斷對象與原型的關係,只是恰好經過構造函數的 prototype 屬性值作中轉。

那麼,是否有能夠直接判斷對象和原型二者的操做呢?

這個就是 isPrototypeOf 的用法了:左邊是原型對象,右邊是實例對象,用於判斷左邊的原型是否在右邊實例對象的原型鏈當中:

Object.prototype.isPrototypeOf(b);

但它跟 instanceof 有個本質上的區別,instanceof 是運算符,而 isPrototypeOf 是 Object.prototype 中的方法,因爲基本全部對象都繼承自這個,因此基本全部對象均可以使用這個方法。

instanceof 和 isPrototypeOf 更多使用的場景是用於判斷語句中,若是須要主動對某個對象獲取它的一些標識,可使用接下來介紹的幾種方式:

typeof

在 JavaScript 中數據類型大致上分兩類:原始類型和引用類型。

原始類型對應的值是原始值,引用類型對應的值爲對象。

對於原始值而言,使用 typeof 運算符能夠獲取原始值所屬的原始類型。

對於函數對象,也可使用 typeof 運算符來區分:

typeof

因此它的侷限也很大,基本只能用於區分原始值的標識,對於對象,自定義對象,它的結果都是 object,沒法進行區分。

對象的類屬性

在對象一節中,介紹過,對象有一個類屬性,其實也就是經過 Object.prototype.toString() 方法能夠獲取包含原始類型和引用類型名稱的字符串,對其進行截取能夠獲取類屬性。

對象類屬性

相比於 typeof,它的好處在於能夠區別全部的數據類型的本質,包括內置引用對象(數組、函數、正則等),也能夠區分 null。

侷限在於,須要本身封裝個工具方法獲取類屬性,但這不是難點,問題在於,對於自定義的構造函數,都是返回 Function,而不少對象實際上是經過構造函數建立出來的,因此沒法區分不一樣的構造函數所建立的對象。

constructor 的 name 屬性

constructor 是對象的一個屬性,它的值是繼承自原型的取值。而原型該屬性的取值,在不手動破壞對象的原型鏈狀況下,爲建立對象的構造函數。

即,默認狀況下,構造函數的 prototype 指向原型,原型的 constructor 指向構造函數,那麼從該構造函數建立的對象都繼承了原型的這個屬性可指向構造函數。

因此,在這些場景下,可用對象的 constructor.name 來獲取構造函數的函數名,用函數名做爲對象的標識。

function A(){}   //定義構造函數A
var a = new A();
var b = {};

函數名

這種方式有個侷限,若是手動修改構造函數的 prototype,破壞了對象的原型鏈,那麼此時,新建立的對象的 constructor 就不是指向建立它的構造函數了,此時,這種方式就沒法處理了。

因爲 JavaScript 不像 Java 這種靜態的類結構語言,因此沒有一種完美的方式適用於各自場景中來區分對象的標識,只能是在適用的場景選擇適合的方式。

因此,在 JavaScript 有一種編程理念:鴨式辯型

鴨式辯型

我不是很理解中文翻譯爲何是這個詞,應該是某個英文詞直譯過來的。

它的理念是:像鴨子同樣走路、游泳、嘎嘎叫的鳥就稱它爲鴨子。

通俗點說,編程時,不關心對象所屬的標識,不關心對象繼承自哪一個原型、由哪一個構造函數建立,只要這個對象含有相同的屬性、行爲,那麼就認爲它們歸屬於同一類。

有個例子就是:類數組對象,它本質並非數組對象,但因爲具備數組對象的特徵,因此基本上能夠把它當作數組來使用。

對應到編程中,不該用判斷對象是否擁有相同的標識來區分對象,而是應該判斷對象是否含有指望的屬性便可。


你們好,我是 dasu,歡迎關注個人公衆號(dasuAndroidTv),公衆號中有個人聯繫方式,歡迎有事沒事來嘮嗑一下,若是你以爲本篇內容有幫助到你,能夠轉載但記得要關注,要標明原文哦,謝謝支持~
dasuAndroidTv2.png

相關文章
相關標籤/搜索