Home
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

4

Local AI

--

Uedu Code

--

Taipei AQI 57 · Taichung AQI 74 · Tainan AQI 63 · Kaohsiung AQI 64
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 27

物件導向:prototype

建構函式、prototype 與原型鏈
Interaction實作 20 minute(s)

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 會做四件事:

  1. 建立一個新的空物件 {}
  2. 把這個物件的原型([[Prototype]])設成 Dog.prototype
  3. 執行 Dog 函式,函式裡的 this 指向這個新物件
  4. 函式沒有 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 依序找:

  1. 先找物件自己身上的屬性
  2. 找不到,就到它的原型([[Prototype]])找
  3. 還找不到,再到原型的原型找……這一串就是原型鏈(prototype chain)
  4. 一路找到 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

1.

下面的程式會印出什麼?

function hello() {
  return 'hi';
}
hello.count = 3;
console.log(typeof hello, hello.count);
2.

下面的程式會印出什麼?

function Dog(nick) {
  this.nick = nick;
}
const a = Dog('Lucky');
const b = new Dog('Coco');
console.log(a, b.nick);
3.

執行 new Dog('Lucky') 時,下列哪一件事不會發生?

第 1 次作答

想一想:prototype 與原型鏈

1.

下面的程式會印出什麼?

function Cat(name) {
  this.name = name;
}
const a = new Cat('Mimi');
Cat.prototype.meow = function () {
  return this.name + ':喵';
};
console.log(a.meow());
2.

下面的程式會印出什麼?

const base = { wheels: 4 };
const bike = Object.create(base);
bike.wheels = 2;
delete bike.wheels;
console.log(bike.wheels, bike.hasOwnProperty('wheels'));
3.

為什麼不建議在 Array.prototype 上加自己的方法?

第 1 次作答

實作:替 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。
這一課完成了!
Next lesson:物件導向:class
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。