相信大家都這樣用過 map
:
let arr = [0, 1, 2] let doubleArr = arr.map(c => c * 2) console.log(doubleArr) // 0, 2, 4
不知道你有沒有想過, arr
本身並沒有設定 map
屬性,那為什麼可以用 map
這個函數呢?
把它列印出來看看:
console.log(arr) // 0: 0 // 1: 1 // 2: 2 // length: 3 // __proto__: Array(0)
出現了一個名為 __proto__
的物件,如果再將其展開,就會看到所有 Array 物件可以使用的函數;當然我們也可以在其中找到 map
函數,而這也正是例子中所呼叫的 arr.map
這個函數:
console.log(arr.map === arr.__proto__.map) // true
這裡出現的 __proto__
物件,也就是所謂的 原型物件(Prototype) 。
不同於 Java、C# 等基於類(Class) 的物件導向語言,通過定義類、建立範例、指定繼承等方式來傳遞屬性及方法;Javascript 則是個基於原型(Prototype)的對語言 ,通過預先建立出的原型物件,當新物件建立時指定物件的原型要參照哪個原型物件。
而當我們呼叫物件的屬性或方法時,若物件本身沒有這個屬性或方法,JavaScript 會自動尋找它原型中的方法,這也就是為什麼可以直接呼叫 arr.map
而不會出錯的原因。
你可能已經發現在前面的例子中,__proto__
物件裡仍然有 __proto__
屬性:
console.log(arr.__proto__) // Array 的 Prototype console.log(arr.__proto__.__proto__) // Object 的 Prototype console.log(arr.__proto__.__proto__.__proto__) // null
在上述機制中,每當物件建立時都會繫結原型,既然物件都有原型,物件原型本身也是物件,自然也不例外;由這個例子中我們可以看出:
arr
是陣列範例,原型是 Arrayarr.__proto__
是陣列的原型,原型是 Objectarr.__proto__.__proto__
是物件的原型,原型是 null
arr.__proto__.__proto__.__proto__
是 null,沒有任何屬性由於每個物件都有原型,這樣就形成了一個關聯一個、層層相互依賴的從屬關係,我們把它們叫做原型鏈(Prototype Chain) ;通過這種機制,讓物件得以使用原型中的屬性和方法,並憑藉原型鏈一層一層的按順序繼承,讓物件能擁有原型鏈上所有原型的功能,這就是 JavaScript 物件背後的運作機制。
補充:在 JavaScript 中,幾乎每個原型鏈的末端都會是 Object,並最後指向到 null
。
說了這麼多,該來點程式碼了,接下來就來練習一下原型的建立、設定及修改吧。
先來建立一個新的物件建構函式:
function Person(name) { this.name = name }Person.prototype.hello = function () { console.log(`Hello ${this.name}.`) }let gary = new Person('Gary') gary.hello() // Hello Gary.Object.getPrototypeOf(gary) // {hello: ƒ, constructor: ƒ}
上面的例子建立了一個簡單的物件建構函式 Person()
,並在建構函式中設定物件屬性。物件的方法中,由於方法不需要讓每個物件都獨自擁有一份,以避免造成冗餘的記憶體消耗,應該要像前面 Array.prototype.map
的例子那樣把物件的方法設定給原型物件(Person.prototype
),讓這個建構函式建立出來的物件都可以共用這些方法。最後建立一個新的 Person
物件,並通過 getPrototypeOf(obj)
獲取新產生物件的原型。
Q:為什麼不直接用__proto__
獲取原型物件?
A:因為雖然__proto__
被幾乎所有的瀏覽器支援,但它是非標準屬性;通過getPrototypeOf
取得物件的原型是正確的方法。提醒:
Person.prototype
不是Person
的原型,而是建構函式執行後所建立的新物件的原型;千萬不要把建構函式的prototype
屬性與物件的原型搞混!
接著再建立新的物件原型,並繼承 Person
:
function Engineer(name, skill) { Person.call(this, name) this.skill = skill } Engineer.prototype = Object.create(Person.prototype) Engineer.prototype.constructor = Engineerlet alice = new Engineer('Alice', 'JavaScript') alice.hello() // Hello Alice. console.log(alice.skill) // JavaScriptObject.getPrototypeOf(alice) // Person {constructor: ƒ}
這裡建立了新的物件 Engineer
的原型,並通過 Engineer.prototype
的指定,讓它的原型繼承自 Person.prototype
,最後再重新設定 Engineer.prototype.constructor
,讓建構函式重新指回自己;這樣就完成了最基本的原型繼承。
Q:為什麼需要重新設定constructor
?
A:這邊功過Object.create
複製了Person.prototype
的全部屬性,連同constructor
屬性都會被覆蓋掉,如果constructor
屬性錯誤,在做instanceof
判斷時會產生錯誤的結果;因此這邊設定繼承時需要再次將constructor
重新指定回建構函式本身。
原型的參照、繼承是直接參照到原型物件上,並非是在每個物件都複製一份原型;因此可以利用這個特性,在原型上增加自定義的屬性和方法,讓所有該型別的物件都能得到新方法;許多針對舊版瀏覽器的 Polyfill 就是這樣實現的。
例如我們在寫 Vue 專案的時候,可能都有做過類似的操作,把共用性高的屬性方法放到 Vue.prototype
中:
Object.defineProperty(Vue.prototype, '$date', { value: dateTimeFormat }) // 之後就可以這樣用 vm.$date(dateObj)
這樣的確很方便,但也要提醒開大家,當我們在做原型修改的時候要特別小心。接著剛才的例子,如果嘗試對 Person
原型中的方法做個修改:
Person.prototype.hello = function () { console.log(`Bye ${this.name}.`) }gary.hello() // Bye Gary. alice.hello() // Bye Alice.
如結果所示,當物件原型做修改時,所有原型鏈上有這個原型的物件,通通都會受到影響,不管物件是在修改前還是修改後建立的。
建議大家除非是 Polyfill,否則應該要極力避免對原生物件的原型進行修改,防止造成可能的意外結果。
看完前面這一大段,是不是覺得心很累?別擔心,從 ES6 開始新增了 Class
語法糖,使開發者體驗提升了很多。下面把前面的例子用 Class
重構一下:
class Person { constructor (name){ this.name = name } // 方法會自動放到 Person.prototype hello() { console.log(`Hello ${this.name}.`) } }class Engineer extends Person { constructor (name, skill){ super(name) // 呼叫 Person 的建構函式 this.skill = skill } }let alice = new Engineer('Alice', 'JavaScript') alice.hello() // Hello Alice.Object.getPrototypeOf(alice) // Person {constructor: ƒ}
很方便,同樣的功能,程式碼的可讀性卻提高了不少,繁瑣的設定也都能交給語法自動幫你完成。不過方便的語法背後,底層仍然是物件原型及原型鏈。
以上是 JavaScript 中關於物件原型的說明,希望能幫你理解物件原型,在這個什麼都是物件的語言中,充分理解並掌握物件原型,是成為專業碼農必須要突破的關卡之一。
更多程式設計相關知識,請存取:!!
以上就是了解JavaScript中的物件原型和原型鏈的詳細內容,更多請關注TW511.COM其它相關文章!