再談javascriptjs原型與原型鏈及繼承相關問題

什麼是原型語言 

  1. 只有對象,沒有類;對象繼承對象,而不是類繼承類。 javascript

  2. 「原型對象」是核心概念。原型對象是新對象的模板,它將自身的屬性共享給新對象。一個對象不但能夠享有本身建立時和運行時定義的屬性,並且能夠享有原型對象的屬性。 html

  3. 每個對象都有本身的原型對象,全部對象構成一個樹狀的層級系統。root節點的頂層對象是一個語言原生的對象,只有它沒有原型對象,其餘全部對象都直接或間接繼承它的屬性。 java

原型語言建立有兩個步驟 

  1. 使用」原型對象」做爲」模板」生成新對象 :這個步驟是必要的,這是每一個對象出生的惟一方式。以原型爲模板建立對象,這也是」原型」(prototype)的原意。 node

  2. 初始化內部屬性 :這一步驟不是必要的。通俗點說,就是,對」複製品」不滿意,咱們能夠」再加工」,使之得到不一樣於」模板」的」個性」。 web

因此在JavaScript的世界裏,萬物皆對象這個概念從一而終。typescript

js高級---本地對象、內置對象、宿主對象



全局對象:通常全局對象會有兩個,一個是ecma提供的Global對象,一個是宿主提供。如在瀏覽器中是window、在nodejs中是global。【因此啊,在瀏覽器中全局對象Global+window】瀏覽器

一般狀況下ecma提供的Global對象對是不存在的,沒有具體的對象框架

宿主對象-host object:即由 ECMAScript 實現的宿主環境提供的對象,包含兩大類,一個是宿主提供,一個是自定義類對象,ECMAScript官方未定義的對象都屬於宿主對象,全部非本地對象都是宿主對象。宿主提供對象原理--->由宿主框架經過某種機制註冊到ECscript引擎中的對象,如宿主瀏覽器(以遠景爲參考)會向ECscript注入window對象,構建其實現javascript。ide

內置對象-Build-in object:由 ECMAScript 實現提供的、獨立於宿主環境的全部對象,在 ECMAScript 程序開始執行時出現,即在引擎初始化階段就被建立好的對象。這意味着開發者沒必要明確實例化內置對象,它已被實例化了Global(全局對象)、Math、JSON函數

基本包裝類型對象:ECMAScript還提供了3個特殊的引用類型: Boolean、Number、String。這些類型與其餘內置對象類型類似,但同時具備各自的基本類型相應的特殊行爲。實際上,每當讀取一個基本類型值得時候,後臺就會建立一個對應的基本包裝類型的對象,從而讓咱們可以調用一些方法來操做這些數據 包裝類型,是一個專門封裝原始類型的值,並提供對原始類型的值執行操做的API對象

其餘內置對象與基本包裝類型對象的區別?

普通的內置對象與基本包裝類型的主要區別就是對象的生命期,使用new操做符建立的引用類型的實例,在執行流離開當前做用域以前都一直保存在內存中,而自動建立的基本包裝類型的對象,則只是存在於一行代碼的執行瞬間,而後當即被當即銷燬。這意味着咱們不能再運行時爲基本包裝類型值添加屬性和方法。

var s1="some text"; s1.color="red"; var s2=new String("some text"); s2.color="red"; console.log(s1.color);//undefined console.log(s2.color);//red console.log(s1==s2);//true console.log(s1===s2);//false

在第二行爲s1添加一個color屬性,第三行代碼執行時,再次訪問s1,結果s1的color屬性被銷燬了。詳情推薦閱讀《JavaScript內置對象--基本包裝類型(Boolean、Number、String)詳解

原生對象-native object:也叫內部對象、本地對象。獨立於宿主環境的ECMAScript實現提供的對象。與宿主無關,在javascript(遠景瀏覽器)、nodejs(node平臺)、jscript(ie瀏覽器)、typescript(微軟平臺)等等中均有這些對象。簡單來講,本地對象就是 ECMA-262 定義的類(引用類型)。

Object、Function、Array、String、Boolean、Number、Date、RegExp、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError、Global

在運行過程當中動態建立的對象,須要new,以Number爲例:

var n1=Number('1'); var n2=1; n2.xxx=2;console.log(n2); //undefined console.log(n1===n2)//false console.log(n1.toString()===n2.toString())//true console.log(n1.__proto__===Number)//false console.log(n2.__proto__===Number)//false console.log(n1.__proto__===n2.__proto__)//true

n1和n2雖然數值都是1,但n2的類型屬於'object',n1則爲'number',身爲基本類型number的n1直接指向了數字1,而n2指向了一個地址,這個地址中存放了數值1,這就是對象和基本類型的區別。

v2-f7020e0bc392c01ab4f4b829d72e5951_hd.jpg

可是原型對象只存在於函數對象。也就是本質上只要是經過new Function建立的函數對象會有一個原型對象

而對於其餘非Function的引用類型歸根結底也是經過new Function建立的

如上面提到的Array類型、Object類型。

實際上,在每一個函數對象建立的時候,都會自帶一個prototype的屬性,這個屬性至關於一個指針,指向他自己的原型對象,這個原型對象裏包含着自定義的方法屬性,

function a(){       this.name='xiaoming';       this.sayName=function () {           console.log(this.name);       }   }

在默認狀況下,a.prototype下會帶有一個constructor屬性,這個屬性指向建立當前函數對象的構造函數,好比這裏

constructor指向構造函數a自己也就是說:a.prototypr.constructor==a   //true

另外默認還有一個_proto_屬性,這個屬性指向由建立這個函數對象的引用類型中繼承而來的屬性和方法。

當經過構造函數實例化一個對象b時,即new a();

首先這個new出來的對象屬於普通對象,因此沒有prototype屬性。但他有_proto_這個屬性,這個屬性指向建立它的引用類型的原型對象,在這個例子中指向a.prototype,從而繼承來自引用類型a的屬性和方法。推薦閱讀《JS 的 new 究竟是幹什麼的?

var 對象 = new 函數對象 這個聲明形式能夠引伸出:

函數.__proto__ ===Function.prototype Function.__proto__ === Function.prototype Object.__proto__ === Function.prototype //Objec也是個函數,函數都是由Function構造出來的。 Number.__proto__ === Function.prototype 構造函數.prototype.__proto__ ===Object.prototype Function.prototype.__proto__ ===Object.prototype Number.prototype.__proto__ ===Object.prototype Object.__proto__ .__proto__ ===null

理解了以上的關係後,'__proto__'是對象的屬性、'prototype'是函數的屬性這句話也就懂了

null是對象原型鏈的終點,其值既有(是一個對象)又無(不引用任何對象),表明着對象本源的一種混沌、虛無的狀態,正與老子《道德經》中的「道」,有着同等的意義(心中一萬隻艹尼瑪奔騰而過,仍是寫java爽啊)。好比:《undefined與null的區別

v2-0144a9d2325492f19eae6e639bf52c8c_r.jpg

在JS中,undefined是全局對象的一個屬性,它的初始值就是原始數據類型undefined,而且沒法被配置,也沒法被改變。undefined從字面意思上理解爲「未定義」,即表示一個變量沒有定義其值。

而null是一個JS字面量,表示空值,即沒有對象。與undefined相比,null被認爲是「指望一個對象,可是不引用任何對象的值」,而undefined是純粹的「沒有值」。

// null爲對象原型鏈的終點 console.log(Object.getPrototypeOf(Object.prototype)); // null // null是一個對象 console.log(typeof null); // object // null 爲空 console.log(!null); // true

JS中的全部事物都是對象,對象是擁有屬性和方法的數據

爲了描述這些事物,JS便有了「原型(prototype)」的概念

原型模式是js對繼承的一種實現:使用原型,能複用代碼,節省內存空間 (java類的代碼在內存只有一份,而後每一個對象執行方法都是引用類的代碼,全部子類對象調用父類方法的時候,執行的代碼都是同一份父類的方法代碼。可是JS沒有類,屬性和方法都是存在對象之中,根本沒有辦法作到java那樣經過類把代碼共享給全部對象)。

推薦閱讀《深入理解JavaScript基於原型的面向對象

從一張圖看懂原型對象、構造函數、實例對象之間的關係

20180915180258272497037.jpg

  • prototype:構造函數中的屬性,指向該構造函數的原型對象。

  • constructor:原型對象中的屬性,指向該原型對象的構造函數

  • _proto_:實例中的屬性,指向new這個實例的構造函數的原型對象

20180915180353355731389.jpg

在JavaScript 中,每一個對象都有一個指向它的原型(prototype)對象的內部連接。這個原型對象又有本身的原型,直到某個對象的原型爲 null 爲止(也就是再也不有原型指向),組成這條鏈的最後一環。這種一級一級的鏈結構就稱爲原型鏈(prototype chain)

要清楚原型鏈,首先要弄清楚對象

普通對象

    最普通的對象:有__proto__屬性(指向其原型鏈),沒有prototype屬性。

    原型對象(Person.prototype 原型對象還有constructor屬性(指向構造函數對象))

函數對象:

    凡是經過new Function()建立的都是函數對象。

    擁有__proto__、prototype屬性(指向原型對象)。


JavaScript 對象是動態的屬性「包」(指其本身的屬性)。JavaScript 對象有一個指向一個原型對象的鏈。當試圖訪問一個對象的屬性時,它不只僅在該對象上搜尋,還會搜尋該對象的原型,以及該對象的原型的原型,依此層層向上搜索,直到找到一個名字匹配的屬性或到達原型鏈的末尾。

20180915181327215440724.jpg

原型-顯式原型-隱式原型-共享原型鏈 

顯式原型(explicit prototype property )每個函數在建立以後都會擁有一個名爲prototype的屬性,這個屬性指向函數的原型對象。用來實現基於原型的繼承與屬性的共享。

隱式原型 (implicit prototype link) JS中任意對象都有一個內置屬性__proto__(部分瀏覽器爲[[prototype]]),指向建立這個對象的函數(即構造函數)constructor的prototype。用來構成原型鏈,一樣用於實現基於原型的繼承。



當咱們「讀取」 obj.toString 時,JS 引擎會作下面的事情:

1. 看看 obj 對象自己有沒有 toString 屬性。沒有就走到下一步。

2. 看看 obj.__proto__ 對象有沒有 toString 屬性,發現 obj.__proto__ 有 toString 屬性,因而找到了

3. 若是 obj.__proto__ 沒有,那麼瀏覽器會繼續查看 obj.__proto__.__proto__,若是 obj.__proto__.__proto__ 也沒有,那麼瀏覽器會繼續查,obj.__proto__.__proto__.proto__

直到找到 toString 或者 __proto__ 爲 null(無論你從那個屬性開始,連續引用__proto__的指針,最後輸出的那個值就是null)。

上面的過程,就是「讀」屬性的「搜索過程」。

而這個「搜索過程」,是連着由 __proto__ 組成的鏈子一直走的。

這個鏈子,就叫作「原型鏈」。


要搞清楚 valueOf / toString / constructor 是怎麼來的,就要用到 console.dir 了。

共享原型鏈(Shared prototype chain)此模式全部子對象及後代對象都共享一個原型(都是經過b.prototype=a.prototype;這種模式鏈接的對象),在這些後代對象上修改原型,會影響因此處在同一共享原型鏈上的全部對象。並且此模式只繼承原型鏈上的屬性和方法,經過this定義的屬性和方法沒法訪問和繼承

v2-550a5636884c765eceb0e165b9a75a01_hd.jpg


那麼 obj.toString 和 obj2.toString 實際上是同一個東西,也就是 obj2.__proto__.toString。

這有什麼意義呢?

若是咱們改寫 obj2.__proto__.toString,那麼 obj.toString 其實也會變!

這樣 obj 和 obj2 就是具備某些相同行爲的對象,這就是意義所在。

若是咱們想讓 obj.toString 和 obj2.toString 的行爲不一樣怎麼作呢?

直接賦值就行了:

obj.toString = function(){ return '新的 toString 方法' }

原型對象

每建立一個函數都會有一個prototype屬性,這個屬性是一個指針,指向一個對象(經過該構造函數建立實例對象的原型對象)原型對象是包含特定類型的全部實例共享的屬性和方法。原型對象的好處是,能夠讓全部實例對象共享它所包含的屬性和方法

原型對象屬於普通對象。Function.prototype是個例外,它是原型對象,卻又是函數對象,做爲一個函數對象,它又沒有prototype屬性。

02357dea-8458-3850-a8d0-31e0a9574979.jpg


對象與函數

擁有了描述事物的能力,卻沒有創造事物的能力,顯然是不完整的,所以須要一個Object的生成器來進行對象的生成。


JS將生成器以構造函數constructor來表示,構造函數是一個指針,指向了一個函數

函數(function) 函數是指一段在一塊兒的、能夠作某一件事的程序。構造函數是一種建立對象時使用的特殊函數

20180904194400773818284.jpg

對象的構造函數function Object同時也是一個對象,所以須要一個可以描述該對象的原型,該原型即是Function.prototype,函數的原型用來描述全部的函數。對象的構造函數的__proto__指向該原型。

20180904194443703786282.jpg

函數的原型自己也是對象,所以其__proto__指向了對象的原型。一樣,該對象也須要一個對應的生成器,即其構造函數function Function。

20180904194537290312073.jpg

函數的構造函數是由函數生成的一個對象,因此其原型即爲函數的原型,其隱式原型也一樣爲函數的原型Function.prototype。

instanceof操做符的內部實現機制和隱式原型、顯式原型有直接的關係。instanceof的左值通常是一個對象,右值通常是一個構造函數,用來判斷左值是不是右值的實例。它的實現原理是沿着左值的__proto__一直尋找到原型鏈的末端,直到其等於右值的prototype爲止。

instanceof 的做用是判斷一個對象是否是一個函數的實例。好比 obj instanceof fn, 其實是判斷fn的prototype是否是在obj的原型鏈上。因此

instanceof運算符的實質:用來檢測 constructor.prototype是否存在於參數 object的原型鏈上。

根據上圖展現的Object和Function的繼承依賴關係,咱們能夠經過instanceof操做符來看一下Object和Function的關係:

console.log(Object instanceof Object); // true console.log(Object instanceof Function); // true console.log(Function instanceof Object); // true console.log(Function instanceof Function); // true

函數與對象相互依存,分別定義了事物的描述方法和事物的生成方法,在生成JS萬物的過程當中缺一不可。

Function instanceof Function    // true, why? Function.prototype是原型對象,倒是函數對象
  • Object特殊在Object.prototype是憑空出來的。語法上,全部的{}都會被解釋爲new Object();

  • Function特殊在__proto__ == prototype。語法上,全部的函數聲明都會被解釋爲new Function()。

咱們來看Function和Object的特殊之處:

  1. Object是由Function建立的:由於Object.__proto__ === Funciton.prototype;

  2. 同理,Function.prototype是由Object.prototype建立的;

  3. Funciton是由Function本身建立的!

  4. Object.prototype是憑空出來的!

推薦閱讀 《JavaScript 內置對象與原型鏈結構》與《JavaScript中的難點之原型和原型鏈

這幾句話能解釋一切關於原型方面的問題:

  1. 當 new 一個函數的時候會建立一個對象,『函數.prototype』 等於 『被建立對象.__proto__』

  2. 一切函數都是由 Function 這個函數建立的,因此『Function.prototype === 被建立的函數.__proto__』

  3. 一切函數的原型對象都是由 Object 這個函數建立的,因此『Object.prototype === 一切函數.prototype.__proto__』

推薦閱讀:《對原型、原型鏈、 Function、Object 的理解

原型鏈是實現繼承的主要方法

先說一下繼承,許多OO語言都支持兩張繼承方式:接口繼承、實現繼承。

    |- 接口繼承:只繼承方法簽名

    |- 實現繼承:繼承實際的方法

因爲函數沒有簽名,在ECMAScript中沒法實現接口繼承,只支持實現繼承,而實現繼承主要是依靠原型鏈來實現。

原型鏈基本思路:

利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。


每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數想指針(constructor),而實例對象都包含一個指向原型對象的內部指針(__proto__)。若是讓原型對象等於另外一個類型的實例,此時的原型對象將包含一個指向另外一個原型的指針(__proto__),另外一個原型也包含着一個指向另外一個構造函數的指針(constructor)。假如另外一個原型又是另外一個類型的實例……這就構成了實例與原型的鏈條。

原型鏈基本思路(圖解):

v2-901202a60d3f6e9fcc90a69d06fe0282_hd.jpg

推薦閱讀《JS重點整理之JS原型鏈完全搞清楚



類-對象冒充-class

類(Class)是面向對象程序設計(OOP,Object-Oriented Programming)實現信息封裝的基礎。類是一種用戶定義類型,也稱類類型。每一個類包含數聽說明和一組操做數據或傳遞消息的函數。類的實例稱爲對象

在ECMAScript 2015 中引入的JS類(classes)以前,要在JS中實現類即是採用原型繼承的方式。

當把一個函數做爲構造函數,使用new關鍵字來建立對象時,即可以把該函數看做是一個類,建立出來的對象則是該類的實例,其隱式原型__proto__指向的是該構造函數的原型。


在訪問該對象的屬性或方法時,JS會先搜索該對象中是否認義了該屬性或方法,若沒有定義,則會回溯到其__proto__指向的原型對象去搜索,若仍然未搜索到,則會繼續回溯該原型的原型,直到搜索到原型鏈的終點null;


這種特性能夠理解爲:構造函數生成的實例,繼承於該構造函數的原型


得益於這種特性,咱們可使用定義構造函數的方式來定義類。

20180904195226663971138.jpg

function Person() {} // 定義Person構造函數 // 一般以大寫字母開頭來定義類名 console.log(new Person() instanceof Person); // true

以上定義了Person類,該構造函數是由Function構造而來,因此其隱式原型指向函數的原型,而爲了描述該事物,同時生成了該類的原型Person.prototype,該原型又是由Object構造而來,因此其隱式原型指向了對象的原型。

後記:文字有點亂,就是多篇文章的精華提煉。發現把一個本身懂的事情,深刻淺出講明白,並不是易事。文有不妥之處,請留言告知,謝謝。

文章首發於:https://www.zhoulujun.cn/html/webfront/ECMAScript/js/2015_0715_119.html,若是不妥之處,請到官網留言,謝謝!

參考文字:

【道生萬物】理解Javascript原型鏈

js高級---本地對象、內置對象、宿主對象

js原型與原型鏈

「每日一題」什麼是 JS 原型鏈?

JS理解原型、原型鏈

一張圖弄清Javascript中的原型鏈、prototype、__proto__的關係

js中的原型、原型鏈、繼承模式

說說原型(prototype)、原型鏈和原型繼承

淺談JS原型和原型鏈

原型語言解釋

基於類 vs 基於原型

相關文章
相關標籤/搜索