JS基礎進階—繞不開的Object.defineProperty()

Object.defineProperty()來自 ECMAScript 5.1 (ECMA-262) 規(guī)范
兼容 Internet Explorer 9+ 等其他現(xiàn)代瀏覽器

初識Object.defineProperty()

??Object.defineProperty() 方法用于在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性, 并返回這個對象,該方法允許精確添加或修改對象的屬性。
??乍看描述,也許會讓人不太理解,在對象上定義屬性我們通常使用對象字面量語法或者.操作符來處理,比如

  var obj = {}; obj.attr = 1;
  // 或是
  var obj2 = {attr: 2};

??該方法的重要意義在于后面一句,即該方法允許精確添加或修改對象的屬性
??舉個栗子,僅用于初步體會該方法的效果。我們有時候會需要遍歷一個對象的所有屬性,這時可能會使用for in方法

  var obj = {
    name: 'sky',
    age: 18,
    sex: 'male'
  }
  for (var i in obj) {
    console.log(i);
  }
  // 打印結果
  // name
  // age
  // sex

??我們知道for in方法會遍歷對象和其原型上所有可枚舉的屬性,我們自己創(chuàng)建的對象的原型是Object.prototype,該原型上有一個可能常用到的方法Object.prototype.toString(),通常我們用這個方法準確判斷數(shù)據的類型。我們發(fā)現(xiàn)這個方法并沒有被遍歷出來,如前所述,這個方法顯然是不可遍歷的。那么我們如何自己實現(xiàn)為屬性賦予這些‘’精確‘’特性的效果呢。這就輪到Object.defineProperty()出場了。
??在此之前,我們再看一些“有趣”的東西,先不用理解具體原理,我們在上面程序后面加上一句

console.log(Object.getOwnPropertyDescriptor(obj.__proto__, 'toString'));

??打開控制臺發(fā)現(xiàn)了如下輸出



??發(fā)現(xiàn)得到了一些有趣的東西,enumerable(可枚舉的)屬性是false,這似乎就和之前的現(xiàn)象產生了聯(lián)系。

認識Object.defineProperty()

語法:

Object.defineProperty(obj, prop, descriptor)

參數(shù):
obj 要在其上定義屬性的對象。
prop要定義或修改的屬性的名稱。
descriptor將被定義或修改的屬性描述符。

返回: 被傳遞給函數(shù)的對象

我們發(fā)現(xiàn)重點就在第三個參數(shù),屬性描述符上面。

屬性描述符

在理解屬性描述符之前,需要先了解一點前置知識。

getter 和 setter

對象屬性是由名字、值和一組特性構成的。在ES5中,屬性值可以用一個或兩個方法替代,這兩個方法就是getter和setter。由getter和setter定義的屬性稱做 "存取器屬性",它不同于 "數(shù)據屬性",數(shù)據屬性就是一個簡單的值。

var prop = {
    a: 0,
    get b(){
        return 1;
    }   
};

console.log(prop.a); //0
console.log(prop.b); //1

??上面代碼中,屬性a稱為“數(shù)據屬性”,它只有一個簡單的值;像屬性b這種用getter和setter方法定義的屬性稱為“存取器屬性”。當一個屬性被定義為存取器屬性時,JavaScript會忽略它的value和writable特性,取而代之的是set和get(還有configurable和enumerable)特性。
??當程序查詢存取器屬性的值時,JS調用getter方法(無參數(shù))。這個方法的返回值就是屬性存取表達式的值。
??當程序設置一個存取器屬性的值時,JS調用setter方法,將賦值表達式右側的值當做參數(shù)傳入setter。從某種意義上講,這個方法負責 "設置"屬性值??梢院雎詓etter方法的返回值。
??如果屬性同時具有getter和setter,那么它是一個讀/寫屬性。如果它只有getter方法,那么它是一個只讀屬性。如果它只有setter方法,那么它是一個只寫屬性,讀取只寫屬性總是返回undefined。

var prop = {
    get b(){
        return 1;
    }   
};
prop.b = 3; // 這個設置是無效的
console.log(prop.b); //1

和數(shù)據屬性一樣,存取器屬性是可以繼承的。且從 ECMAScript 2015 開始,還可以使用一個計算屬性名的表達式綁定到給定的函數(shù)。這里不再贅述。

屬性描述符的配置

??對象里目前存在的屬性描述符有兩種主要形式:數(shù)據描述符和存取描述符。數(shù)據描述符是一個具有值的屬性,該值可能是可寫的,也可能不是可寫的。存取描述符是由getter-setter函數(shù)對描述的屬性。描述符必須是這兩種形式之一,不能同時是兩者。
??相信在理解了前置知識后,這一段也是很清晰明了的。
??以下相關定義摘自MDN

數(shù)據描述符存取描述符均具有以下可選鍵值:
??configurable
??當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除。默認為 false。
??enumerable
??當且僅當該屬性的enumerable為true時,該屬性才能夠出現(xiàn)在對象的枚舉屬性中。默認為 false。

數(shù)據描述符同時具有以下可選鍵值:
??value
??該屬性對應的值。可以是任何有效的 JavaScript 值(數(shù)值,對象,函數(shù)等)。默認為 undefined。
??writable
??當且僅當該屬性的writable為true時,value才能被賦值運算符改變。默認為 false。

存取描述符同時具有以下可選鍵值:
??get
??一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。默認為 undefined。
??set
??一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一參數(shù),并將該參數(shù)的新值分配給該屬性。默認為 undefined。

描述符可同時具有的鍵值

configurable enumerable value writable get set
數(shù)據描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

如果一個描述符不存在value,writable,configurable和enumerable關鍵字中任意一個,那么它將被認為是一個數(shù)據描述符。如果一個描述符同時有(value或writable)和(get或set)關鍵字,將會產生一個異常。

實際使用

不要忽視繼承

??首先需要注意的是用作屬性描述符的對象如果繼承了某些關鍵字屬性,可能會影響正常的使用,來看一個例子

  var a = {};
  var descriptor = {
    value: 2
  };
  descriptor.__proto__ = {
    writable: true
  };
  Object.defineProperty(a, 'test', descriptor);
  console.log(a.test); // 2
  a.test = 3;
  console.log(a.test); // 3

??在例子中我們?yōu)橛米鲗傩悦枋龇膁escriptor添加了value屬性,而如之前所介紹的,如果其他鍵值沒有設置,則默認為false(此時該描述符被認為是一個數(shù)據描述符)。這時我們?yōu)閍.test屬性賦值,發(fā)現(xiàn)賦值沒有按照預期失敗,而是成功了。顯然是因為descriptor繼承了原型上的writable屬性,因此屬性a變?yōu)榭蓪懙摹?br> ??你可能認為這種更改顯而易見,但實際項目中往往較為復雜,提前意識到這些問題往往可以省下很多時間。

創(chuàng)建屬性

??如果對象中不存在指定的屬性,Object.defineProperty()就創(chuàng)建這個屬性。當描述符中省略某些字段時,這些字段將使用它們的默認值。擁有布爾值的字段的默認值都是false。value,get和set字段的默認值為undefined。一個沒有get/set/value/writable定義的屬性將被當做數(shù)據描述符處理。

  var obj = {}; // 創(chuàng)建一個新對象
  // 為新對象添加屬性及其屬性描述符(數(shù)據描述符)
  Object.defineProperty(obj, 'prop', {
    value: 10,
    writable : true,
    enumerable : true,
    configurable : true
  });
  // 此時對象obj擁有了屬性prop,值為10,且可修改
  console.log(obj.prop); // 10
  obj.prop = 20;
  console.log(obj.prop); // 20
  var obj2 = {}; // 創(chuàng)建一個新對象
  // 為新對象添加屬性及其屬性描述符(存取描述符)
  var val;
  Object.defineProperty(obj, 'prop', {
    get : function(){
      return val;
    },
    set : function(newValue){
      val = newValue;
    },
    enumerable : true,
    configurable : true
  });
  obj2.prop = 20; // 為obj2設置值為20
  console.log(obj2.prop); // 20

對于enumerableconfigurable屬性,將在后續(xù)詳細說明。

修改屬性

??如果屬性已經存在,Object.defineProperty()將嘗試根據描述符中的值以及對象當前的配置來修改這個屬性。如果舊描述符將其configurable 屬性設置為false,則該屬性被認為是“不可配置的”,并且沒有屬性可以被改變(除了單向改變 writable 為 false)。當屬性不可配置時,不能在數(shù)據和訪問器屬性類型之間切換。

  var obj = {}; // 創(chuàng)建一個新對象
  // 為新對象添加屬性及其屬性描述符
  Object.defineProperty(obj, 'prop', {
    value: 10,
    writable : true,
    enumerable : true,
    configurable : true
  });
  // 更改該對象的屬性描述符
  Object.defineProperty(obj, 'prop', {
    writable : false
  });
  console.log(obj.prop); // 10
  // Object.getOwnPropertyDescriptor()方法返回指定對象上一個自有屬性對應的屬性描述符。
  //(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找的屬性)
  console.log(Object.getOwnPropertyDescriptor(obj, 'prop'));
  // {value: 10, writable: false, enumerable: true, configurable: true}

??當試圖改變不可配置屬性(除了單向改變 writable 為 false)的值時會拋出錯誤,除非當前值和新值相同。

  var obj = {}; // 創(chuàng)建一個新對象
  // 為新對象添加屬性及其屬性描述符
  Object.defineProperty(obj, 'prop', {
    value: 10,
    writable : true,
    enumerable : true,
    configurable : false
  });
  // 在configurable為false時重新更改屬性描述符
  // 注意,如前所述,當將writable從true改為false是允許的
  Object.defineProperty(obj, 'prop', {
    enumerable : false
  });
  // Uncaught TypeError: Cannot redefine property: prop

??下面具體解釋屬性描述符的可選鍵值
??例子引用自MSDN:

Writable 屬性

??當writable屬性設置為false時,該屬性被稱為“不可寫”。此時無法為屬性賦一個新值。在非嚴格模式下,試圖寫入非可寫屬性不會改變它,也不會引發(fā)錯誤。

  var o = {}; // 創(chuàng)建一個新對象

  Object.defineProperty(o, 'a', {
    value: 37,
    writable: false
  });

  console.log(o.a); // 37
  o.a = 25; // 此時不會拋出錯誤
  // 如果在嚴格模式下,即使重新賦的值與原來相同,仍舊會拋出錯誤
  console.log(o.a); // 37 重新賦值不會起作用

  // 嚴格模式下
  (function() {
    'use strict';
    var o = {};
    Object.defineProperty(o, 'b', {
      value: 2,
      writable: false
    });
    o.b = 3; // throws TypeError:  Cannot assign to read only property 'b' of object '#<Object>'
    return o.b; // 2
  }());
Enumerable 屬性

??enumerable定義了對象的屬性是否可以在 for...in循環(huán)和 Object.keys()中被枚舉。

  var o = {};
  Object.defineProperty(o, "a", { value : 1, enumerable:true });
  Object.defineProperty(o, "b", { value : 2, enumerable:false });
  Object.defineProperty(o, "c", { value : 3 }); // enumerable 的默認值為false
  o.d = 4; // 如果使用直接賦值的方式創(chuàng)建對象的屬性,則這個屬性的enumerable為true

  for (var i in o) {
    console.log(i);  // 打印 'a' 和 'd'
  }

  console.log(Object.keys(o)); // ["a", "d"]
  // propertyIsEnumerable() 方法返回一個布爾值,表示指定的屬性是否可枚舉。
  o.propertyIsEnumerable('a'); // true
  o.propertyIsEnumerable('b'); // false
  o.propertyIsEnumerable('c'); // false
Configurable 屬性

??configurable屬性性表示對象的屬性是否可以被刪除,以及除writable屬性外的其他屬性是否可以被修改。

  var o = {};
  Object.defineProperty(o, "a", {
    get: function () {
      return 1;
    },
    configurable: false
  });

  // 拋出一個錯誤
  Object.defineProperty(o, "a", { configurable: true });
  // 拋出一個錯誤
  Object.defineProperty(o, "a", { enumerable: true });
  // 拋出一個錯誤 (set 熟悉之前未定義)
  Object.defineProperty(o, "a", {
    set: function () {
    }
  });
  // 拋出一個錯誤 (即使get屬性做了與之前相同的事情)
  Object.defineProperty(o, "a", {
    get: function () {
      return 1;
    }
  });
  // 拋出一個錯誤
  Object.defineProperty(o, "a", { value: 12 });

  console.log(o.a); // 1
  delete o.a; // 不會起作用,嚴格模式下拋出錯誤TypeError: Cannot delete property 'a' of #<Object>
  console.log(o.a); // 1

??如果o.a的configurable屬性為true,則不會拋出任何錯誤,并且該屬性將在最后被刪除。

set和get

??在了解了getter和setterd的原理和使用之后,這里也便理解了get和set屬性的用法。
??同樣使用MSDN上的例子,例子展示了如何實現(xiàn)一個自存檔對象。 當設置temperature 屬性時,archive 數(shù)組會獲取日志條目。

  function Archiver() {
    var temperature = null;
    var archive = [];

    Object.defineProperty(this, 'temperature', {
      get: function () {
        console.log('get!');
        return temperature;
      },
      set: function (value) {
        temperature = value;
        archive.push({ val: temperature });
      }
    });

    this.getArchive = function () {
      return archive;
    };
  }

  var arc = new Archiver();
  arc.temperature; // 'get!'
  arc.temperature = 11;
  arc.temperature = 13;
  arc.getArchive(); // [{ val: 11 }, { val: 13 }]
需要注意的地方

使用點.運算符和Object.defineProperty()為對象的屬性賦值時,數(shù)據描述符中的屬性默認值是不同的,如下例所示。

  var o = {};
  o.a = 1;
  // 等同于
  Object.defineProperty(o, "a", {
    value: 1,
    writable: true,
    configurable: true,
    enumerable: true
  });

  // 另一方面,
  Object.defineProperty(o, "a", { value: 1 });
  // 等同于
  Object.defineProperty(o, "a", {
    value: 1,
    writable: false,
    configurable: false,
    enumerable: false
  });

相關的一些api

1.Object.defineProperties()的用法和Object.defineProperty()基本一樣,不同的是可以為對象的多個屬性添加描述符。

  var obj = {};
  Object.defineProperties(obj, {
    'prop1': {
      value: 1,
      writable: true
    },
    'prop2': {
      value: 2,
      writable: false
    }
  });

2.Object.getOwnPropertyDescriptor()方法返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找)。在一開始舉例的時候就用到了這個api。
3.Object.getOwnPropertyDescriptors()方法用來獲取一個對象的所有自身屬性的描述符。

Object.defineProperty()的重要意義

??個人認為理解掌握Object.defineProperty()對于JS從基礎走向進階具有幫助作用:
??1.了解框架原理。如Vue使用Object.defineProperty用于構成其響應式系統(tǒng)的一部分。
??2.閱讀框架源碼。Object.defineProperty是你在閱讀許多框架源碼無法繞過的一個api。
??3.了解babel語法轉換器轉譯原理。babel的語法轉換經常用到Object.defineProperty。
??4.做更多你想做的事:)

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容