你可能不知道的 Object.defineProperty()

最近在寫一個 《JavaScript API 全解析》系列(剛寫完 String,現正在寫 Object,可戳 JavaScript API 全解析),想把 MDN 推薦使用的 API 所有擼一遍,也算是給本身準備一份資料。由於 Object.defineProperty() 涉及到的知識點比較複雜,因此單獨拎出來放到這裏,歡迎你們拍磚。vue

語法

defineProperty(o: any, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): any;
複製代碼

描述

用於在一個對象上定義新的屬性或修改現有屬性, 並返回該對象.git

參數

  • o 目標對象github

  • p 須要定義的屬性或方法名 (可修改既有的, 也可添加新屬性或方法)web

  • attributes 屬性描述符, 具體屬性以下:面試

interface PropertyDescriptor {
  configurable?: boolean;
  enumerable?: boolean;
  value?: any;
  writable?: boolean;
  get?(): any;
  set?(v: any): void;
}
複製代碼

屬性描述符

ECMAScript 中有兩種屬性: 數據屬性訪問器屬性.數組

數據屬性包括: [[Configurable]], [[Enumerable]], [[Writable]], [[Value]]瀏覽器

訪問器屬性包括: [[Configurable]], [[Enumerable]], [[Get]], [[Set]]mvvm

屬性描述符可同時具備的鍵值

configurable enumerable value writable get set
數據屬性 Yes Yes Yes Yes No No
訪問器屬性 Yes Yes No No Yes Yes

簡言之, 定義了 valuewritable , 必定不能有 getset, 反之亦然, 不然報錯.函數

描述符可同時具備的鍵值

Configurable

若是某個屬性的 configurable 爲false, 那麼:post

  1. 將不能刪除此屬性, 即 delete obj.xxx 無效, 在嚴格模式下直接報錯.
// 非嚴格模式下刪除一個"不可配置"的屬性會返回false
const obj = {};

Object.defineProperty(obj, 'name', {
  value: 'yancey',
  configurable: false,
});

delete obj.name; // false

// obj.name並無被刪除
obj.name; // yancey
複製代碼
// 嚴格模式下刪除一個"不可配置"的屬性直接報錯
(function() {
 'use strict';
  var o = {};
  Object.defineProperty(o, 'b', {
    value: 2,
    configurable: false,
  });
  delete o.b; // Uncaught TypeError: Cannot delete property 'b' of #<Object>
  return o.b;
})();
複製代碼
  1. 當 enumerable 或 writable 是false時, 再次將它們變成true則報錯; 但當它們是true時, 卻能夠把它們變成false ( 注意必須是在不可配置的前提下, 若是屬性可配置, enumerable 和 writable 可任意切換 true 和 false)
const obj = {};

Object.defineProperty(obj, 'name', {
  value: 'yancey',
  configurable: false,
  writable: false,
});

// 當"writable"和"configurable"均爲false時, 嘗試將"writable"變爲true會報錯
// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(obj, 'name', { writable: true });
複製代碼
const obj = {};

Object.defineProperty(obj, 'name', {
  value: 'yancey',
  configurable: false,
  writable: true,
});

// 但"writable"可成功從true切換到false
Object.defineProperty(obj, 'name', { writable: false });
複製代碼
  1. 不管如何再次修改getset都會報錯, 由於二者的屬性值是一個函數,在 JS 中不可能存在一個相同的函數。

:::tip REVIEW 複雜數據類型在中存儲數據名和一個堆的地址, 在中存儲屬性. 訪問時先從棧獲取地址, 再到堆中拿出相應的值. :::

const obj = {};

Object.defineProperty(obj, 'name', {
  value: 'yancey',
  configurable: false,
});

// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(obj, 'name', { get: function() {} });

// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(obj, 'name', { set: function() {} });
複製代碼
  1. 只要writable 是 true, 能夠任意從新定義 value, 但當writable是 false 時, 須要看具體數據類型. 第一個例子中, 雖然 configurable 是 false, 但只要 writable 是 true, 即可以從新定義 value; 第二個例子中, value 是 基本數據類型, 因此再次定義 value 時只要覆蓋原值便可; 第三個例子 value 是複雜數據類型, 一樣由於 堆棧 問題而不能從新賦值.
const obj = {};

Object.defineProperty(obj, 'name', {
  value: [],
  configurable: false,
  writable: true,
});

// 任意重定義value不報錯
Object.defineProperty(obj, 'name', { value: 123 }); // {name: 123}

// 任意重定義value不報錯
Object.defineProperty(obj, 'name', { value: {}); // {name: {}}
複製代碼
const obj = {};

Object.defineProperty(obj, 'name', {
  value: 123,
  configurable: false,
  writable: false,
});

// 當value是基本數據類型, 用原值覆蓋不會報錯
Object.defineProperty(obj, 'name', { value: 123 }); // {name: 123}

// 用其餘值代替必然報錯
Object.defineProperty(obj, 'name', { value: {}); // Uncaught TypeError: Cannot redefine property: name
複製代碼
const obj = {};

Object.defineProperty(obj, 'name', {
  value: [],
  configurable: false,
  writable: false,
});

// 當value是複雜數據類型, 修改value一定報錯, 一樣是堆棧的緣由
Object.defineProperty(obj, 'name', { value: [] }); // {name: 123}
複製代碼

Writable

若是某個屬性的writable設爲false, 那麼該屬性將不能被賦值運算符改變. 但屬性值假如是數組時, 將不受 push, splice等方法的影響.

const obj = {};

Object.defineProperty(obj, 'hobby', {
  value: ['girl', 'music', 'sleep'],
  writable: false,
  configurable: true,
  enumerable: true,
});

// "writable: false"並不對push、shift等方法起做用
obj.hobby.push('drink');
obj.hobby; // ['girl', 'music', 'sleep', 'drink']

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// 當 hobby 被"賦值"給一個空數組時, 此屬性的屬性值不會被改變
obj.hobby = [];
obj.hobby; // ['girl', 'music', 'sleep', 'drink']

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// 而當使用"嚴格模式"時, 給一個"不可寫"屬性賦值將直接報錯
(function() {
 'use strict';
  var o = {};
  Object.defineProperty(o, 'b', {
    value: 2,
    writable: false
  });
  o.b = 3; // throws TypeError: "b" is read-only
  return o.b; // 2
}());
複製代碼

Enumerable

定義了對象的屬性是否能夠在 for...in 循環和 Object.keys() 中被枚舉

const obj = {
  name: 'yancey',
  age: 18,
  say() {
    return 'say something...';
  },
};

Object.defineProperty(obj, 'hobby', {
  value: ['girl', 'music', 'sleep'],
  enumerable: true,
});

Object.defineProperty(obj, 'income', {
  value: '100,00,000',
  enumerable: false,
});

// 如下迭代器均不能輸出"不可枚舉屬性", 即 income 的相關信息
for (const i in obj) {
  console.log(obj[i]);
}
Object.keys(obj);
Object.values(obj);
Object.entries(obj);
複製代碼

Getter & Setter

Getter 爲讀取屬性時調用的函數. Setter 爲設置屬性是調用的函數, Setter 會有一個參數, 即設置的那個值.

下面的代碼建立一個 obj 對象, 定義了兩個屬性 name 和 _time, 注意 _time 的下劃線是一個經常使用記號, 用於表示只能經過對象方法訪問的屬性. 而訪問器屬性 time 則包含一個 getter 函數和一個 setter 函數. getter 函數返回被修飾的 _time 的值, setter 則根據被設置的值修改 name. 所以當obj.time = 2, name 會變成我爲長者+2s. 這是使用訪問器屬性的常見方式, 即設置一個屬性的值會致使其餘屬性發生變化.

const obj = {
  name: '長者',
  _time: 1,
};

Object.defineProperty(obj, 'time', {
  configurable: true,
  get() {
    return `default: ${this._time}s`;
  },
  set(newValue) {
    if (Number(newValue)) {
      this._time = newValue;
      this.name = `我爲${this.name}+${newValue}s`;
    }
  },
});

obj.time; // 'default: 1s'
obj.time = 2; // 2
obj.name; // '我爲長者+2s'
複製代碼

再看另外一個例子, 經過 Object.defineProperty 劫持 obj.input, 將輸入的值 set 到 id 爲 name 的標籤裏. 這裏便有了種 Vue.js 的味道, 推薦一篇文章 剖析 Vue 實現原理 - 如何實現雙向綁定 mvvm.

<p>Hello, <span id='name'></span></p>
<input type='text' id='input'>

const obj = {
  input: '',
};

const inputDOM = document.getElementById('input');
const nameDOM = document.getElementById('name');

inputDOM.addEventListener('input', function (e) {
  obj.input = e.target.value;
})

Object.defineProperty(obj, 'input', {
  set: function (newValue) {
    nameDOM.innerHTML = newValue.trim().toUpperCase();
  }
})
複製代碼

MVVM?

最後看一個關於繼承的例子, 咱們建立了一個 Person 構造函數, 它包括兩個參數: firstNamelastName, 此構造函數暴露出四個屬性: firstName, lastName, fullName, species, 咱們想讓前三個屬性動態變化, 最後一個屬性是一個常量而不容許變化.

下面這段代碼顯然沒有達到想要的效果: 在嘗試修改 firstNamelastName 時, fullName 並無實時被更新; species屬性能隨意被改變.

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.fullName = this.firstName + ' ' + this.lastName;
  this.species = 'human';
}

const person = new Person('Yancey', 'Leo');

// 雖然 firstName 和 lastName 被修改了, 但 fullName 仍然是 "Yancey Leo"
person.firstName = 'Sayaka';
person.lastName = 'Yamamoto';

// 咱們定義了一個關於「人」的構造函數, 因此並不但願 species 被修改爲 fish
person.species = 'fish';

// 當咱們修改了 fullName, 也一樣但願 firstName 和 lastName 被更新
person.fullName = 'Kasumi Arimura';
複製代碼

因此咱們使用 Object.defineProperty() 重寫這個例子. 須要注意的是: 被劫持的屬性應放在原型裏. 經過下面這種方式, 即便建立多個實例, 也不會衝突, 因此能夠放心使用.

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Object.defineProperty(Person.prototype, 'species', {
  value: 'human',
  writable: false,
});

Object.defineProperty(Person.prototype, 'fullName', {
  get() {
    return this.firstName + ' ' + this.lastName;
  },
  set(newValue) {
    const newValueArr = newValue.trim().split(' ');
    if (newValueArr.length === 2) {
      this.firstName = newValueArr[0];
      this.lastName = newValueArr[1];
    }
  },
});

const person = new Person('Yancey', 'Leo');

person.firstName = 'Sakaya';
person.lastName = 'Yamamoto';
person.fullName; // 'Sayaka Yamamoto'

person.fullName = 'Kasumi Arimura';
person.firstName; // 'Kasumi'
person.lastName; // 'Arimura'

person.species = 'fish';
person.species; // 'human'
複製代碼

擴展

除了 Object.defineProperty() 中的 Getter 和 Setter, 還有兩種相似的方式.

__defineGetter__ 和 __defineSetter__()

__defineGetter__ 方法能夠爲一個已經存在的對象設置 (新建或修改) 訪問器屬性, __defineSetter__ 方法能夠將一個函數綁定在當前對象的指定屬性上, 當那個屬性被賦值時, 你所綁定的函數就會被調用.

var o = {};
o.__defineGetter__('gimmeFive', function() {
  return 5;
});
o.gimmeFive; // 5
複製代碼

:::danger 該特性是非標準的, 請儘可能不要在生產環境中使用它!

該特性已經從 Web 標準中刪除, 雖然一些瀏覽器目前仍然支持它, 但也許會在將來的某個時間中止支持, 請儘可能不要使用該特性. :::

對象字面量中的 get 語法

對象字面量中的 get 語法只能在新建一個對象時使用.

var o = {
  get gimmeFive() {
    return 5;
  },
};
o.gimmeFive; // 5
複製代碼

參考

Vue 核心之數據劫持

不會 Object.defineProperty 你就 out 了

vue.js 關於 Object.defineProperty 的利用原理

面試官: 實現雙向綁定 Proxy 比 defineproperty 優劣如何?

JAVASCRIPT ES5: MEET THE OBJECT.DEFINEPROPERTY() METHOD

相關文章
相關標籤/搜索