Learning Objectives
- 用物件字面值 { } 建立物件,說明屬性(鍵與值)與方法
- 用點記法與中括號存取、新增、修改與刪除屬性,並分辨兩者的使用時機
- 說明物件是參照:兩個變數可以指向同一個物件
- 用 for…in、Object.keys、Object.values、Object.entries 走訪物件
- 用解構取出屬性,用展開語法複製物件
物件:把相關的資料放在一起
要記錄一位同學的姓名、年齡、信箱,可以宣告三個變數;但同學一多,變數就散落一地,也不知道哪個信箱屬於誰。 物件(object)把描述同一件事物的資料放在一起:
const person = {
name: '王小明', // 屬性:鍵 name,值 '王小明'
age: 20,
speak() { // 方法:值是函式的屬性
return '你好';
},
};
- 每一筆「鍵: 值」叫做一個屬性(property),屬性之間用逗號分隔
- 值可以是任何型別:字串、數字、陣列、另一個物件,或是函式
- 值是函式的屬性叫做方法(method),代表這個物件「能做的事」
JavaScript 裡除了字串、數字、布林值、null、undefined 這些基本型別以外,其他的值都是物件:
陣列、函式、日期(Date)、正規表示法(RegExp)都是。你在 document.querySelector() 用到的點,就是在呼叫 document 物件的方法。
建立物件通常用上面的物件字面值 { };new Object() 也可以,但比較少用。
最後一個屬性後面可以加逗號(現在的瀏覽器都接受,加了之後新增屬性比較方便);很舊的瀏覽器會出錯,所以舊教材會提醒不要加。
存取、新增、修改與刪除屬性
右邊會畫出 person 物件。播放到第 15~17 行時,注意物件上多了 email、age 從 20 變成 21、favorite color 不見了。第 13 行的 person.key 為什麼是 undefined?
點記法與中括號記法
| 寫法 | 例子 | 什麼時候用 |
|---|---|---|
| 點記法(dot notation) | person.name |
屬性名稱是合法的識別字,而且寫程式時就知道名稱 |
| 中括號記法(bracket notation) | person['name']、person[key] |
名稱有空白或特殊字元,或名稱存在變數裡、執行時才知道 |
幾個要注意的地方:
- 屬性的鍵一律是字串(或 Symbol)。
obj[1]和obj['1']是同一個屬性。 - 讀取不存在的屬性得到
undefined,不會出錯;但對undefined再取屬性(person.phone.number)就會出現TypeError。 'phone' in person可以檢查屬性是否存在。- 物件裡可以再放物件,例如
person.address.city,這樣分層的做法也叫命名空間(namespace):把相關的名稱收在同一個物件底下。 - 用
const宣告的物件仍然可以修改屬性:const只保證這個變數不會改指向別的物件,不保證物件的內容不變。
方法與 this
呼叫 counter.add(3) 時,右邊 add 的框架裡有一列 this,它指向的就是 counter。所以 this.count += n 改的是 counter.count。
物件是參照
這是物件最重要、也最容易出錯的觀念。
- 基本型別(數字、字串、布林值…):
let b = a會把值複製一份給b,之後改b不影響a。 - 物件:變數裡放的不是物件本身,而是「指向這個物件的參照」(reference)。
const obj2 = obj1只是讓obj2指向同一個物件, 沒有產生新物件。透過任何一個變數修改,另一個變數看到的都是改過的結果。
=== 比較兩個物件時,比的是「是不是同一個物件」,不是內容。兩個內容一模一樣、但分別建立的物件,=== 的結果是 false。
把物件傳給函式也一樣:函式拿到的是同一個物件,在函式裡修改屬性,外面也看得到。 要真的複製一份物件,請看後面的展開語法,以及第 10 課〈JSON 與複製〉。
兩個變數指向同一個物件
播放到第 9 行時看右邊:obj1 和 obj2 的箭頭指向同一個物件。第 13 行建立的 obj3 是另一個物件,雖然內容和 obj1 一樣。
想一想:屬性與參照
下面的程式會印出什麼?
const car = { brand: 'Toyota', year: 2020 };
const prop = 'brand';
console.log(car[prop], car.prop);
下面的程式會印出什麼?
const a = { n: 1 };
const b = a;
b.n = 2;
console.log(a.n);
下面的程式會印出什麼?
const p = { x: 1 };
const q = { x: 1 };
console.log(p === q, p.x === q.x);
走訪物件:for…in 與 Object.keys/values/entries
| 寫法 | 得到 |
|---|---|
for (const key in obj) |
每一圈拿到一個屬性名稱,要用 obj[key] 取值 |
Object.keys(obj) |
所有屬性名稱組成的陣列 |
Object.values(obj) |
所有屬性值組成的陣列 |
Object.entries(obj) |
[鍵, 值] 配對組成的陣列,常搭配 for…of 與解構 |
for…in 和 for…of 不一樣:
for…in走訪屬性名稱(鍵),一般物件、陣列都可以用;用在陣列上拿到的是索引字串'0'、'1'…for…of走訪值,只能用在可以逐一取值(iterable)的東西:陣列、字串、Map、Set…一般物件不行,會出現TypeError: … is not iterable
for…in 還會走訪從原型(prototype)繼承來的可列舉屬性(原型在第 27 課說明)。
Object.keys 等方法只看物件自己的屬性,所以現在比較常用 Object.keys/entries 搭配 for…of。
需要用 for…in 又只想要自己的屬性時,舊程式會用 obj.hasOwnProperty(key) 過濾。
走訪陣列請用一般的 for 或 for…of,不要用 for…in(下一課〈陣列〉會細講)。
用 for…in 與 Object.entries 走訪成績
第 4 行的 subject 每一圈是一個屬性名稱。第 11 行的 Object.entries 把物件變成 [鍵, 值] 的陣列,每一圈直接解構成 subject 和 score 兩個變數。最後一段示範對一般物件用 for…of 會出錯。
解構與展開
解構(destructuring)一次把好幾個屬性取出來,存成同名的變數:
const { title, price } = book; // 等於 const title = book.title; const price = book.price;
const { price: cost } = book; // 取出 price,存到變數 cost
const { stock = 0 } = book; // book 沒有 stock 時,用預設值 0
展開(spread)... 把一個物件的屬性一一複製到新的物件字面值裡:
const copy = { ...book }; // 新物件,屬性和 book 一樣
const sale = { ...book, price: 360 }; // 先複製,再用後面的 price 蓋過去
展開建立的是新物件,所以 copy === book 是 false。但它只複製一層:如果屬性值本身是物件(例如 book.author),
新物件裡放的是同一個內層物件的參照。這叫淺複製,第 10 課會再比較淺複製與深複製。
解構與展開
看第 10 行執行完的右邊:sale 是新的物件,但 sale.author 和 book.author 的箭頭指向同一個 {name: 'Amy'}。
想一想:走訪、解構與展開
下面的程式會印出什麼?
const user = { name: 'Amy', age: 20 };
const keys = [];
for (const k in user) {
keys.push(k);
}
console.log(keys.join(','));
下面的程式會印出什麼?
const item = { name: '筆', price: 30 };
const { name, qty = 1 } = item;
console.log(name, qty);
下面的程式會印出什麼?
const a = { x: 1, tags: ['js'] };
const b = { ...a, x: 2 };
b.tags.push('css');
console.log(a.x, a.tags.length);
實作:購物車的總金額
cart 物件記錄每樣商品的價格。請走訪 cart(用 for…in 或 Object.entries 都可以),算出總金額,並找出最貴的商品名稱。
重點精華回顧
- 物件把相關的資料放在一起:每一筆「鍵: 值」是一個屬性,值是函式的屬性叫做方法。
- 存取屬性用點記法
obj.name或中括號obj['name'];名稱在變數裡或含有空白時用中括號。 - 指定新的屬性就是新增,
delete obj.key刪除,'key' in obj檢查是否存在;讀不存在的屬性得到undefined。 - 方法裡的
this是呼叫它的那個物件(counter.add()的this就是counter)。 - 物件是參照:
const b = a讓兩個變數指向同一個物件;===比的是不是同一個物件。 for…in走訪屬性名稱;Object.keys/values/entries把物件轉成陣列;一般物件不能用for…of。- 解構
const { a, b } = obj一次取出多個屬性;展開{ ...obj }建立新物件,但只複製一層。