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

什麼是原型語言 

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

  2. “原型對象”是核心概念。原型對象是新對象的模板,它將自身的屬性共享給新對象。一個對象不但可以享有自己創建時和運行時定義的屬性,而且可以享有原型對象的屬性。 

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

原型語言創建有兩個步驟 

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

  2. 初始化內部屬性 :這一步驟不是必要的。通俗點說,就是,對”複製品”不滿意,我們可以”再加工”,使之獲得不同於”模板”的”個性”。 

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

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



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

通常情況下ecma提供的Global對象對是不存在的,沒有具體的對象

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

內置對象-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 基於原型


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章