2022 年 6 月 19 日

物件

正如我們在 資料類型 章節中所知,JavaScript 中有八種資料類型。其中七種被稱為「基本型別」,因為它們的值只包含單一項目(無論是字串、數字或其他任何東西)。

相反地,物件用於儲存各種資料和更複雜實體的鍵值集合。在 JavaScript 中,物件幾乎滲透到語言的每個面向。因此,在深入探討其他任何內容之前,我們必須先了解它們。

可以使用大括號 {…} 來建立物件,並附上一個選用的 屬性 清單。屬性是一個「金鑰:值」配對,其中 金鑰 是字串(也稱為「屬性名稱」),而 可以是任何東西。

我們可以將物件想像成一個裝有簽名檔案的櫃子。每筆資料都以金鑰儲存在其檔案中。透過名稱可以輕鬆找到檔案,或新增/移除檔案。

可以使用兩種語法建立一個空物件(「空櫃子」)

let user = new Object(); // "object constructor" syntax
let user = {};  // "object literal" syntax

通常使用大括號 {...}。該宣告稱為物件文字

文字和屬性

我們可以立即將一些屬性放入 {...} 中,作為「鍵:值」對

let user = {     // an object
  name: "John",  // by key "name" store value "John"
  age: 30        // by key "age" store value 30
};

屬性在冒號 ":" 之前有一個鍵(也稱為「名稱」或「識別碼」),並且在冒號的右側有一個值。

user 物件中,有兩個屬性

  1. 第一個屬性的名稱為 "name",值為 "John"
  2. 第二個屬性的名稱為 "age",值為 30

產生的 user 物件可以想像成一個櫃子,上面貼有兩個標籤為「名稱」和「年齡」的簽名檔案。

我們可以隨時新增、移除和讀取其中的檔案。

可以使用點號表示法存取屬性值

// get property values of the object:
alert( user.name ); // John
alert( user.age ); // 30

值可以是任何類型。我們來新增一個布林值

user.isAdmin = true;

若要移除屬性,可以使用 delete 算子

delete user.age;

我們也可以使用多字元屬性名稱,但必須加上引號

let user = {
  name: "John",
  age: 30,
  "likes birds": true  // multiword property name must be quoted
};

清單中的最後一個屬性可以用逗號結尾

let user = {
  name: "John",
  age: 30,
}

這稱為「尾隨」或「懸掛」逗號。可以更輕鬆地新增/移除/移動屬性,因為所有行都變得相同。

方括號

對於多字元屬性,點號存取無法運作

// this would give a syntax error
user.likes birds = true

JavaScript 無法理解。它認為我們要處理 user.likes,然後在遇到意外的 birds 時會產生語法錯誤。

點號需要鍵為有效的變數識別碼。這表示:不包含空格,不以數字開頭,不包含特殊字元(允許使用 $_)。

有一個替代的「方括號表示法」,可搭配任何字串運作

let user = {};

// set
user["likes birds"] = true;

// get
alert(user["likes birds"]); // true

// delete
delete user["likes birds"];

現在一切都很好。請注意,括號內的字串有加上適當的引號(任何類型的引號都可以)。

方括號也提供一種方法,可以取得屬性名稱作為任何表達式的結果,而不是文字字串,例如從變數取得,如下所示

let key = "likes birds";

// same as user["likes birds"] = true;
user[key] = true;

在此,變數 key 可以在執行階段計算或取決於使用者輸入。然後我們使用它來存取屬性。這讓我們有很大的彈性。

例如

let user = {
  name: "John",
  age: 30
};

let key = prompt("What do you want to know about the user?", "name");

// access by variable
alert( user[key] ); // John (if enter "name")

點號表示法無法以類似的方式使用

let user = {
  name: "John",
  age: 30
};

let key = "name";
alert( user.key ) // undefined

計算屬性

建立物件時,我們可以在物件文字中使用方括號。這稱為計算屬性

例如

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // the name of the property is taken from the variable fruit
};

alert( bag.apple ); // 5 if fruit="apple"

計算屬性的意義很簡單:[fruit] 表示屬性名稱應該從 fruit 取得。

因此,如果訪客輸入 "apple"bag 將會變成 {apple: 5}

基本上,這與下列運作方式相同

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// take property name from the fruit variable
bag[fruit] = 5;

…但看起來比較漂亮。

我們可以在方括號中使用更複雜的表達式

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

方括號比點號表示法強大許多。它們允許使用任何屬性名稱和變數。但它們的寫法也比較繁瑣。

因此,在大部分情況下,當屬性名稱已知且簡單時,會使用點號。如果我們需要更複雜的內容,則會改用方括號。

屬性值簡寫

在實際程式碼中,我們經常使用現有變數作為屬性名稱的值。

例如

function makeUser(name, age) {
  return {
    name: name,
    age: age,
    // ...other properties
  };
}

let user = makeUser("John", 30);
alert(user.name); // John

在上面的範例中,屬性與變數具有相同的名稱。從變數建立屬性的使用案例非常常見,因此有一個特殊的屬性值簡寫可以讓它更簡短。

我們可以只寫 name,取代 name:name,如下所示

function makeUser(name, age) {
  return {
    name, // same as name: name
    age,  // same as age: age
    // ...
  };
}

我們可以在同一個物件中同時使用一般屬性和簡寫

let user = {
  name,  // same as name:name
  age: 30
};

屬性名稱限制

正如我們所知,變數不能使用語言保留字作為名稱,例如「for」、「let」、「return」等。

但對於物件屬性,沒有這樣的限制

// these properties are all right
let obj = {
  for: 1,
  let: 2,
  return: 3
};

alert( obj.for + obj.let + obj.return );  // 6

簡而言之,屬性名稱沒有限制。它們可以是任何字串或符號(一種特殊的識別符號類型,稍後會討論)。

其他類型會自動轉換為字串。

例如,數字 0 在用作屬性鍵時會變成字串 "0"

let obj = {
  0: "test" // same as "0": "test"
};

// both alerts access the same property (the number 0 is converted to string "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (same property)

有一個名為 __proto__ 的特殊屬性有一個小問題。我們無法將它設定為非物件值

let obj = {};
obj.__proto__ = 5; // assign a number
alert(obj.__proto__); // [object Object] - the value is an object, didn't work as intended

從程式碼中可以看到,指派給基本型別 5 會被忽略。

我們將在 後續章節 中討論 __proto__ 的特殊性質,並建議 修復 此類行為的方法。

屬性存在測試,「in」運算子

與許多其他語言相比,JavaScript 中物件的一個顯著特點是,可以存取任何屬性。如果屬性不存在,不會產生錯誤!

讀取不存在的屬性只會傳回 undefined。因此,我們可以輕鬆測試屬性是否存在

let user = {};

alert( user.noSuchProperty === undefined ); // true means "no such property"

還有一個用於此目的的特殊運算子 "in"

語法為

"key" in object

例如

let user = { name: "John", age: 30 };

alert( "age" in user ); // true, user.age exists
alert( "blabla" in user ); // false, user.blabla doesn't exist

請注意,in 的左側必須是屬性名稱。這通常是一個帶引號的字串。

如果我們省略引號,表示變數應包含要測試的實際名稱。例如

let user = { age: 30 };

let key = "age";
alert( key in user ); // true, property "age" exists

in 運算子為何存在?與 undefined 進行比較不就夠了嗎?

嗯,大多數時候與 undefined 進行比較都能正常運作。但有一個特殊情況會失敗,但 "in" 卻能正確運作。

這是當物件屬性存在,但儲存 undefined

let obj = {
  test: undefined
};

alert( obj.test ); // it's undefined, so - no such property?

alert( "test" in obj ); // true, the property does exist!

在上述程式碼中,屬性 obj.test 在技術上存在。因此,in 運算子運作正確。

像這樣的狀況很少發生,因為不應明確指定 undefined。我們大多使用 null 表示「未知」或「空」值。因此,in 運算子在程式碼中是一個罕見的訪客。

「for..in」迴圈

若要遍歷物件的所有鍵,有一個迴圈的特殊形式:for..in。這與我們之前研究過的 for(;;) 結構完全不同。

語法

for (key in object) {
  // executes the body for each key among object properties
}

例如,讓我們輸出 user 的所有屬性

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // values for the keys
  alert( user[key] ); // John, 30, true
}

請注意,所有「for」結構都允許我們在迴圈內宣告迴圈變數,例如此處的 let key

此外,我們可以在這裡使用 key 以外的另一個變數名稱。例如,"for (let prop in obj)" 也廣泛使用。

像物件一樣排序

物件是有序的嗎?換句話說,如果我們遍歷一個物件,我們會以新增順序取得所有屬性嗎?我們可以依賴這個嗎?

簡短的答案是:「以特殊方式排序」:整數屬性已排序,其他屬性則按建立順序顯示。詳細資訊如下。

舉例來說,我們考慮一個包含電話代碼的物件

let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for (let code in codes) {
  alert(code); // 1, 41, 44, 49
}

物件可用於向使用者建議選項清單。如果我們製作一個主要針對德語受眾的網站,那麼我們可能會希望 49 是第一個。

但是,如果我們執行程式碼,我們會看到完全不同的畫面

  • 美國 (1) 排在第一個
  • 然後是瑞士 (41) 等等。

電話區碼會以遞增排序,因為它們是整數。因此,我們會看到 1, 41, 44, 49

整數屬性?那是什麼?

此處的「整數屬性」一詞表示一個字串,可以不變更地轉換為整數或從整數轉換回來。

因此,"49" 是整數屬性名稱,因為當它轉換為整數數字並返回時,它仍然相同。但是 "+49""1.2" 則不是

// Number(...) explicitly converts to a number
// Math.trunc is a built-in function that removes the decimal part
alert( String(Math.trunc(Number("49"))) ); // "49", same, integer property
alert( String(Math.trunc(Number("+49"))) ); // "49", not same "+49" ⇒ not integer property
alert( String(Math.trunc(Number("1.2"))) ); // "1", not same "1.2" ⇒ not integer property

…另一方面,如果鍵是非整數,則它們會按建立順序列出,例如

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // add one more

// non-integer properties are listed in the creation order
for (let prop in user) {
  alert( prop ); // name, surname, age
}

因此,要修正電話區碼的問題,我們可以透過讓區碼成為非整數來「作弊」。在每個區碼之前加上加號 "+" 就足夠了。

像這樣

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA"
};

for (let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

現在它可以按預期運作。

摘要

物件是具有多個特殊功能的關聯式陣列。

它們儲存屬性(鍵值對),其中

  • 屬性鍵必須是字串或符號(通常是字串)。
  • 值可以是任何類型。

要存取屬性,我們可以使用

  • 點表示法:obj.property
  • 方括號表示法 obj["property"]。方括號允許從變數中取得鍵,例如 obj[varWithKey]

其他運算子

  • 要刪除屬性:delete obj.prop
  • 要檢查具有給定鍵的屬性是否存在:"key" in obj
  • 要反覆運算物件:for (let key in obj) 迴圈。

我們在本章節中所學習的內容稱為「純粹物件」,或僅稱為 Object

JavaScript 中還有許多其他類型的物件

  • Array 用於儲存已排序的資料集合,
  • Date 用於儲存日期和時間的資訊,
  • Error 用於儲存錯誤的資訊。
  • …等等。

它們有我們稍後會學習的特殊功能。有時候人們會說「陣列類型」或「日期類型」,但正式來說它們不是自己的類型,而是屬於單一的「物件」資料類型。而且它們會以各種方式擴充它。

JavaScript 中的物件非常強大。我們在此僅觸及了一個非常龐大的主題的表面。我們將在教學課程的後續部分與物件密切合作,並進一步了解它們。

作業

重要性:5

寫程式碼,每一個動作一行

  1. 建立一個空的物件 user
  2. 加入屬性 name 值為 John
  3. 加入屬性 surname 值為 Smith
  4. 變更 name 的值為 Pete
  5. 從物件中移除 name 屬性。
let user = {};
user.name = "John";
user.surname = "Smith";
user.name = "Pete";
delete user.name;
重要性:5

寫一個函式 isEmpty(obj),如果物件沒有屬性,回傳 true,否則回傳 false

應該像這樣運作

let schedule = {};

alert( isEmpty(schedule) ); // true

schedule["8:30"] = "get up";

alert( isEmpty(schedule) ); // false

開啟一個沙盒進行測試。

只要迴圈物件,如果至少有一個屬性,就立即 return false

function isEmpty(obj) {
  for (let key in obj) {
    // if the loop has started, there is a property
    return false;
  }
  return true;
}

在沙盒中開啟包含測試的解答。

重要性:5

我們有一個物件儲存團隊的薪資

let salaries = {
  John: 100,
  Ann: 160,
  Pete: 130
}

寫一個程式碼加總所有薪資,並儲存在變數 sum 中。以上面的範例來說,應該是 390

如果 salaries 為空,則結果必須為 0

let salaries = {
  John: 100,
  Ann: 160,
  Pete: 130
};

let sum = 0;
for (let key in salaries) {
  sum += salaries[key];
}

alert(sum); // 390
重要性:3

建立一個函式 multiplyNumeric(obj),將 obj 的所有數字屬性值乘以 2

例如

// before the call
let menu = {
  width: 200,
  height: 300,
  title: "My menu"
};

multiplyNumeric(menu);

// after the call
menu = {
  width: 400,
  height: 600,
  title: "My menu"
};

請注意 multiplyNumeric 不需要回傳任何東西。它應該就地修改物件。

補充:在此使用 typeof 來檢查數字。

開啟一個沙盒進行測試。

function multiplyNumeric(obj) {
  for (let key in obj) {
    if (typeof obj[key] == 'number') {
      obj[key] *= 2;
    }
  }
}

在沙盒中開啟包含測試的解答。

教學地圖

留言

留言前請先閱讀…
  • 如果你有改善建議,請 提交 GitHub 問題 或發起拉取請求,而不是留言。
  • 如果你看不懂文章中的某個部分,請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,要插入多行,請將它們包在 <pre> 標籤中,要插入超過 10 行,請使用沙盒 (plnkrjsbincodepen…)