注意: 本文章爲 《重學js之JavaScript高級程序設計》系列第五章【JavaScript引用類型】。 關於《重學js之JavaScript高級程序設計》是從新回顧js基礎的學習。前端
面向對象的語言有一個標誌,那就是它們都有類的概念,而經過類能夠建立任意多個具備相同屬性和方法的對象。可是,再前面提到過。ES中沒有類的概念,所以它的對象也與基於類的語言中的對象有所不一樣。web
對象的定義:‘無序屬性的集合,其屬性能夠包含基本值、對象或者函數。’ 嚴格來說,這就至關於說對象是一組沒有特定順序的值。對象的每一個屬性或方法都有一個名字,而每一個名字都映射到一個值。因此咱們能夠把 ES 的對象想象成散列表:無非就是一組名值對,其中值能夠是數據或函數。segmentfault
每一個對象都是基於一個引用類型建立的,這個引用類型能夠是上一章討論的原生類型,也能夠是自定義類型。數組
最簡單的方式就是建立一個Object的實例,而後再爲它添加屬性和方法。函數
let p = new Object() p.name = 'js' p.age = 20 p.job = 'jizhe' p.sayName = function() { alert(this.name) } p.sayName() // js
工廠模式:抽象了建立具體對象的過程。考慮到ES中沒法建立類,因而就用一種特定的函數來封裝以特定接口建立對象的細節。post
function p(name, age, job) { let o = new Object() o.name = name o.age = age o.job = job o.sayName = function() { alert(this.name) } return o } let p1 = p('tc', 30, '老宋') let p2 = p('bd', 22, '百度') p1.sayName // tc p2.sayName // bd
函數 p() 可以根據接受的參數來構建一個包含全部必要信息的 Person 對象。能夠無數次的調用這個函數,而每次它都會返回一個包含三個屬性一個方法的對象。工廠模式雖然解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題(即怎麼樣找到一個對象的類型)學習
在前面幾章介紹過,ES的構造函數能夠用來建立特定類型的對象。像Object 和 Array這樣原生構造函數,在運行時會自動出如今執行環境中。此外,也能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。以下:this
function p(name, age, job) { this.name = name this.age = age this.job = job this.sayName = function (){ alert(this.name) } } let p1 = new P('tc', 33, 'haha') let p2 = new P('gg', 32, '小夭同窗') p1.sayName() // haha p2.sayName() // 小夭同窗
在上面的例子中,p()函數取代了上一小段的函數。除了內容代碼相同,還有如下區別:prototype
另外若是要建立P實例,必須使用 new 操做符,以這種方式調用構造函數實際上會經歷如下4個步驟:設計
對象的 constructor 屬性,最初是用來標識對象類型的。可是,提到檢測對象類型,仍是 instanceof 操做符更可靠。
instanceof 判斷某個對象是否屬於另一個對象的實例
優勢: 相比於工廠模式,構造函數模式能夠將它的實例標識爲一種特定的類型。
注意: 若是以這種方式定義的構造函數是定義在 Global對象中的,所以除非另有說明,instaceof 操做符 和 construcotr 屬性始終會假設是在全局做用域中查詢構造函數。
構造函數與其餘函數的惟一區別,就是在於調用它們的方式不一樣。不過,構造函數也是函數,不存在定義構造函數的特殊語法。任何函數,只要經過new操做符來調用,那它就能夠做爲構造函數;而任何函數,若是不經過new 操做符來調用,那它和普通函數也沒有射門兩樣。
// 構造函數調用 let p = new Person('tc', 22, '哈哈哈') p.sayName // tc // 普通函數調用 P('gc', 23, 'oo') // 添加到 window windwo.sayName // gc // 在另一個對象的做用域中調用 let o = new Object() P.call(o, 'new', 33, 'suzhou') o.sayName // new
構造函數雖然好用,但也有缺點。使用構造函數的主要問題就是每一個方法都要在每一個實例上從新建立一遍。
咱們每次建立一個函數的時候都有 一個 prototype 屬性,這個屬性是一個指針,指向一個對象。而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法 。若是按照字面意思,那麼 prototype 就是經過調用構造函數而建立的那個對象實例的原型對象。使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。換句話說,沒必要再構造函數中定義對象實例的信息,而是能夠將這些信息直接添加到原型對象中。
不管何時,只要建立了一個新韓淑,就會根據一組特定的規則爲該函數建立一個 prototype 屬性, 這個屬性指向函數的原型對象。在默認狀況下,全部原型對象都會自動得到一個 constructor 構造函數屬性,這個屬性包含了一個指向 prototype 屬性所在函數的指針。經過這個構造函數,咱們還能夠繼續爲原型對象添加其餘屬性和方法。
建立了自定義的構造函數以後,其原型對象默認只會 取得 constructor 屬性; 至於其餘方法,則都是從 Object 繼承而來的。當調用構造函數建立一個新實例後,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。在不少實現中,這個內部屬性的名字是 proto ,並且經過腳本能夠訪問到;而在其餘實現中,這個屬性對腳本則是徹底不可見的。不過,要明確的真正重要的一點,就是這個鏈接存在於實例於構造函數的原型對象之間,而不是存在於實例於構造函數之間
另外,每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定名字的屬性。搜索首先從對象實例自己開始。若是在實例中找到了具備給定名字的屬性,則返回該屬性的值;若是沒有找到,則繼續搜索指針指向的原型對象,在原型隊形中查找具備給定名字的屬性。若是在原型對象中找到了這個屬性,則返回該屬性的值。也就是說咱們調用p.sayName()的時候,會前後執行兩次搜索,首先,解析器會問:實例 p 有 sayName 屬性嗎,若是沒有,則再問p的原型有sayName屬性嘛,若是有 則讀取保存在原型對象中的函數。
雖然能夠經過對象實例訪問保存在原型中的值,但卻不能經過對象實例重寫原型中的值。若是咱們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那麼咱們就在實例中建立該屬性,該屬性會屏蔽原型中的那個屬性。
有兩種方式使用 in 操做符:單獨使用和在 for-in循環中使用。在單獨使用時,in操做符會在經過對象可以訪問給定屬性時返回 true,不管該屬性存在於實例中仍是原型中。 另外因爲in操做符只要經過對象可以訪問到屬性就返回 true ,hasOwnProperty()只在屬性存在於實例中時才返回true,所以只要 in 操做符返回 true 而 hasOwnProperty()返回 false,就能夠肯定屬性時原型中的屬性。
**注意:**在使用 for-in 循環時,返回的是可以經過對象訪問的、可枚舉的屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性的實例屬性也會在 for-in循環中返回。
爲了減小沒必要要的輸入,以及從視覺上更好的封裝原型的功能,常見的做法是用包含一個說有屬性和方法的對象字面量來重寫整個原型對象。
function P(){ } P.prototype = { name: 'tc', age: 22, job: 'web', sayName: function(){ console.log(this.name) } }
因爲在原型中查找值的過程是一次搜索,所以咱們對原型對象所作的任何修改都可以當即從實例上反映出來。儘管能夠隨時爲原型添加屬性和方法,而且修改可以當即在全部對象實例中反映出來,但若是是重寫整個原型對象,那麼狀況就不一樣了。另外,調用構造函數時會爲實例添加一個指向最初原型__proto__指針,而把原型修改成另外一個對象就等於切斷了構造函數和最初原型之間的聯繫。實例中的指針僅指向原型,而不是指向構造函數。
原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型(Object、Array、String)都在其構造函數的原型上定義了方法。
typeof Array.prototype.sort // function typeof String.prototype.substring // function
經過原生對象的原型,不只能夠取得全部默認方法的引用,也能夠定義新的方法。能夠像修改自定義對象的原型同樣修改原生對象的原型,所以能夠隨時添加方法。
重要:儘管能夠這樣作,可是並不推薦在產品化的程序中修改原生對象的原型。若是因某個實現中缺乏某個方法,就在原生對象的原型中添加這個方法,那麼當在另外一個支持該方法的實現中運行代碼時,就可能的致使命名衝突,並且,這樣作也可能會意外地重寫原生方法
原型模式也有缺點,第1、它省略了爲構造函數傳遞初始化參數這一環節,結果全部實例在默認狀況下都將取得相同的屬性值。雖然這個在某一程度上帶來了不方便,但其最大的問題仍是由其共享的本性所致使的。
在原型中,全部的屬性時被不少實例共享的,這種共享對於函數很是合適。對於那些包含基本值的屬性也行,可是對於引用類型值的屬性來講,就有問題了。
建立自定義類型的最多見方式,就是組合使用構造函數模式和原型模式。構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。結果每一個實例都會有本身的一份實例屬性的副本,但同時又共享着對方法的引用,最大限度地節省了內存。
把全部信息都封裝了在構造函數中,而經過構造函數中初始化原型,又保持了同時使用構造函數和原型的有點。也就是說能夠經過檢查某一個應該存在的方法是否有效,來決定是否初始化原型。
function P(name, age, job) { this.name = name this.age = age this.job = job if(typeof this.sayName != 'function') { P.prototype.sayName = fucntion() { console.log(this.name) } } } let p = new P('tc', 23, 'web') p.sayName() // tc
上面的代碼 只有在 sayName() 方法不存在的狀況下才會將它添加到原型中。這段代碼只有在初次調用函數時纔會執行。事後,原型已經初始化完成,不須要再修改。這樣對原型所作的修改,可以當即在全部實例中獲得反映。另外if語句檢查的能夠是初始化以後應該存在的任何屬性或方法。
**注意: ** 使用動態原型模式時,不能使用對象字面量重寫原型,若是在已經建立了實例的狀況下重寫原型,那麼就會切斷現有實例與新原型之間的聯繫。
建立一個函數,該函數的做用僅僅時封裝建立對象的代碼,而後再返回新建立的對象。
注意: 寄生構造函數模式返回的對象與構造函數或者與構造函數的原型屬性之間沒有關係,也就是說,構造函數返回的對象與在構造函數外部建立的對象沒有什麼不一樣。爲此,不能依賴 instanceof 操做符來肯定對象類型。
與寄生構造函數模式相似,使用穩妥構造函數模式建立的對象與構造函數之間也沒有什麼關係,所以 instanceof 操做符對這種對象也沒有意義。
<p align="center" style="font-size:30px;color:#1c8a95"> 歡迎關注 公衆號【小夭同窗】 </p>
重學js系列
ES6入門系列
Git教程