《JavaScript物件導向精要》之三:理解物件

明易發表於2018-12-16

JavaScript 中的物件是動態的,可在程式碼執行的任意時刻發生改變。基於類的語言會根據類的定義鎖定物件。

3.1 定義屬性

當一個屬性第一次被新增到物件時,JavaScript 會在物件上呼叫一個名為 [[Put]] 的內部方法。

[[Put]] 方法會在物件上建立一個新節點來儲存屬性。

當一個已有的屬性被賦予一個新值時,呼叫的是一個名為 [[Set]] 的方法。

3.2 屬性探測

檢查物件是否已有一個屬性。JavaScript 開發新手錯誤地使用以下模式檢測屬性是否存在。

if(person.age){
  // do something with ag
}
複製程式碼

上面的問題在於 JavaScript 的型別強制會影響該模式的輸出結果。
當 if 判斷中的值如下時,會判斷為

  • 物件
  • 非空字串
  • 非零
  • true

當 if 判斷中的值如下時,會判斷為

  • null
  • undefined
  • 0
  • false
  • NaN
  • 空字串

因此判斷屬性是否存在的方法是使用 in 操作符。

in 操作符會檢查自有屬性和原型屬性

所有的物件都擁有的 hasOwnProperty() 方法(其實是 Object.prototype 原型物件的),該方法在給定的屬性存在且為自有屬性時返回 true

var person = {
  name: "Nicholas"
}

console.log("name" in person); // true
console.log(person.hasOwnpropert("name")); // true

console.log("toString" in person); // true
console.log(person.hasOwnproperty("toString")); // false
複製程式碼

3.3 刪除屬性

設定一個屬性的值為 null 並不能從物件中徹底移除那個屬性,這只是呼叫 [[Set]]null 值替換了該屬性原來的值而已。

delete 操作符針對單個物件屬性呼叫名為 [[Delete]] 的內部方法。刪除成功時,返回 true

var person = {
  name: "Nicholas"
}

person.name = null;
console.log("name" in person); // true
delete person.name;
console.log(person.name); // undefined 訪問一個不存在的屬性將返回 undefined
console.log("name" in person); // false
複製程式碼

3.4 屬性列舉

所有人為新增的屬性預設都是可列舉的。可列舉的內部特徵 [[Enumerable]] 都被設定為 true

for-in 迴圈會列舉一個物件所有的可列舉屬性。

ECMAScript 5 的 Object.keys() 方法可以獲取可列舉屬性的名字的陣列。

var person = {
  name: "Nicholas",
  age: 18
}

Object.keys(person); // ["name", "age"];
複製程式碼

for-inObject.keys() 的一個區別是:前者也會遍歷原型屬性,而後者返回自有(例項)屬性。

實際上,物件的大部分原生方法的 [[Enumerable]] 特徵都被設定為 false。可用 propertyIsEnumerable() 方法檢查一個屬性是否為可列舉的。

var arr = ["abc", 2];
console.log(arr.propertyIsEnumerable("length")); // false
複製程式碼

3.5 屬性型別

屬性有兩種型別:資料屬性訪問器屬性

資料屬性包含一個值。[[Put]] 方法的預設行為是建立資料屬性

訪問器屬性不包含值而是定義了一個當屬性被讀取時呼叫的函式(稱為 getter)和一個當屬性被寫入時呼叫的函式(稱為 setter)。訪問器屬性僅需要 gettersetter 兩者中的任意一個,當然也可以兩者。

// 物件字面形式中定義訪問器屬性有特殊的語法:
var person = {
  _name: "Nicholas",

  get name(){
    console.log("Reading name");
    return this._name;
  },
  set name(value){
    console.log("Setting name to %s", value);
    this._name = value;
  }
};
    
console.log(person.name); // "Reading name" 然後輸出 "Nicholas"

person.name = "Greg";
console.log(person.name); // "Setting name to Greg" 然後輸出 "Greg"
複製程式碼

前置下劃線 _ 是一個約定俗成的命名規範,表示該屬性是私有的,實際上它還是公開的。

訪問器就是定義了我們在物件讀取或設定屬性時,觸發的動作(函式),_name 相當於一個內部變數。

當你希望賦值(讀取)操作會觸發一些行為,訪問器就會非常有用。

當只定義 getter 或 setter 其一時,該屬性就會變成只讀或只寫。

3.6 屬性特徵

在 ECMAScript 5 之前沒有辦法指定一個屬性是否可列舉。實際上根本沒有方法訪問屬性的任何內部特徵。

為了改變這點,ECMAScript 5 引入了多種方法來和屬性特徵值直接互動。

3.6.1 通用特徵

資料屬性和訪問器屬性均由以下兩個屬性特製:

  • [[Enumerable]] 決定了是否可以遍歷該屬性;
  • [[Configurable]] 決定了該屬性是否可配置。

所有人為定義的屬性預設都是可列舉、可配置的。

可以用 Object.defineProperty() 方法改變屬性特徵。

其引數有三:擁有該屬性的物件、屬性名和包含需要設定的特性的屬性描述物件。

var person = {
  name: "Nicholas"
}
Object.defineProperty(person, "name", {
  enumerable: false
})

console.log("name" in person); // true
console.log(person.propertyIsEnumerable("name")); // false

var properties = Object.keys(person);
console.log(properties.length); // 0

Object.defineProperty(person, "name",{
  configurable: false
})

delete person.name; // false
console.log("name" in person); // true

Object.defineProperty(person, "name",{ // error! 
  // 在 chrome:Uncaught TypeError: Cannot redefine property: name
  configurable: true
})
複製程式碼

無法將一個不可配置的屬性變為可配置,相反則可以。

3.6.2 資料屬性特徵

資料屬性額外擁有兩個訪問器屬性不具備的特徵。

  • [[Value]] 包含屬性的值(哪怕是函式)。
  • [[Writable]] 布林值,指示該屬性是否可寫入。所有屬性預設都是可寫的。
var person = {};

Object.defineProperty(person, "name", {
  value: "Nicholas",
  enumerable: true,
  configurable: true,
  writable: true
})
複製程式碼

Object.defineProperty() 被呼叫時,如果屬性本來就有,則會按照新定義屬性特徵值去覆蓋預設屬性特徵(enumberableconfigurablewritable 均為 true)。

但如果用該方法定義新的屬性時,沒有為所有的特徵值指定一個值,則所有布林值的特徵值會被預設設定為 false。即不可列舉、不可配置、不可寫的。

當你用 Object.defineProperty() 改變一個已有的屬性時,只有你指定的特徵會被改變。

3.6.3 訪問器屬性特徵

訪問器屬性額外擁有兩個特徵。[[Get]][[Set]],內含 gettersetter 函式。

使用訪問器屬性特徵比使用物件字面形式定義訪問器屬性的優勢在於:可以為已有的物件定義這些屬性。而後者只能在建立時定義訪問器屬性

var person = {
  _name: "Nicholas"
};

Object.defineProperty(person, "name", {
  get: function(){
    return this._name;
  },
  set: function(value){
    this._name = value;
  },
  enumerable: true,
  configurable: true
})

for(var x in person){
  console.log(x); // _name \n(換行) name(訪問器屬性)
}
複製程式碼

設定一個不可配置、不可列舉、不可以寫的屬性:

Object.defineProperty(person, "name",{
  get: function(){
    return this._name;
  }
})
複製程式碼

對於一個新的訪問器屬性,沒有顯示設定值為布林值的屬性,預設為 false

3.6.4 定義多重屬性

Object.defineProperties() 方法可以定義任意數量的屬性,甚至可以同時改變已有的屬性並建立新屬性。

var person = {};

Object.defineProperties(person, {

  // data property to store data
  _name: {
    value: "Nicholas",
    enumerable: true,
    configurable: true,
    writable: true
  },

  // accessor property
  name: {
    get: function(){
      return this._name;
    },
    set: function(value){
      this._name = value;
    }
  }
})
複製程式碼

3.6.5 獲取屬性特徵

Object.getOwnPropertyDescriptor() 方法。該方法接受兩個引數:物件和屬性名。如果屬性存在,它會返回一個屬性描述物件,內涵 4 個屬性:configurableenumerable,另外兩個屬性則根據屬性型別決定。

var person = {
  name: "Nicholas"
}

var descriptor = Object.getOwnPropertyDescriptor(person, "name");

console.log(descriptor.enumerable); // true
console.log(descriptor.configuable); // true
console.log(descriptor.value); // "Nicholas"
console.log(descriptor.wirtable); // true
複製程式碼

3.7 禁止修改物件

物件和屬性一樣具有指導其行為的內部特性。

其中, [[Extensible]] 是布林值,指明該物件本身是否可以被修改。預設是 true。當值為 false 時,就能禁止新屬性的新增。

建議在 "use strict"; 嚴格模式下進行。

3.7.1 禁止擴充套件

Object.preventExtensions() 建立一個不可擴充套件的物件(即不能新增新屬性)。 Object.isExtensible() 檢查 [[Extensible]] 的值。

var person = {
  name: "Nocholas"
}

Object.preventExtensions(person);

person.sayName = function(){
  console.log(this.name)
}

console.log("sayName" in person); // false
複製程式碼

3.7.2 物件封印

一個被封印的物件是不可擴充套件的且其所有屬性都是不可配置的(即不能新增、刪除屬性或修改其屬性型別(從資料屬性變成訪問器屬性或相反))。只能讀寫它的屬性

Object.seal()呼叫此方法後,該物件的 [[Extensible]] 特徵被設定為 false,其所有屬性的 [[configurable]] 特徵被設定為 false

Object.isSealed() 判斷一個物件是否被封印。

3.7.3 物件凍結

被凍結的物件不能新增或刪除屬性,不能修改屬性型別,也不能寫入任何資料屬性。簡言而之,被凍結物件是一個資料屬性都為只讀的被封印物件。

  • Object.freeze() 凍結物件。
  • Object.isFrozen() 判斷物件是否被凍結。

Object.freeze()Object.seal()更嚴格,它阻止更改任何現有屬性

3.8 總結

  • in 操作符檢測自有屬性和原型屬性,而 hasOwnProperty() 只檢查自有屬性。
  • delete 操作符刪除物件屬性。
  • 屬性有兩種型別:資料屬性和訪問器屬性。
  • 所有屬性都有一些相關特徵。[[Enumerable]][[Configurable]] 的兩種屬性都有的,而資料屬性還有 [[Value]][[Writable]],訪問器屬性還有 [[Get]][[Set]]。可通過 Object.defineProperty()Object.defineProperties() 改變這些特徵。用 Object.getOwnPropertyDescriptor() 獲取它們。
  • 3 種可以鎖定物件屬性的方式。

相關文章