本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
在上篇文章當中,我們介紹了 JavaScript 物件的繼承關係。
由於 JavaScript 沒有內建 class 的概念,而是透過「原型」的關係,使物件得以繼承自另外一個物件,那麼這個被繼承的物件我們就稱之為「原型」 (prototype)。
當函式被建立的時候,都會有個原型物件 prototype
。 透過擴充這個 prototype
的物件,就能讓每一個透過這個函式建構的物件都擁有這個「屬性」或「方法」:
var Person = function(name){
this.name = name;
};
// 在 Person.prototype 新增 sayHello 方法
Person.prototype.sayHello = function(){
return "Hi, I'm " + this.name;
}
var p = new Person('Kuro');
p.sayHello(); // "Hi, I'm Kuro"
當我們透過 new
建構一個 Person
的實體時,以上面範例來說就是物件 p
,這個物件 p
的原型物件會自動指向建構式的 prototype
屬性,也就是 Person.prototype
。
像上面這張圖,就是我們在上一篇曾介紹過的「原型鏈」的概念。
而透過「原型」來新增方法 (method),其實是非常實用的概念,而且是在原型新增後馬上就可以用:
var Person = function(name){
this.name = name;
};
var p = new Person('Kuro');
p.sayHelloWorld(); // TypeError: p.sayHelloWorld is not a function
Person.prototype.sayHelloWorld = function(){
return "Hello, World!";
}
p.sayHelloWorld(); // "Hello, World!"
像這樣,物件 p
與他的原型鏈上的所有物件原本都沒有 sayHelloWorld()
方法,但我們透過 Person.prototype.sayHelloWorld
新增了對應的方法後,我們無需重新建置物件 p
,馬上就可以透過 p.sayHelloWorld()
來呼叫。
像這種手法,也是很多「Polyfill」用來增強擴充那些舊版本瀏覽器不支援的語法。 如 Array.prototype.find()
在 ES6 以前是不存在的,但我們可以透過檢查 Array.prototype.find
是否存在。
如果不存在就可以對 Array.prototype
新增 find
方法,然後就可以直接使用。
if (!Array.prototype.find) {
Array.prototype.find = function(predicate) {
if (this === null) {
throw new TypeError('Array.prototype.find called on null or undefined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;
var thisArg = arguments[1];
var value;
for (var i = 0; i < length; i++) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return value;
}
}
return undefined;
};
}
參考來源: MDN: Array.prototype.find()
__proto__
與 prototype
的關係?介紹原型的時候我們曾經提過,在 JavaScript 每一個物件都會有它的原型物件 [[prototype]]
。
在過去,雖然 JavaScript 沒有提供標準方法讓我們直接對原型物件 [[prototype]]
來進行存取,不過幸運的是,大多數的瀏覽器 (精準一點說,大多數的 JavaScript 引擎) 都有提供一種叫做 __proto__
的特殊屬性,來讓我們取得某個物件的原型物件。 要注意的是,並非所有 JavaScript 的執行環境都支援 __proto__
,而瀏覽器之間對 __proto__
的支援也並非完全相容。 [註1]
好消息是,自從 ES5 開始,如果我們想要取得某個物件的原型物件時,就可以透過 Object.getPrototypeOf( )
這個標準方法:
var Person = function(name){
this.name = name;
};
var p = new Person('Kuro');
// 在 Person.prototype 新增 sayHello 方法
Person.prototype.sayHello = function(){
return "Hi, I'm " + this.name;
}
// 所以 p 也可以呼叫 sayHello 方法
console.log( p.sayHello() ); // "Hi, I'm Kuro"
console.log(Object.getPrototypeOf( p ) === Person.prototype); // true
console.log(Object.getPrototypeOf( p ) === Function.prototype); // false
console.log(Object.getPrototypeOf( Person ) === Function.prototype); // true
console.log( p.__proto__ === Person.prototype ); // true
console.log( p.__proto__ === Function.prototype ); // false
console.log( Person.__proto__ === Function.prototype ); // true
所以簡單來說,不管是 __proto__
這個特殊屬性或者是 Object.getPrototypeOf( )
其實都是取得某個物件的原型物件 [[prototype]]
的方式。
所以,現在我們知道了 __proto__
其實是順著原型鏈向上取得原型物件的特殊屬性,那麼 prototype
呢?
前面說過,「每一個函式被建立之後,都會自動產生一個 prototype
的屬性」,但這並 "不" 代表這個 prototype
屬性就是這個函式的原型物件,而是透過 new
這個函式「建構」出來的物件會有個 [[prototype]]
的隱藏屬性,會指向建構函式的 prototype
屬性。
這也是大家在理解「原型」時最容易搞混的地方。
前面說的都是「建構式」與「原型」的關係,那麼如果我們要透過「物件」來達到原型繼承的話可以怎麼做?
第一個是透過昨天介紹的 Object.setPrototypeOf()
。
像這樣,將 cutman
指定為 rockman
的原型物件:
Object.setPrototypeOf(rockman, cutman);
這裡就不重複說明,可以去看昨天的文章。
第二種,我們可以透過 Object.create()
。
// Person 物件
var Person = {
name: 'Default_Name',
sayHello: function(){
return "Hi, I'm " + this.name;
}
};
// 透過 Object.create() 將 Person 作為原型物件來建立一個新的物件
var p = Object.create(Person);
p.sayHello(); // "Hi, I'm Default_Name"
p.name = 'Kuro';
p.sayHello(); // "Hi, I'm Kuro"
像這樣,我們可以先建立一個物件作為「原型」,然後透過 Object.create()
來產生一個新的物件,此時新物件的 [[prototype]]
就會是我們所指定的那個原型物件。
Object.create()
實作的原理簡單來說就像這樣:
Object.create = function (proto){
function F() {}
F.prototype = proto;
return new F();
}
當我們把原型物件作為參數傳入 proto
, Object.create()
會回傳一個 new F()
,也就是透過一個封裝過的建構式建構出來的物件,並把 prototype
指向作為參數的 proto
。
所以看到這裡,現在我們可以簡單歸納 __proto__
與 prototype
的關係:
以 JavaScript 的內建物件 (build-in object) 來說,像是 Array
、Function
...等,它們的 prototype
屬性也是一個物件,實際上是繼承自 Object.prototype
而來。
所以我們做個簡單測試:
console.log( Object.getPrototypeOf(Function.prototype
) === Object.prototype ); // true
// 或是透過 __proto__
console.log( Function.prototype.__proto__ === Object.prototype ); // true
像上面這樣,可以看出在原型鏈內下層物件的 __proto__
屬性或是透過 Object.getPrototypeOf()
取得的物件,實際上會指向上層物件的 prototype
屬性,就可以看出它們之間的繼承關係。
我們花了兩天文章在解釋的東西,大致上可以用這張圖來表示 (原型鏈內物件彼此之間的關係)
最後跟大家分享一則有趣的 推特
相信在理解了 JavaScript 原型鏈的繼承原理之後,看了這則推特應該是哈哈大笑而不是大罵花惹發了吧 XD
如同我曾在 Day3 結尾說的,
雖然這樣都是合法的,但為了身心健康,請不要惡搞你的程式碼
當然同事的也不要! XD
__proto__
屬性在語言規範中視作一個標準屬性,但考慮到過去瀏覽器的實作不一,仍建議透過 Object.getPrototypeOf()
來取得原型物件。第1張圖p右邊的prototype是誤打嗎?實測後p.__proto__才會有值
依我的理解是這樣,供參考:
p 的 prototype,用程式碼表示:p.__proto__
p 的 prototype 屬性,用程式碼表示:p.prototype
我的意思是,直接在chrome,打sm.prototype會是undefined
function Person(height) {
this.height = height;
}
function SuperMan(height, power) {
Person.call(this, height);
this.power = power;
}
SuperMan.prototype = Object.create(Person.prototype);
SuperMan.prototype.constructor = SuperMan;
let p = new Person(178);
let sm = new SuperMan(178, 500);
不過我剛看到筆者最後有更新
每個平台實作方式可能不一樣
薦議用Object.getPrototypeOf
// 可以Work
Object.getPrototypeOf(sm)