Learning Objectives
- 用物件當作命名空間,並說明 JavaScript 的函式也是一種物件
- 用建構函式與 new 建立多個同類型的物件,說明 new 做了哪四件事
- 把共用的方法放在 prototype 上,說明原型鏈如何查找屬性與方法
- 用 Object.create 指定原型、用 hasOwnProperty 分辨自己的屬性,並說明為什麼不要修改內建物件的 prototype
物件、命名空間與「函式也是物件」
物件是一群「名稱:值」的組合:用屬性描述它的特徵(名字、年齡),用方法(值是函式的屬性)描述它能做的動作。
建立單一物件有兩種寫法:
const dog = new Object(); // 用 Object 建構函式建立空物件,再逐一加上屬性
dog.name = 'Lucky';
dog['age'] = 3;
const cat = { name: 'Mimi', age: 2 }; // 物件實字(object literal),最常用
命名空間(namespace):JavaScript 沒有內建命名空間,但可以把相關的變數與函式都放進同一個物件,例如 MyApp.version、MyApp.greet()。
這樣全域只多了一個名稱 MyApp,比較不會和別人的程式撞名。(現在更常用的做法是 ES 模組,第 29 課會介紹。)
另外,JavaScript 的函式也是物件:typeof 雖然顯示 function,但它一樣可以有自己的屬性,例如 add.length 是參數的個數。
這一點是理解 prototype 的基礎:每個函式都有一個叫 prototype 的屬性。
命名空間物件與函式的屬性
看右邊的 MyApp:version 與 greet 都是它的屬性,greet 的值是一個函式。第 11 行替函式 add 加了一個屬性,第 14 行印出 typeof add 是 function,add.length 是 2。
建構函式與 new
要建立很多個「同一種」物件(很多隻狗),每次都寫物件實字太麻煩,可以寫一個建構函式(constructor)當作物件產生器:
function Dog(name, age) { // 慣例:建構函式名稱首字大寫
this.name = name;
this.age = age;
}
const d1 = new Dog('Lucky', 3);
用 new 呼叫函式時,JavaScript 會做四件事:
- 建立一個新的空物件
{} - 把這個物件的原型(
[[Prototype]])設成Dog.prototype - 執行
Dog函式,函式裡的this指向這個新物件 - 函式沒有
return物件時,自動回傳這個新物件
忘了寫 new,Dog('Lucky', 3) 就只是普通的函式呼叫:沒有新物件,結果是 undefined,this 也不會指向你想要的物件。
下面的範例把方法 bark 也寫在建構函式裡,注意最後一行:兩隻狗的 bark 不是同一個函式。
用建構函式建立兩隻狗
播放到第 3 行時看右邊的 this:它是 new 剛建立的空物件,執行完建構函式後才有了 name、age、bark。右邊會顯示每個物件的 [[Prototype]](原型),兩隻狗都指向 Dog.prototype。最後一行印出 false:每建立一隻狗,就建立了一個新的 bark 函式。
prototype:讓所有實例共用方法
每個函式都有一個 prototype 屬性,一開始是一個幾乎空的物件。用 new 建立的物件,原型會指向這個物件,
所以放在 Dog.prototype 上的方法,所有的狗都能用,而且共用同一個函式:
function Dog(name) {
this.name = name; // 每隻狗不一樣的資料:放在 this 上
}
Dog.prototype.bark = function () { // 大家都一樣的方法:放在 prototype 上
return this.name + ':汪!';
};
兩個名稱容易搞混:
| 名稱 | 在誰身上 | 意思 |
|---|---|---|
Dog.prototype |
建構函式 | 「將來用 new Dog 建立的物件」的原型 |
[[Prototype]] |
每個物件 | 這個物件的原型;用 Object.getPrototypeOf(obj) 讀取 |
投影片寫的 obj.__proto__ 也能讀到物件的原型,但這是舊的寫法,現在建議用 Object.getPrototypeOf()。
修改 prototype 的效果是全面的:之後才加到 Dog.prototype 的方法,已經建立好的狗也馬上可以用。
方法放在 prototype 上
這次狗的身上只有 name,展開右邊的 [[Prototype]] 才看得到 bark。第 14 行印出 true:兩隻狗共用同一個函式。第 17 行在兩隻狗都建立之後才加上 sit,d2.sit() 照樣可以呼叫。
原型鏈:屬性是怎麼找到的
讀取 obj.屬性 時,JavaScript 依序找:
- 先找物件自己身上的屬性
- 找不到,就到它的原型(
[[Prototype]])找 - 還找不到,再到原型的原型找……這一串就是原型鏈(prototype chain)
- 一路找到
Object.prototype(所有一般物件的原型源頭),它的原型是null;還是找不到,結果就是undefined
所以 lucky.toString() 找得到:toString 定義在 Object.prototype 上。自己身上有同名屬性時,就不再往上找,會遮住原型上的值。
| 檢查方式 | 意思 |
|---|---|
obj.hasOwnProperty('legs') |
是不是自己身上的屬性(不看原型);新寫法 Object.hasOwn(obj, 'legs') |
'legs' in obj |
自己身上或原型鏈上有沒有這個屬性 |
obj instanceof Dog |
Dog.prototype 在不在 obj 的原型鏈上 |
自己的屬性會遮住原型上的屬性
Dog.prototype.legs 是 4,但第 12 行替 tripod 自己加了 legs = 3。執行 describe 時,this.legs 在 lucky 身上找不到,往原型找到 4;在 tripod 身上直接找到 3。最後讀一個原型鏈上都沒有的 fly,得到 undefined。
Object.create:直接指定原型
不一定要用建構函式。Object.create(proto) 會建立一個新物件,並把它的原型直接設成 proto:
const animal = { speak() { return this.name + ' 發出了聲音'; } };
const bird = Object.create(animal); // bird 的原型是 animal
這就是 JavaScript「以原型為基礎」(prototype-based)的物件導向:物件直接從另一個物件繼承,不需要類別。
Object.create(null) 會建立一個沒有原型的物件,連 toString 都沒有,有時用來當作單純的對照表。
三層原型鏈:parrot → bird → animal
展開右邊 parrot 的 [[Prototype]]:第一層是 bird(有 fly),再往上一層是 animal(有 speak)。parrot.speak() 往上找了兩層,但 this 仍然是 parrot,所以印出「阿鸚」。
可以擴展內建物件的 prototype 嗎?
技術上可以:Array.prototype.last = function () { … } 之後,頁面上每一個陣列都多了 last 方法。投影片介紹「擴展類型功能」時就是這個原理。
但實務上不建議修改內建物件(Array、String、Object 等)的 prototype:
- 影響全部:頁面上所有程式(包含別人寫的程式庫)用到的陣列都被改了,出錯時很難追查
- 和別人衝突:兩個程式庫都加了同名但做法不同的方法,後載入的會蓋掉先載入的
- 和未來的標準衝突:曾有舊程式庫在
Array.prototype加了自己的flatten,2018 年標準要新增同名方法時,發現會讓使用那個程式庫的網站壞掉,最後新方法只好改名為flat for…in走訪陣列時,會連自己加上去的屬性一起列出來
修改自己寫的建構函式的 prototype(例如 Dog.prototype)沒有這些問題。想替內建型別加功能時,改寫成一般函式(last(arr))或用下一課的 class … extends Array。
想一想:建構函式與 new
下面的程式會印出什麼?
function hello() {
return 'hi';
}
hello.count = 3;
console.log(typeof hello, hello.count);
下面的程式會印出什麼?
function Dog(nick) {
this.nick = nick;
}
const a = Dog('Lucky');
const b = new Dog('Coco');
console.log(a, b.nick);
執行 new Dog('Lucky') 時,下列哪一件事不會發生?
想一想:prototype 與原型鏈
下面的程式會印出什麼?
function Cat(name) {
this.name = name;
}
const a = new Cat('Mimi');
Cat.prototype.meow = function () {
return this.name + ':喵';
};
console.log(a.meow());
下面的程式會印出什麼?
const base = { wheels: 4 };
const bike = Object.create(base);
bike.wheels = 2;
delete bike.wheels;
console.log(bike.wheels, bike.hasOwnProperty('wheels'));
為什麼不建議在 Array.prototype 上加自己的方法?
實作:替 Employee 加上共用的方法
在 Employee.prototype 上加兩個方法:getDepartment() 回傳部門,introduce() 回傳像 Ben 在 行銷部 的字串。最後一行要印出 true:兩位員工共用同一個方法。
重點精華回顧
- 物件由屬性與方法組成,可以用物件實字或
new Object()建立;把相關的程式放進一個物件,就是簡單的命名空間。 - 函式也是物件,可以有自己的屬性;每個函式都有
prototype屬性。 - 建構函式搭配
new:建立新物件 → 原型設成建構函式.prototype→ 執行建構函式(this 指向新物件)→ 回傳新物件。 - 每個實例不同的資料放在
this上,共用的方法放在prototype上,所有實例共用同一個函式。 - 讀取屬性時沿著原型鏈往上找,找到就停,找不到是
undefined;hasOwnProperty只看自己,in連原型鏈一起看。 Object.create(proto)直接指定新物件的原型;Object.getPrototypeOf(obj)讀取原型。- 原型的變動會影響所有實例;不要修改內建物件的 prototype。