JavaScript 大全 - 第六章:物件
===
:::info
## Attribute vs Property
Attribute 和 Property 都被翻譯成「屬性」,但兩者是不同的東西。
### Attribute (屬性)
- 是標記語言的概念 (例如:HTML),而標記語言本身是一種 text,所以 attribute 這種 text 描述的性質在標記語言中常被使用
- 需要被長期保存的東西會用文字描述,
### Property (特性)
- 儲存在記憶體,而記憶體會隨著程序執行結束被釋放,無法長期儲存
在 JavaScript 中,DOM 物件通常都是多重繼承,同時繼承了 HTML 和 JavaScript 的 Object:
- JavaScript 的 Object 是記憶體物件,所以是使用 property
- HTML 本身就是標記語言,所以是使用 attribute
當這兩個東西都被繼承至同一個物件上時,會讓人混淆。這是因為有些常用的 attribute,例如:`id`、`class` 等,所以 DOM 把它們 map 到 property 上以方便使用。因此,一個物件就會同時有 `id` 這個 attribute 和 property(在 JavaScript 中,`class` 是保留字,所以它被 map 到 property 上時就變成 `className` )。
資料來源:
- [DOM 系列:Attribute 和 Property | w3cplus](https://www.w3cplus.com/javascript/dom-attributes-and-properties.html)
- [DOM 系列:Attribute 和 Property - mystonelxj 的博客 - CSDN 博客](https://blog.csdn.net/mystonelxj/article/details/87926748)
:::
:::info
## Invoke vs Call
- Invoke (調用):自動被呼叫並執行
- Call (呼叫):手動呼叫並執行
> Function calling is when you call a function yourself in a program.
> 當你在程序中自己呼叫函數時會呼叫函數。
> While function invoking is when it gets called automatically.
> 而函數調用是在函數被自動呼叫時
例如:建立物件時:
- 會自動執行 constructor,這就是 invoke
- 手動執行 `person.call()` 函數,這就是 call
```javascript
class Person {
constructor() {
console.log('invoke');
}
call() {
console.log('call');
}
}
var person = new Person(); // invoke
person.call(); // call
```
> The code inside a function is not executed when the function is defined.
> 定義函數時,不會執行函數內部的程式碼。
> The code inside a function is executed when the function is invoked.
> 函數被 invoke 時,會執行函數內部的程式碼。
> It is common to use the term "call a function" instead of "invoke a function".
> 常用 term 是「call 函數」而不是「invoke 函數」。
> It is also common to say "call upon a function", "start a function", or "execute a function".
> 也常說成「call 函數」、「啟動函數」或「執行函數」
> A JavaScript function can be invoked without being called.
> JavaScript 函數不需要被 call,也能 invoked。
>
> -[JavaScript Function Invocation | W3Schools](https://www.w3schools.com/js/js_function_invocation.asp)
資料來源:
- [筆記本而已: "invoke" 在程式語言中該怎麼翻譯 (定義)](http://lifeiskuso.blogspot.com/2016/10/invoke.html)
- [What is the difference between 'call' and 'invoke'? - Quora](https://www.quora.com/What-is-the-difference-between-call-and-invoke)
:::
:::info
## Argument vs Parameter
- Argument (引數):用於呼叫函數
- Parameter (參數):是方法的宣告
在下面範例中:
- `a` 和 `20` 都是 argument (引數),或稱函式引數 (Argument of a Function)
- `x` 和 `y` 都是 parameter (參數)
```javascript
function add(x, y) {
console.log(x + y);
}
var a = 10;
add(a, 20);
```
在其他程式語言有指令列引數 (Command-line Argument):
```java
class Hello {
public static void main(String[] args) {
if (args.length > 0) {
System.out.println("The command line arguments are:");
for (String val: args)
System.out.println(val);
}
else
System.out.println("No command line arguments found.");
}
}
```
像此範例中,`Titan` 和 `Ya` 都是 Command-line Argument:
```shell
$ javac hello.java
$ java Hello
No command line arguments found.
$ java Hello Titan Ya
The command line arguments are:
Titan
Ya
```
資料來源:[引數 (Argument) vs. 參數 (Parameter) | NotFalse 技術客](https://notfalse.net/6/arg-vs-param)
:::
物件 (object) 是合成值 (composite value):聚集多個值 (基礎型別值或其他物件),可透過名稱儲存並取回這些值
物件的 property 是無序群集 (collection),每個 property 都有名稱與值 (key 和 value)
物件名稱是字串,所以也可以說物件將字串 map 至值。字串對值 (string-to-value) 的 mapping 有多種:
- hash (雜湊)
- hashtable (雜湊表)
- dictionart (字典)
- associative array (關聯式陣列)
物件能繼承其他物件的 property,這些物件就稱為它的「原型」(prototype)。物件的方法通常就是繼承而來的 property (簡稱「繼承特性」),這種「原型繼承」(prototypal ingeritance) 是 JavaScript 的關鍵特色
JavaScript 物件是動態的 (dynamic),通常可以加入或刪除 property,但也可用來模擬靜態型別 (statically typed) 語言的靜態 (static) 物件或 structs (結構),也可 (藉由忽略 string-to-value map 中 value 的部份) 用來代表一組字串
在 JavaScript 中,任何不是字串、數字、`true`、`false`、`null` 或 `undefined` 的值都是物件,雖然字串、數字與 booleans 不是物件,但它們的行為很像是不可變物件 (immutable object)
物件是可變的 (mutable),是透過參考 (by reference) 來操作它,而非值 (by value)
如果變數 `x` 參考一個物件,執行程式碼 `var y = x;`,變數 `y` 就持有對同一物件的參考,而非該物件的複製品 (copy of that object),任何透過變數 `y` 對物件做的修改,藉由變數 `x` 也看得到
property 具有名稱和值:
- property 名稱
- 可以是任何字串,包括空字串
- 不能有同名的特性
- property 值
- 可以是任何 JavaScript 值或是 (在 ECMAScript 5 中) getter (取值器) 和/或 setter (設值器) 函數
除了名稱與值之外,每個 property 還有與其相關的值,稱為特性屬性 (property attributes):
- writable (可寫) 屬性指名該 property 值能夠設定與否
- enumerable (可列舉) 屬性指名該 property 名稱是否可被 `for/in` 迴圈回傳
- configurable (可配置) 屬性指名該 property 是否可被刪除,以及它的屬性能夠更動與否
在 ES5 出現之前,你的程式碼建立的所有物件 property 都是 writable、enumerable 和configurable。在 ES5 能夠配置 property 的屬性。
除了 property 之外,每個物件也有三個關聯的物件屬性 (object attributes):
- 物件的 prototype 是對另一個物件的參考,property 是從該物件繼承而來
- 物件的 class 是個字串,用來區分該物件的種類
- 物件的 extensible (可擴充) flag 指定 (在 ES5 中) 是否可在該物件上加新入新 property
下面是三個 JavaScript 物件種類和兩種 property 類型:
- Native (原生) 物件是由 ECMAScript 規格所定義的物件或物件類別。例如:陣列、函數、日期與正規運算式都是 native 物件
- Host (宿主) 物件是由嵌入式 JavaScript 直譯器的 host 環境 (例如:web 瀏覽器) 所定義的物件。在客戶端 JavaScript 用來表示網頁結構的 HTMLElement 物件就是一種 host 物件。Host 物件也可以式 native 物件,例如:當 host 環境定義的方法只是普通的 JavaScript Function 物件時
- User-defined (使用者定義) 的物件是透過 JavaScript 程式碼執行時所建立的任何物件
- Own (自有) property 是物件上直接定義的 property
- Inherited (繼承) property 是由物件的原型物件所定義的 property
## 6.1 建立物件
可用以下方式建立物件:
- obejct literal (物件字面值)
- `new` 關鍵字
- `Object.create()` (ECMAScript 5)
### 6.1.1 Obejct literal
- 在 JavaScript 中最簡單的建立物件方式
- 一串以逗號和冒號分隔的 name:value pairs,病史用大括號 (curly braces) 包住
- 在 ES5,obejct literal 中最後一個 property 後面接的逗號會被忽略,在大多數 ES3 實作中,該逗號也會被忽略,但在 IE 會視為錯誤
property 具有名稱和值:
- property 名稱:
- JavaScript 識別字或 string literal (包括空字串)
- 可不加引號,例如:`{ name: 'Titan' }`
- 要在 property 名稱使用保留字
- 在 ES3 一定要加上引號,例如:`{ 'for': 'Titan' }`
- 在 ES5 可不用加上引號,例如:`{ for: 'Titan' }`
- property 值
- 可以是任何 JavaScript 運算式,該運算式的值 (可能是基本型別值或物件值) 就是 property 值
```javascript
var empty = {}; // 沒有 property 的物件
var point = { x: 0, y: 0 }; // 兩個 property
var point2 = { x: point.x, y: point.y + 1 };
var book = {
"main title": "JavaScript", // 包含空格的 property 名稱
'sub-title': "The Definitive Guide", // 或包含 '-' (hyphen),property 名稱 使用 string literal
"for": "all audiences", // for 是保留字,要加上引號
author: { // property 值是物件
firstname: "David", // 沒有引號的 property 名稱
surname: "Flanagan"
} // 最後一個 property 後面接的逗號會被忽略
};
```
obejct literal 是運算式,每次被估算時會建立、初始化一個新的個別物件,每個 property 值在每次估算 literal 時,也會被估算,這代表如果出現在函數中的迴圈主體,而函數重複被 call,單一 obejct literal 也會建立許多新物件,其中每個物件的 property 值可能彼此不同。
驗證:
```javascript
(function() {
if (typeof Object.prototype.uniqueId == 'undefined') {
var id = 0;
Object.prototype.uniqueId = function() {
if (typeof this.__uniqueid == 'undefined') {
this.__uniqueid = ++id;
}
return this.__uniqueid;
};
}
})();
function createObj() {
for (var i = 0; i < 3; i++) {
var obj = { x: i };
console.log(obj);
}
}
createObj();
// { x: 0, __uniqueid: 1 }
// { x: 1, __uniqueid: 2 }
// { x: 2, __uniqueid: 3 }
createObj();
// { x: 0, __uniqueid: 4 }
// { x: 1, __uniqueid: 5 }
// { x: 2, __uniqueid: 6 }
```
資料來源:
- [unique object identifier in javascript - Stack Overflow](https://stackoverflow.com/questions/1997661/unique-object-identifier-in-javascript)
:::danger
不懂:
- 估算?
:::
### 6.1.2 使用 `new` 建立物件
- `new` 運算子建立並初始化新物件
- `new` 關鍵字之後必須接著函數 invoke,透過這種方式使用的函數稱為建構式 (constructor),用來初始化新建立的物件
核心 JavaScript 內建了 native 型別用的建構式,例如:
```javascript
var o = new Object(); // 建立空物件,等同於 {}
var a = new Array(); // 建立空陣列,等同於 []
var d = new Date(); // 建立代表目前時間的 Date 物件
var r = new RegExp('js'); // 建立用來 pattern matching 的 RegExp 物件
```
### 6.1.3 原型 (prototype)
每個 JavaScript 物件都有兩個物件 (或 `null`,但很少見) 與之關聯:object inherits property (原型繼承特性) 和 prototype (原型)
- 所有用 object literal 建立的物件都有同一個原型物件
- 用 `Object.prototype` 來參考這個原型物件
- 透過 `new` 關鍵字與建構式 invoke 所產生的物件,其原型為建構函數 (constructor function) 的 `prototype` property 值
- 使用 `new Object()` 建立的物件繼承至 `Object.prototype`,跟用 `{}` 建立的物件相同
- `Array()` 和 `Date()` 同理
- `Object.prototype` 是少數沒有原型的物件:沒有繼承任何 property。其他原型物件則有原型的普通物件 (normal object)
- 所有內建的建構式 (以及大多數使用者定義的建構式) 都有一個繼承至 `Object.prototype` 的原型,例如:
- `Date.prototype` 從 `Object.prototype` 繼承 property,所以使用 `new Date()` 建立的 `Date` 物件,同時繼承了 `Date.prototype` 與 `Object.prototype` 兩者的 property,這些串成一系列的原型物件就稱為原型鏈 (prototype chain)
### 6.1.4 `Object.create()`
- ES5 定義的方法,可建立新的物件
- `Object.create()` 的 argument (引數)
- 第一個:作為該物件的原型
- 第二個:可選 argument,描述新物件的 property
`Object.create()` 是個靜態 (static) 函數,不再個別物件上 invoke 的方法,要使用它,只需把想作為原型的物件傳給它:
```javascript
// o1 繼承了 property x 和 y
var o1 = Object.create({ x: 1, y: 2 });
```

也可傳 `null` 給它:
- 建立沒有原型的新物件
- 建立的物件不會繼承任何 property,連 `toString()` 這種基本方法也沒有 (這也代表此新物件不能當作 `+` 運算子的運算元)
```javascript
// o2 不會繼承任何 property 或方法
var o2 = Object.create(null);
```

如果要建立一個普通的空物件 (就像 `{}` 或 `new Object()` 回傳的一樣),就傳入 `Object.prototype`:
```javascript
var o3 = Object.create(Object.prototype);
```

使用任意原型建立新物件的能力 (換句話說:為任何物件建立「繼承者」(heir) 的能力),非常強大。下面範例的函數會回傳一個繼承了 argument 物件的新物件:
```javascript
// inherit() 回傳一個新建物件,該物件從原型物件 p 那繼承了 property。
// 試著用 ES5 的 Object.create() 函數
// 如果有定義就使用,不然就使用較舊的技巧
function inherit(p) {
if (p == null) throw TypeError(); // p 必須是非 null 物件
if (Object.create) // 如果 Object.create() 有定義
return Object.create(p); // 就直接用它
var t = typeof p; // 不然就做型別檢查
if (t !== "object" && t !== "function") throw TypeError();
function f() {}; // 定義一個空殼的建構函式
f.prototype = p; // 將 prototype property 設為 p
return new f(); // 使用 f() 建立一個 p 的「繼承者」
}
```

`inherit()` 不能完全取代 `Object.create()`,因為:
- 不能建立原型是 `null` 的物件
- 不能像 `Object.create()` 可接受額外的第二個 argument
當你想要防止你不能控制的 library 函數不經意地 (但非惡意) 修改某物件時,`inherit()` 函數就能派上用場:
- 不用直接把該物件傳給 library 函數,可傳給它該物件的繼承者
- 如果該函數讀取繼承者的 property,就能獨到繼承來的值
- 如果該函數設定某個 property,只會影響到繼承者,而非原本的物件
```javascript
var o = { x: "don't change this value" };
library_func(inherit(o)); // 防止對 0 的意外修改
```
```javascript
var obj1 = { x: "don't change this value" };
function library_func(obj) {
obj.x = 'modified this value';
}
library_func(obj1);
console.log(obj1); // { x: "modified this value" }
var obj2 = { x: "don't change this value" };
library_func(inherit(obj2));
console.log(obj2); // { x: "don't change this value" }
```
## 6.2 查看與設定 property
取得 property:
- 點 ( `.` ) 運算子
- 中括號 (square bracket,`[]` ) 運算子
```
expression.identifier
expression[expression]
```
- 左邊:值為物件的運算式
- 右邊:
- 點 ( `.` ) 運算子:必須用來命名 property 的簡單識別字
- 中括號 ( `[]` ):裡面的值必須是估算值為 property 名稱字串的運算式
```javascript
var book = {
"main title": "JavaScript",
'sub-title': "The Definitive Guide",
"for": "all audiences",
author: {
firstname: "David",
surname: "Flanagan"
}
};
var author = book.author;
var name = author.surname;
var title = book["main title"];
```
建立或設定 property:物件要放在指定運算式的左邊
```javascript
book.edition = 6; // 為 book 建立 "edition" property
book["main title"] = "ECMAScript"; // 設定 "main title" property
```
如果物件中有 property 名為保留字:
- 在 ES3 中,在點運算子之後的識別字不能是保留字:
- 例如:不能寫 `o.for` 或 `o.class`,因為 `for` 是關鍵字,而 `class` 是保留字,作為未來使用
- 所以必須改用中括號才能存取它:`o['for']` 與 `o['class']`
- ES5 放寬此限制 (某些 ES3 實作也是),在點之後可以是保留字
使用中括號時,中括號內的運算式的估算值必須是字串,或能轉成字串的值,例如:陣列可在中括號內使用數字:
```javascript
// 建立一個普通的空物件
var ary = {};
// 加上 porperty 使它成為類陣列物件
var num = 5;
for (var i = 0; i < num; i++) {
ary[i] = i * i;
}
ary.length = num;
// 把它當作真正的陣列來用
for (var i = 0; i < ary.length; i++) {
console.log(ary[i]);
}
```
### 6.2.1 物件作為關聯式陣列 (associative array)
下面的 JavaScript 運算式有相同的值:
```javascript
object.property
object["property"]
```
- 第一種語法:使用點與識別字,很像 C 或 Java 中存取結構 (struct) 或物件的靜態資料欄 (static field)
- 第二種語法:使用中括號與字串,很像陣列存取,但陣列的索引是字串而非數字 (使用數值索引存取陣列的元素時,索引 `1` 會變成字串 `'1'`,然後把該字串當作 porperty 名稱),這種陣列稱為關聯式陣列 (associative array,或稱 hash 或 map 或 dictionary)
#### 強型別與弱型別的物件
在 C、C++、Java 以及類似的強型別 (strongly typed) 語言,物件只能有固定數量的 property,而且這些 property 名稱一定要事先定義。
JavaScript 是弱型別 (loosely typed,weakly typed) 語言,上述規則不適用:程式可在任何物件中建立任意數量的 property。
#### 存取物件的 property
用點 ( `.` ) 運算子:property 名稱是用識別字表示
- 識別字必須逐字輸入 JS 程式中
- 也就是 property 名稱打錯、大小寫不同,都會無法存取到指定的 property
- 識別字不是資料型別,因此程式無法對它們進行操作
```javascript
var obj = {
a: 1,
hi: 'hello'
};
console.log(obj.A); // undefined
console.log(obj.gi); // undefined
```
若用 `[]` 陣列表示法 (notation) 存取物件的 property 時,property 名稱是用字串表示,字串是 JS 的資料型別,所以程式執行時,它們可被操作或建立。
使用陣列表示法配合字串運算式可以彈性地存取物件的 property,例如:
```javascript
var addr = "";
var customer = {
address0: 'a',
address1: 'b',
address2: 'c'
};
for (i = 0; i < 3; i++)
addr += customer["address" + i] + ' ';
console.log(addr); // "a b c"
```
上面的傳程式碼讀取並串接 (concatenates) `customer` 物件的 `address0`、`address1` 與 `address2` property。
範例:透過網路資源計算使用者故事投資目前的價值,可讓使用者輸入他所擁有的股票名稱、多少股份,你可能會用 `portfolio` 物件來儲存這些資訊,每支股票再這個物件中都有對應的 property,property 名稱就是股票名稱,而 property 值為他所有的股份數量。假設使用者持有 50 股的 IBM 股票,`portfolio.ibm` 的 property 值就會是 `50`:
```javascript
var portfolio = {
ibm: 50,
apple: 30
};
console.log(portfolio.ibm); // 50
```
下面的 `addStock()` 函數可用來新增一支股票置投資組合 (portfolio) 中:
```javascript
function addStock(portfolio, stockName, shares) {
portfolio[stockName] = shares;
}
addStock(portfolio, 'google', 80);
console.log(portfolio); // { ibm: 50, google: 80, apple: 30 }
```
在以上情境就無法使用點 ( `.` ) 運算子來存取,因為無法事先預知 property 名稱是什麼,也就無法存取到物件的 property。不過可用 `[]` 運算子,因為它使用字串值 (字串值是動態的,在 runtime 時可以改變),而非識別字 (識別字是靜態的,必須寫死在程式中) 來指定 property。
關聯式陣列 (associative array) 與 `for/in` 陳述句 (statement) 並用:
```javascript
function getValue(portfolio) {
var total = 0.0;
for(stock in portfolio) { // 對投資組合中的每支股票
var shares = portfolio[stock]; // 取得股份數量
var price = getQuote(stock); // 查詢股價
total += shares * price; // 對這支股票的價值進行加總
}
return total;
}
function getQuote(stock) {
// 先假設每支股票的價值為 10 元...
return 10;
}
var total = getValue(portfolio);
console.log(total); // 1600
```
### 6.2.2 繼承
JavaScript 物件有一組「自有特性」(own properties),並且它們也從其原型物件繼承了一組 property。為了理解這點,必須深入探討特性存取 (property access)。下面範例會用到 `inherit()` 這個函數,用來建立繼承至特定原型的物件。
```javascript
// inherit() 回傳一個新建物件,該物件從原型物件 p 那繼承了 property。
// 試著用 ES5 的 Object.create() 函數
// 如果有定義就使用,不然就使用較舊的技巧
function inherit(p) {
if (p == null) throw TypeError(); // p 必須是非 null 物件
if (Object.create) // 如果 Object.create() 有定義
return Object.create(p); // 就直接用它
var t = typeof p; // 不然就做型別檢查
if (t !== "object" && t !== "function") throw TypeError();
function f() {}; // 定義一個空殼的建構函式
f.prototype = p; // 將 prototype property 設為 p
return new f(); // 使用 f() 建立一個 p 的「繼承者」
}
```
如果要查物件 `o` 的 property `x`:
- 若 `o` 有名為 `x` 的 own property
- 找到 property `x`
- 若 `o` 沒有名為 `x` 的 own property
- 若 `o` 的原型物件中 ( `o.__proto__` ) 有名為 `x` 的 own property
- 找到 property `x`
- 若 `o` 的原型物件中沒有名為 `x` 的 own property,但本身具有原型
- 繼續往此原型中尋找,持續找到名為 `x` 的 property 為止
- 或直到搜尋到一個原型為 `null` 的物件為止
```javascript
var o = {}; // o 從 Obejct.prototype 繼承了物件方法
o.x = 1;
var p = inherit(o); // p 繼承 property 至 o 與 Obejct.prototype
p.y = 2;
var q = inherit(p); // q 繼承 property 至 p、o 與 Obejct.prototype
q.z = 3;
var s = q.toString(); // toString 是從 Obejct.prototype 繼承而來
console.log(q.x + q.y); // 3,繼承至 o 的 x 和繼承至 p 的 y
```
指定值給物件 `o` 的 property `x`:
- 非繼承:
- 若有名為 `x` 的 own property:單純改變現有的 property 值
- 若沒有名為 `x` 的 own property:在 `o` 上建立一個名為 `x` 的 property
- 繼承:
- 若 `o` 曾繼承 property `x`:
- 此繼承特性 (inherit property) 會被新建立的同名 own property 所隱藏 (hidden)
- 且只會在原物件上建立或設定 property,永遠不會修改到原型鏈中的物件
```javascript
o = {};
o.x = 1;
p = Object.create(o);
console.log(p.x); // 1
p.x = 2;
console.log(p.x); // 2
console.log(p); // { x: 2 }
console.log(p.__proto__); // { x: 1 }
```
property 的指定會檢視原型鏈 (prototype chain) 來判斷這個定是否可行。例如:若 `o` 繼承了名為 `x` 的唯讀 (read-only) property,此指定就不被允許,例如:
```javascript
obj = {};
obj.x = 1;
Object.defineProperties(obj, {
x: { writable: false } // 將property 設為唯讀
});
o = Object.create(obj);
console.log(o.x); // 1
o.x = 2;
console.log(o.x); // 1
console.log(o); // {}
```
繼承只會在找 property 時發生,不會在設定 property 時發生,因為可讓開發者選擇性地覆寫 (override) 繼承 property:
```javascript
var unitCircle = { r:1 }; // 要繼承的物件
var c = inherit(unitCircle); // c 繼承 property r
c.x = 1; c.y = 1; // c 定義兩個 own property
c.r = 2; // c 覆寫它繼承的 property
console.log(c); // {x: 1, y: 1, r: 2}
console.log(unitCircle.r); // 1,沒有影響到原型物件
```
:::danger
不懂:
- 如果 `o` 繼承 property `x`,而此 property 具有 setter 方法的 accessor (存取器) property,就會 call 該 setter 方法,而非在 `o` 上新建的 property `x`。然而,請注意被呼叫的 setter 方法會作用在物件 `o` 上,而不是定義那個 property 的原型物件,所以如果 setter 方法定義任何 property,他會在 `o` 上定義,一樣不會修改到原型鏈上的物件。
:::
### 6.2.3 property 存取錯誤
property 存取運算式 (property access expression) 不一定總是回傳值或設定值,下面說明尋找或設定 property 時,可能發生的錯誤。
#### 尋找
尋找不存在的 property 不會產生錯誤,例如:若 `o` 的 own property 或繼承 property 中都沒有 property `x`,那 property 存取運算式 `o.x` 的估算值會是 `undefined`:
```javascript
var book = { 'sub-title': 'Hello' };
console.log(book.subtitle); // undefined
```

尋找不存在的物件的 property 會產生錯誤,因為 `null` 與 `undefined` 值都沒有 property,所以會產生 `TypeError` 例外:
```javascript
console.log(book.subtitle.length);
```

因為會拋出例外,可以用以下兩種方式防止例外發生:
```javascript
// 較為冗長,但明確
var len = undefined;
if (book) {
if (book.subtitle) len = book.subtitle.length;
}
// 簡潔,取得的內容會是 book.subtitle.length 或 undefined
var len = book && book.subtitle && book.subtitle.length;
```
:::info
補:
- `&&` 運算子短路行為 (short-circuiting)
:::
在 `null` 與 `undefined` 值上設定 property 也會產生 `TypeError`。
某些 property 是唯讀 (read-only) 的,不能設定,而某些物件不允許新增新 property。以上這些操作都會無聲無息的失敗,沒有產生錯誤。例如:內建的建構式的 `prototype` property 是唯讀的,指定是無聲的失敗,不會產生任何訊息,而且 `Object.prototype` 也沒被改變:
```javascript
Object.prototype = 0;
```
但在 ES5 的 strict 模式中會受到限制,任何 property 設定動作失敗時,都會拋出 `TypeError` 例外。
```javascript
'use strict';
Object.prototype = 0;
```

設定 `o` 物件的 `p` property 會在以下情況失敗:
- `o` 的 own property 是唯讀的:無法設定唯讀 property (有個例外,參見 `Object.defineProperty()` 方法:允許設定 `configurable` 唯讀 property)
- `o` 有個唯讀的繼承 property `p`:無法使用同名的 own property 隱藏 (hide) 唯讀的繼承 property
- `o` 沒有 own property `p`,`o` 沒有繼承具有 setter 方法的 property `p`,而且 `o` 的 `extensible` 屬性為 `fasle`:如果 `o` 中沒有 `p`,而且沒有 setter 方法可 call,那 `p` 就得被加入 `o`,但如果 `o` 是不可擴充 (not extensible),就不能為它定義新 property
## 6.3 刪除 property
- `delete` 運算子從物件中刪除 property
- 只會刪除 own property,不會刪除繼承 property (要刪除繼承 property 必須至定義該 property 的原型物件上刪除,這樣會影響到所有繼承此原型的物件)
- 它的單一運算元應該是個 property 存取運算式 (property access expression)
- `delete` 不是作用在值上,而是作用在 property 本身
```javascript
var book = {
"main title": "JavaScript",
'sub-title': "The Definitive Guide",
"for": "all audiences",
author: {
firstname: "David",
surname: "Flanagan"
}
};
delete book.author;
delete book["main title"];
```
使用 `delete` 運算子的回傳值:
- `true`:
- 刪除成功
- 刪除動作無效 (例如:刪除不存在的 property)
- 無意義地用在非 property 存取運算式的運算式上
```javascript
var o = { x: 1 };
delete o.x;
delete o.x;
delete o.toString();
delete 1;
```
`delete` 不會刪除 `configurable` 屬性為 `false` (代表不可配置,nonconfigurable) 的 property,不過它會刪除不可擴充 (nonextensible) 物件的可配置 (configurable) property。
:::danger
補內文
:::
## 6.4 測試特性
檢查某物件是否具有指定名稱的 property,可用 `in` 運算子、`hasOwnProperty()` 與 `propertyIsEnumerable()` 方法,或單純地找該 property。
### `in` 運算子
左邊是 property 名稱 (字串),右邊是物件,如果該物件擁有指定名稱的 own 或繼承 property,運算式就回傳 `true`:
```javascript
var o = { x: 1 };
'x' in o; // true
'y' in o; // false
'toString' in o; // true
```
- 物件的 `hasOwnProperty()`:測試該物件是否有指定名稱的 own property,如果是繼承 property 就回傳 `false`
- `propertyIsEnumerable()`:在指定名稱的 property 為 own property,而且 `enumerable` 屬性為 `true` 時,才會回傳 `true`
- 某些內建 property 是不可列舉的 (not enumerable)
- 除非你用 ES5 方法來將 property 設為不可列舉 (nonenumerable),不然普通的 JavaScript 程式碼建立的 property 都是可列舉的 (enumerable)
- 使用 `!==` 來確定其值不是 `undefined` 就夠用了,不需用到 `in` 運算子
```javascript
var o = { x: 1 };
o.x !== undefined; // true
o.y !== undefined; // false
o.toString !== undefined; // true
```
但有些是簡單的 property 存取技巧無法達成,只有 `in` 運算子可以做到:
- 不存在的 property
- 存在但被設為 `undefined` 的 property
```javascript
var o = { x: undefined };
o.x !== undefined; // false
o.y !== undefined; // false
"x" in o; // true
"y" in o; // false
delete o.x; // 刪除 property x
"x" in o; // false
```
`!==` 和 `===` 可分辨 `undefined` 和 `null`,`!=` 則不行。不過有時不需要區分這麼詳細:
```javascript
// 如果 o 有值為 null 或 undefined 的 property x,則其值加倍
if (o.x != null) o.x *= 2;
// 如果 o 有值不能轉為 false 的 property x,則將其值加倍
// 如果 x 是 undefined、null、false、""、0 或 NaN,就不要動它
if (o.x) o.x *= 2;
```
## 6.5 列舉 (Enumerating) property
遍歷 (iterate through) 物件的 property,或是取得物件 property 清單
`for/in` 迴圈匯兌指定物件的每個可列舉 property (own 或繼承),都會執行一次迴圈 body,將 property 名稱指定給迴圈變數。物件繼承的內建方法都不可列舉,但程式碼加至物件的 property 都是可列舉的。
:::danger
不懂:
- 除非用下面會說到的函數之一將之設為不可列舉
- 哪個?
:::
```javascript
var o = {x:1, y:2, z:3};
o.propertyIsEnumerable("toString")
for(p in o)
console.log(p);
```
有些工具 (utility) library 會新增方法 (或 property) 至 `Object.prototype` 中,所以這些方法或 property 都會被繼承,每個物件都可使用。但再 ES5 之前,無法讓這些新增的方法變成不可列舉,所以都會被 `for/in` 迴圈所列舉。為了防止這種狀況,可以過濾 `for/in` 回傳的特性,有兩種方式:
```javascript
for(p in o) {
if (!o.hasOwnProperty(p)) continue; // 跳過繼承 property
}
for(p in o) {
if (typeof o[p] === "function") continue; // 跳過方法
}
```
下面是己幾個工具函數,使用 `for/in` 迴圈操作物件 property:
```javascript
// 延伸
function extend(o, p) {
for(prop in p) {
o[prop] = p[prop];
}
return o;
}
// 合併
function merge(o, p) {
for(prop in p) {
if (o.hasOwnProperty[prop]) continue;
o[prop] = p[prop];
}
return o;
}
// 限制
function restrict(o, p) {
for(prop in o) {
if (!(prop in p)) delete o[prop];
}
return o;
}
// 減去
function subtract(o, p) {
for(prop in p) {
delete o[prop];
}
returno;
}
// 聯集
function union(o,p) { return extend(extend({},o), p); }
// 交集
function intersection(o,p) { return restrict(extend({}, o), p); }
// keys
function keys(o) {
if (typeof o !== "object") throw TypeError();
var result = [];
for(var prop in o) {
if (o.hasOwnProperty(prop))
result.push(prop);
}
return result;
}
```
除了 `for/in` 迴圈,ES5 還定義了兩種列舉 property 名稱用的函數:
- `Object.keys()`:回船物件的可列舉 own property 名稱陣列,很像上面範例的 `keys()`
- `Object.getOwnPropertyNames()`:類似 `Object.keys()`,但它回傳指定物件全部的 own property,不僅是可列舉 property。在 ES3 中是無法寫出此函數,因為 ES3 沒有提供取得物件的不可列舉的 property
```javascript
var obj = { x: 1, y: 2 };
Object.defineProperties(obj, {
x: { enumerable: false }
});
Object.keys(obj); // ["y"]
Object.getOwnPropertyNames(obj); // ["x", "y"]
```
## 6.6 Property Getters 和 Setters
- 由 getter 與 setter 所定義的 property 有時稱為 accessor (存取器) property,用來區分它們與具有簡單值的資料 property
- 當程式查詢 accessor property 值時
- JavaScript 會 invoke getter 方法 (不傳入 arguments)
- 這些方法的回傳值就會成為該 property 存取運算式的值
- 當成式要設定 accessor property 值時
- JavaScript 會 invoke setter 方法
- 傳入指定運算式右邊的值
- 負責設定該 property 值
- setter 方法的回傳值會被忽略
- accessor property 不同於資料 property
- 沒有 writable 屬性
- 若一個 property 同時擁有 getter 與 setter 方法:可讀/可寫的 property
- 若只有 getter 方法:唯讀的 property
- 若只有 setter 方法:只能寫入的 property (這對資料 property 來說是不可的),嘗試讀取它會產生估計值 `undefined`
定義 accessor property 最簡單的方式就是使用 object literal 擴充語法:
```javascript
var o = {
data_prop: value,
get accessor_prop() { /* function body here */ },
set accessor_prop(value) { /* function body here */ }
};
```
accessor property 定義為:
- 一個或兩個函數,函數名稱與 property 名稱相同
- 用 `get` 及/或 `set` 取代 `function` 關鍵字
- 不需要用冒號分隔 property 名稱與存取該 property 的函數
- 但函數 body 之後還是需要逗號以區隔下一個方法或資料 property
- 把函數當作包含這些定義的物件之方法來 invoke
- 在函數 body 中,`this` 參考至物件 (point object)
- accessor property 可被繼承,如同資料 property,可用之前定義的物件作為其他物件的原型
```javascript
var p = {
x: 1.0,
y: 1.0,
get r() { return Math.sqrt(this.x*this.x + this.y*this.y); },
set r(newvalue) {
var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y);
var ratio = newvalue/oldvalue;
this.x *= ratio;
this.y *= ratio;
},
get theta() { return Math.atan2(this.y, this.x); }
};
var q = inherit(p);
q.x = 0, q.y = 0;
console.log(q.r);
console.log(q.theta);
```
其他會用到 accessor property 的地方包括:
- property 寫入的完整性檢查 (sanity checking)
- 每次 property 讀取時回傳不同值
```javascript
var serialnum = {
$n: 0,
get next() { return this.$n++; },
set next(n) {
if (n >= this.$n) this.$n = n;
else throw "serial number can only be set to a larger value";
}
};
```
## 6.7 Property Attributes
除了名稱與值外,property 還有屬性來指定它們是否可被寫入 (written)、列舉 (enumerated) 或配置 (configured)。
在 ES3 無法設定這些屬性,所以由 ES3 程式建立的 property 都是可寫、可列舉,以及可配置的。
在 ES5 提供可以察看和設定 property 屬性的 API,可用來:
- 在原型物件中加入方法,並將該方法設為不可列舉,就像內建方法一樣
- 讓他們可以封鎖 (lock down) 他們的物件,定一齣無法被修改和刪除的 property
:::danger
不懂:
- 讓他們可以封鎖 (lock down) 他們的物件
- 他們是誰?
:::
accessor property 的 getter 和 setter 方法視為 property 屬性
property 有一個名稱與四個屬性:
- 資料 property 的四個屬性:
- value
- writable
- enumerable
- configurable
- accessor property 的四個屬性 (沒有 value 和 writable 屬性,可寫與否事由是否具有 setter 來決定):
- get
- set
- enumerable
- configurable
用來查詢或設定 property 的屬性的 ES5 方法使用稱為 property descripter (描述子) 的物件來表示那四個屬性。property descripter 擁有與它所描述的 property 屬性同名的 property。
- 資料 property 的 property descripter 物件具有以下 property:
- value
- writable
- enumerable
- configurable
- accessor property 的 property descripter:
- 用 get 與 set property 取代 value 與 writable
writable、enumerable 和 configurable 的 property 是 boolean 值,而 get 與 set property 是函數值。
要為特定物件指定名稱的 property 取得 property descripter,就 call `Object.getOwnPropertyDescriptor()`,但只能用在 own property 上
要查繼承 property 的屬性,必須明確地遍歷 (traverse) 原型鏈 ( `Object.getPrototypeOf()` )
要設定 property 的屬性或建立具有指定屬性的新 property,就 call `Object.defineProperty()`,並傳入要修改的物件、要建立或更改的屬性名稱,以及 property descripter 物件。
- 如果你建立新 property,省略掉的屬性會取 `false` 或 `undefined`
- 如果你修改現有 property,省略掉的屬性就不會改變
`Object.defineProperty()` 會更動現存的 own property 或建立新的 own property,而不會變更繼承 property。