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 }); ``` ![](../../screenshot/2019-11-24-06-38-56.png) 也可傳 `null` 給它: - 建立沒有原型的新物件 - 建立的物件不會繼承任何 property,連 `toString()` 這種基本方法也沒有 (這也代表此新物件不能當作 `+` 運算子的運算元) ```javascript // o2 不會繼承任何 property 或方法 var o2 = Object.create(null); ``` ![](../../screenshot/2019-11-24-06-39-21.png) 如果要建立一個普通的空物件 (就像 `{}` 或 `new Object()` 回傳的一樣),就傳入 `Object.prototype`: ```javascript var o3 = Object.create(Object.prototype); ``` ![](../../screenshot/2019-11-24-06-41-33.png) 使用任意原型建立新物件的能力 (換句話說:為任何物件建立「繼承者」(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 的「繼承者」 } ``` ![](../../screenshot/2019-11-24-06-56-59.png) `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 ``` ![](../../screenshot/2019-11-24-11-20-05.png) 尋找不存在的物件的 property 會產生錯誤,因為 `null` 與 `undefined` 值都沒有 property,所以會產生 `TypeError` 例外: ```javascript console.log(book.subtitle.length); ``` ![](../../screenshot/2019-11-24-11-20-44.png) 因為會拋出例外,可以用以下兩種方式防止例外發生: ```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; ``` ![](../../screenshot/2019-11-24-11-39-17.png) 設定 `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。