要看透 "對象" 的本質

前言

我是一個前端菜鳥,某一天,我發現了新大陸,我發現了 《ECMAScript 規範》 這個東西;javascript

頭皮發麻~html

本文內容較多,請謹慎收看(我的筆記,我的理解都加在了上面,可能有些囉嗦);前端

我是一個菜雞,理解有限,大佬輕噴。java

本文目的:數組

  1. 理解對象屬性的本質
  2. 理解屬性描述符
  3. 從宏觀理解 Object

1. 知其然,知其因此然。

有沒有想過 JavaScript 中的 Object 究竟是個什麼東西

在沒有探究 Object 以前,我是這樣理解 Object 的:瀏覽器

  • 數據的無序集合,裏面包含 N 多個字段,每一個字段保存對應的信息,而對象就是這些個信息無序集合體,何爲無序?
  • 無序 故名思議:沒有順序的,有 「無序」,就存在 「有序」。

"有序","無序" 個人理解是:bash

  • 數組是數據的有序集合,N 多個數據按照數組的索引井井有理的排列,咱們能夠按照 「索引」 獲取指定位置的數據。
  • Object 而言,Object 內部字段是由一個個鍵值對構成的,鍵是字段的名字,值也就是該字段的數據;
    • 在獲取的時候,採用 Object[keyName] 的方式,也就獲取到對應的字段存儲的數據了。
    • 不管字段在對象內部的位置,都是採用 Object.keyName 的方式獲取的,故對象內部是沒有順序的。
// 一個叫 person 的 Object,裏面有三個字段,包含不一樣的數據,他們都屬於 person 這個 Object
var person = {
  name: "Jack",
  age: 18,
  sex: "man"
};
複製代碼

2. ECMAScript 規範

ECMAScript 規範是全部 JavaScript 運行行爲的權威來源;
不管是在你的瀏覽器環境、仍是在服務器環境( Node.js )、仍是在宇航服上[ NodeJS-NASA ]、或在你的物聯網設備上[ JOHNNY-FIVE ];
全部 JavaScript 引擎的開發者都依賴於這個規範來確保他們各類天花亂墜的新特性可以其餘 JavaScript 引擎同樣,按預期工做。

ECMAScript 規範 毫不僅僅對 JavaScript 引擎開發者有用,
它對普通的 JavaScript 編碼人員也很是有用,而你只是沒有意識到或者沒有用到。

Ps:你所用的 JavaScript,都是由這個規範來規定的,它說什麼就是什麼。
複製代碼

3. 從規範角度看對象 👍

在 ECMAScript 規範中是這樣定義對象的:服務器

Object 是一個屬性的集合。每一個屬性既能夠是一個命名的數據屬性,也能夠是一個命名的訪問器屬性,或是一個內部屬性app

  • 命名數據屬性: 由一個名字與一個 ECMAScript 語言類型值和一個 Boolean 屬性集合組成。
  • 命名訪問器屬性: 由一個名字與一個或兩個訪問器函數,和一個 Boolean 屬性集合組成。訪問器函數用於存取一個與該屬性相關聯的 ECMAScript 語言類型 值。
  • 內部屬性: 沒有名字,且不能直接經過 ECMAScript 語言操做。內部屬性的存在純粹爲了規範的目的;

有兩種帶名字的訪問器屬性(非內部屬性):get 和 put,分別對應取值和賦值。函數

規範就是規範,晦澀難懂。 😯

4. 從屬性出發,屬性是什麼? 😳

咱們已知 Object 是屬性的集合。是否有過這個疑問,屬性又是什麼呢?或許規範已經給了咱們答案,告訴了咱們屬性是怎麼構成的。可是,我沒明白。。。

好,咱們一步步分析下規範,回答下 屬性是什麼:

答案 1:屬性是由一個名字與一個 ECMAScript 語言類型值和一個 Boolean 屬性集合組成。

  • 一個名字: 顯而易見使咱們所熟知的 Key
  • 一個 ECMAScript 語言類型值: 也就是值,語言類型值也就是指 String、Number、Blooean、Object、Function... 等等 JavaScript 的語言類型。
  • 一個 Boolean 屬性集合:??????????????

問題來了,什麼是 Boolean 屬性集合? 是否是歷來沒見過這個東西?


答案 2:由一個名字與一個或兩個訪問器函數,和一個 Boolean 屬性集合組成。訪問器函數用於存取一個與該屬性相關聯的 ECMAScript 語言類型 值。

  • 一個名字: 相同的
  • 一個或兩個訪問器函數:??????????????????
  • 一個 Boolean 屬性集合:??????????????

問題又來了,訪問器函數? 聽都沒據說過? 更沒見過? 這個答案也有 Boolean 屬性集合?

再來看一下咱們平常是怎麼使用 Object 的:

// 表達式建立一個對象
var busInfoForm = {
  treeId: "",
  vcName: "",
  parentId: "0",
  unitId: "",
  iBindType: "",
  bindId: ""
};

// 查 - 獲取一個屬性
console.log(busInfoForm.treeId);

// 改 - 設置一個屬性
busInfoForm.vcName = "測試的值";

// 刪 - 刪除一個屬性
delete busInfoForm.bindId;

// 增 - 增長一個屬性
busInfoForm["obj"] = {
  a: "this is obj a"
};

// 遍歷屬性
for (let k in busInfoForm) {
  console.log(k, "-----", busInfoForm[k]);
}
複製代碼

咱們對於數據的操做,無非 增刪改查 定律,上面的例子體現了增刪改查,以及枚舉對象的屬性操做. 咱們能夠看到對象屬性的 名字、值, 但咱們沒有看到 Boolean 集合,也沒有看到訪問器函數。

5. 屬性描述符

這時候,咱們須要知道另外一個概念:屬性描述符

在 MDN 中對於 屬性描述符 的定義以下: 對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。

  • 數據描述符 是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。
  • 存取描述符 是由 getter-setter 函數對描述的屬性。
  • 描述符必須是這兩種形式之一;不能同時是二者。

好吧,是否是懵了,屬性描述符 又是什麼?

繼續看 MDN 上的對於屬性描述符的介紹:

數據描述符和存取描述符均具備如下可選鍵值(默認值是在使用 Object.defineProperty()定義屬性的狀況下):

  • configurable
    • 當且僅當該屬性的 configurable 爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false。
  • enumerable
    • 當且僅當該屬性的 enumerable 爲 true 時,該屬性纔可以出如今對象的枚舉屬性中。默認爲 false。

數據描述符同時具備如下可選鍵值:

  • value
    • 該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined。
  • writable
    • 當且僅當該屬性的 writable 爲 true 時,value 才能被賦值運算符改變。默認爲 false。

存取描述符同時具備如下可選鍵值:

  • get
    • 一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,可是會傳入 this 對象(因爲繼承關係,這裏的 this 並不必定是定義該屬性的對象)。默認爲 undefined。
  • set
    • 一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受惟一參數,即該屬性新的參數值。默認爲 undefined。
configurable enumerable value writable get set
數據描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

若是一個描述符不具備 value,writable,get 和 set 任意一個關鍵字,那麼它將被認爲是一個數據描述符。若是一個描述符同時有(value 或 writable)和(get 或 set)關鍵字,將會產生一個異常。


OK, 屬性描述符的全部 "屬性" 都羅列在了這裏,白話一下:

  • 對於對象中的每個屬性,都有其本身的 「描述符」 ,所謂的描述符,也就是用來描述屬性的行爲的,描述屬性是否能夠增刪改查,以及如何增刪改查。
  • 描述符就像屬性的輔助對象,這個對象和 Object 的屬性結構同樣由鍵值對構成,具體包含哪些鍵值對,以下所示:
  • 數據描述符: 包含 configurable、enumerable、value、writable
  • 存取描述符: 包含 configurable、enumerable、get、set
  • 能夠看到,configurable、enumerable 是都含有的。
  • 而對於 數據描述符 而言,除了公有的,只能存在 value && writable,反過來 存取描述符 只能存在 get、set,這四個屬性兩兩之間是不能共存的。

6. 操做屬性描述符

6.1 Object.defineProperty()

如今,咱們已經有了數據描述符的這個概念,下面來看下 Object.defineProperty() 這個方法

  • 定義:

    • Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
  • 語法:

    • Object.defineProperty(obj, prop, descriptor)
  • 參數:

    • obj: 要在其上定義屬性的對象
    • prop:要定義或修改的屬性的名稱
    • descriptor:將定義或修改的屬性描述符
  • 返回值

    • 被傳遞給函數的對象,原對象

描述:

  • 該方法容許精確添加或修改對象的屬性。
  • 經過賦值操做添加的普通屬性是可枚舉的,可以在屬性枚舉期間呈現出來(for...in 或 Object.keys 方法), 這些屬性的值能夠被改變,也能夠被刪除。
  • 這個方法容許修改默認的額外選項(或配置)。
  • 默認狀況下,使用 Object.defineProperty() 添加的屬性值是不可修改的。

咱們要操做屬性描述符就得使用這個方法。

6.2 Object.getOwnPropertyDescriptor()

defineProperty 這個方法使用來設置屬性描述的,對應的 getOwnPropertyDescriptor 就是用來獲取屬性描述符的。

  • 定義:
    • Object.getOwnPropertyDescriptor() 方法返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不須要從原型鏈上進行查找的屬性)
  • 語法:
    • Object.getOwnPropertyDescriptor(obj, prop)
  • 參數:
    • obj:須要查找的目標對象
    • prop:目標對象內屬性名稱
  • 返回值:
    • 若是指定的屬性存在於對象上,則返回其屬性描述符對象(property descriptor),不然返回 undefined。

描述:

  • 該方法容許對一個屬性的描述進行檢索。

6.3 上代碼 🔠 🔠 🔠

6.3.1 建立屬性

  • 若是對象中不存在指定的屬性,Object.defineProperty()就建立這個屬性。
  • 當描述符中省略某些字段時,這些字段將使用它們的默認值。
  • 擁有布爾值的字段的默認值都是 false。value,get 和 set 字段的默認值爲 undefined。
  • 一個沒有 get/set/value/writable 定義的屬性被稱爲「通用的」,並被「鍵入」爲一個數據描述符。
// 使用表達式的方式建立一個新對象
var person = {
  name: "Jack"
};

// 使用 getOwnPropertyDescriptor 獲取 name 的屬性描述符
console.log(Object.getOwnPropertyDescriptor(person, "name")); // =>
// {value: "Jack", writable: true, enumerable: true, configurable: true}
複製代碼
  • 能夠看到 經過賦值操做添加的普通屬性,其屬性描述符包含 configurable、enumerable、value、writable
  • 默認爲 數據描述符
  • 且這些值除 value 外,皆爲 true,說明該屬性能夠被 刪除、修改、枚舉
// 使用 defineProperty 在對象中添加一個屬性 - 屬性描述符取默認
Object.defineProperty(person, "sex", {});

console.log(person); // => {name: "Jack", sex: undefined}

console.log(Object.getOwnPropertyDescriptor(person, "sex")); // =>
// {value: undefined, writable: false, enumerable: false, configurable: false}
複製代碼
  • 使用 defineProperty 爲 person 添加了一個 'sex' 屬性
  • 第三個參數,傳入了一個空對象
  • 可見該屬性的 屬性描述符 都取了默認值,且該屬性默認爲 數據描述符
person.sex = "man";
delete person.sex;

console.log(person); // {name: "Jack", sex: undefined}
複製代碼
  • 因爲在 sex 在建立時,屬性描述符都取的默認值,writable 爲 false,故不能寫入
  • 一樣它也不可被枚舉、刪除
// 使用 defineProperty 在對象中添加一個屬性 - 配置屬性描述符
Object.defineProperty(person, "age", {
  value: 18,
  writable: true,
  enumerable: true,
  configurable: true
});
console.log(person); // {name: "Jack", age: 18, sex: undefined}

person.age = 16;
console.log(person); // {name: "Jack", age: 16, sex: undefined}

delete person.age;
console.log(person); // {name: "Jack", sex: undefined}
複製代碼
  • 使用 defineProperty 爲對象添加了一個 'age' 屬性。且配置了屬性描述符
  • 能夠看到對象多了一個 鍵爲 "age" -- 值爲 18 的屬性
  • 同時,這個值能夠被修改、刪除、枚舉
// 在對象中添加一個 屬性與存取描述符
var hobbyValue = [];
Object.defineProperty(person, "hobby", {
  get: function() {
    return hobbyValue;
  },
  set: function(newValue) {
    hobbyValue.push(newValue);
  },
  enumerable: true,
  configurable: true
});

console.log(person.hobby); // => []

person.hobby = "吃飯";
person.hobby = "睡覺";
person.hobby = "碼";
console.log(person.hobby); // => ["吃飯", "睡覺", "碼"]
複製代碼
  • 使用 defineProperty 爲 person 對象添加了一個 名爲 hobby 的屬性
  • 屬性的 屬性描述符 包含 set、get,爲存取描述符
  • get 在獲取屬性時調用該函數,返回值將做爲屬性獲取的結果
  • set 在設置屬性時調用該函數,接收一個參數爲要設置的 值
  • configurable、enumerable 是通用的
// 數據描述符和存取描述符不能混合使用
Object.defineProperty(person, "conflict", {
  value: 0x9f91102,
  get: function() {
    return 0xdeadbeef;
  }
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors
複製代碼

6.3.2 修改屬性

  • 若是屬性已經存在,Object.defineProperty()將嘗試根據描述符中的值以及對象當前的配置來修改這個屬性。
  • 若是舊描述符將其 configurable 屬性設置爲 false,則該屬性被認爲是「不可配置的」,而且沒有屬性能夠被改變(除了單向改變 writable 爲 false)。
  • 當屬性不可配置時,不能在 數據訪問器 屬性類型之間切換。
var o = {
  a: "aValue",
  b: "bValue"
};
console.log(Object.getOwnPropertyDescriptor(o, "a")); // =>
// {value: "aValue", writable: true, enumerable: true, configurable: true}

Object.defineProperty(o, "a", {
  get() {
    console.log(this === o);

    return this.b;
  },
  set(newValue) {}
});

console.log(Object.getOwnPropertyDescriptor(o, "a")); // =>
// {get: ƒ, set: ƒ, enumerable: true, configurable: true}
複製代碼
  • 在定義一個 Object o 後,a 屬性默認爲 數據描述符
  • 使用 defineProperty 將 a 屬性的描述符 配置成了 存取描述符,擁有 get、set 兩個存取函數
  • 在 configurable 爲 true 的前提下,數據描述符和存取描述符能夠互相切換
Object.defineProperty(o, "a", {
  configurable: false
});
console.log(Object.getOwnPropertyDescriptor(o, "a")); // =>
// {get: ƒ, set: ƒ, enumerable: true, configurable: false}

Object.defineProperty(o, "a", {
  get() {
    return 11111;
  },
  set(newValue) {}
});
// => Uncaught TypeError: Cannot redefine property: a

Object.defineProperty(o, "a", {
  value: "aaaa"
  // configurable: true
});
// => Uncaught TypeError: Cannot redefine property: a
複製代碼
  • 當將 a 屬性的 configurable 配置成 false 後, 標識「不可配置的」,該屬性將不可再被改變
  • 包括改變 get、set 函數、數據訪問器和存取訪問器的切換
  • 以及 configurable 也不可再被改變 -- 試圖改變時,將會報錯
  • configurable 是單向的

6.3.3 添加屬性時的默認值

  • 考慮特性被賦予的默認特性值很是重要
  • 一般,使用點運算符和 Object.defineProperty()爲對象的屬性賦值時,數據描述符中的屬性默認值是不一樣的
  • 以下例所示:
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
});
複製代碼

7. 小結 - 回到規範,迴歸 Object

在咱們通過一系列的分析後,對於屬性描述符也有了必定的瞭解,建議多推敲下屬性描述符部分。 讀到這裏,咱們回想下規範給予咱們屬性是什麼的答案:

  • 答案 1:屬性是由一個名字與一個 ECMAScript 語言類型值和一個 Boolean 屬性集合組成。
  • 答案 2:由一個名字與一個或兩個訪問器函數,和一個 Boolean 屬性集合組成。訪問器函數用於存取一個與該屬性相關聯的 ECMAScript 語言類型 值。

如今,是否是能夠理解規範的答案了,很明顯:

  • Boolean 屬性集合 指的就是 屬性描述符
  • 一個或兩個訪問器函數 指的就是 存取描述符 中的 get、set

對應的 規範的第一句: 每一個屬性既能夠是一個命名數據屬性,也能夠是一個命名訪問器屬性

  • 命名數據屬性:
    • 一個命名的屬性,有名字,有值,其屬性描述符爲 數據描述符,其描述符包含value、writable、enumerable、configurable
  • 命名訪問器屬性
    • 一個命名的屬性,有名字,屬性的存取操做由屬性描述符中的 getter 與 setter 決定,其屬性描述符爲存取描述符,描述符包含get、set、enumerable、configurable

從 Object 宏觀出發,Object 就是這些 命名數據屬性、命名訪問器屬性、(還有內部屬性 - 未在表面變現出來) 的集合體

8. 參考 👈

ECMAScript 規範 - ES5/類型

MDN - Object.defineProperty

本文完,歡迎各位看官老爺批評

相關文章
相關標籤/搜索