讀es6 語法記錄相關知識點--對象的擴展

對象的擴展

  • 對象屬性的可枚舉性
    目前,有四個操作會忽略enumerable爲false的屬性(僅僅得到enumerable爲true的屬性)。
    for…in循環:只遍歷對象自身的和繼承的可枚舉的屬性。
    Object.keys():返回對象自身的所有可枚舉的屬性的鍵名。
    JSON.stringify():只串行化對象自身的可枚舉的屬性。
    Object.assign(): 忽略enumerable爲false的屬性,只拷貝對象自身的可枚舉的屬性。

這四個操作之中,前三個是 ES5 就有的,最後一個Object.assign()是 ES6 新增的。其中,只有for…in會返回繼承的屬性,其他三個方法都會忽略繼承的屬性,只處理對象自身的屬性。實際上,引入“可枚舉”(enumerable)這個概念的最初目的,就是讓某些屬性可以規避掉for…in操作,不然所有內部屬性和方法都會被遍歷到。比如,對象原型的toString方法,以及數組的length屬性,就通過“可枚舉性”,從而避免被for…in遍歷到。
另外,ES6 規定,所有 Class 的原型的方法都是不可枚舉的。
總的來說,操作中引入繼承的屬性會讓問題複雜化,大多數時候,我們只關心對象自身的屬性。所以,儘量不要用for…in循環,而用Object.keys()代替。

  • 屬性的遍歷
    ES6 一共有 5 種方法可以遍歷對象的屬性。
    (1)for…in
    for…in循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)。
    (2)Object.keys(obj)
    Object.keys返回一個數組,包括對象自身的(不含繼承的)所有可枚舉屬性(不含 Symbol 屬性)的鍵名。
    (3)Object.getOwnPropertyNames(obj)
    Object.getOwnPropertyNames返回一個數組,包含對象自身的所有屬性(不含 Symbol 屬性,但是包括不可枚舉屬性)的鍵名。
    (4)Object.getOwnPropertySymbols(obj)
    Object.getOwnPropertySymbols返回一個數組,包含對象自身的所有 Symbol 屬性的鍵名。
    (5)Reflect.ownKeys(obj)
    Reflect.ownKeys返回一個數組,包含對象自身的所有鍵名,不管鍵名是 Symbol 或字符串,也不管是否可枚舉。

以上的 5 種方法遍歷對象的鍵名,都遵守同樣的屬性遍歷的次序規則。
首先遍歷所有數值鍵,按照數值升序排列。
其次遍歷所有字符串鍵,按照加入時間升序排列。
最後遍歷所有 Symbol 鍵,按照加入時間升序排列。

  • super關鍵字
    我們知道,this關鍵字總是指向函數所在的當前對象,ES6 又新增了另一個類似的關鍵字super,指向當前對象的原型對象。
const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代碼中,對象obj.find()方法之中,通過super.foo引用了原型對象proto的foo屬性。

注意,super關鍵字表示原型對象時,只能用在對象的方法之中,用在其他地方都會報錯。

// 報錯
const obj = {
  foo: super.foo
}

// 報錯
const obj = {
  foo: () => super.foo
}

// 報錯
const obj = {
  foo: function () {
    return super.foo
  }
}

上面三種super的用法都會報錯,因爲對於 JavaScript 引擎來說,這裏的super都沒有用在對象的方法之中。第一種寫法是super用在屬性裏面,第二種和第三種寫法是super用在一個函數裏面,然後賦值給foo屬性。目前,只有對象方法的簡寫法可以讓 JavaScript 引擎確認,定義的是對象的方法。

  • 對象的解構賦值
    對象的解構賦值用於從一個對象取值,相當於將目標對象自身的所有可遍歷的(enumerable)、但尚未被讀取的屬性,分配到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

上面代碼中,變量z是解構賦值所在的對象。它獲取等號右邊的所有尚未讀取的鍵(a和b),將它們連同值一起拷貝過來。

由於解構賦值要求等號右邊是一個對象,所以如果等號右邊是undefined或null,就會報錯,因爲它們無法轉爲對象。

let { ...z } = null; // 運行時錯誤
let { ...z } = undefined; // 運行時錯誤

解構賦值必須是最後一個參數,否則會報錯。

let { ...x, y, z } = someObject; // 句法錯誤
let { x, ...y, ...z } = someObject; // 句法錯誤

注意,解構賦值的拷貝是淺拷貝,即如果一個鍵的值是複合類型的值(數組、對象、函數)、那麼解構賦值拷貝的是這個值的引用,而不是這個值的副本。

另外,擴展運算符的解構賦值,不能複製繼承自原型對象的屬性。

let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined

上面代碼中,對象o3複製了o2,但是隻複製了o2自身的屬性,沒有複製它的原型對象o1的屬性。

const o = Object.create({ x: 1, y: 2 });
o.z = 3;

let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined
z // 3

上面代碼中,變量x是單純的解構賦值,所以可以讀取對象o繼承的屬性;變量y和z是擴展運算符的解構賦值,只能讀取對象o自身的屬性,所以變量z可以賦值成功,變量y取不到值。ES6 規定,變量聲明語句之中,如果使用解構賦值,擴展運算符後面必須是一個變量名,而不能是一個解構賦值表達式,所以上面代碼引入了中間變量newObj,如果寫成下面這樣會報錯。

let { x, ...{ y, z } } = o;

解構賦值的一個用處,是擴展某個函數的參數,引入其他操作。

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // 使用 x 和 y 參數進行操作
  // 其餘參數傳給原始函數
  return baseFunction(restConfig);
}

上面代碼中,原始函數baseFunction接受a和b作爲參數,函數wrapperFunction在baseFunction的基礎上進行了擴展,能夠接受多餘的參數,並且保留原始函數的行爲。

  • 對象的擴展運算符
    對象的擴展運算符(…)用於取出參數對象的所有可遍歷屬性,拷貝到當前對象之中。
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

由於數組是特殊的對象,所以對象的擴展運算符也可以用於數組。
如果擴展運算符後面是字符串,它會自動轉成一個類似數組的對象,因此返回的不是空對象。

let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

如果擴展運算符後面是一個空對象,則沒有任何效果。如果擴展運算符後面不是對象,則會自動將其轉爲對象。

{...{}, a: 1}
// { a: 1 }

// 等同於 {...Object(1)}
{...1} // {}

上面代碼中,擴展運算符後面是整數1,會自動轉爲數值的包裝對象Number{1}。由於該對象沒有自身屬性,所以返回一個空對象。

對象的擴展運算符等同於使用Object.assign()方法。

let aClone = { ...a };
// 等同於
let aClone = Object.assign({}, a);

上面的例子只是拷貝了對象實例的屬性,如果想完整克隆一個對象,還拷貝對象原型的屬性,可以採用下面的寫法。

// 寫法一
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
};

// 寫法二
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

// 寫法三
const clone3 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)

上面代碼中,寫法一的__proto__屬性在非瀏覽器的環境不一定部署,因此推薦使用寫法二和寫法三。

擴展運算符可以用於合併兩個對象。

let ab = { ...a, ...b };
// 等同於
let ab = Object.assign({}, a, b);

如果用戶自定義的屬性,放在擴展運算符後面,則擴展運算符內部的同名屬性會被覆蓋掉。

let aWithOverrides = { ...a, x: 1, y: 2 };
// 等同於
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等同於
let x = 1, y = 2, aWithOverrides = { ...a, x, y };
// 等同於
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });

上面代碼中,a對象的x屬性和y屬性,拷貝到新對象後會被覆蓋掉。

這用來修改現有對象部分的屬性就很方便了。

let newVersion = {
  ...previousVersion,
  name: 'New Name' // Override the name property
};

上面代碼中,newVersion對象自定義了name屬性,其他屬性全部複製自previousVersion對象。

如果把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。

let aWithDefaults = { x: 1, y: 2, ...a };
// 等同於
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同於
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);

與數組的擴展運算符一樣,對象的擴展運算符後面可以跟表達式。

const obj = {
  ...(x > 1 ? {a: 1} : {}),
  b: 2,
};

擴展運算符的參數對象之中,如果有取值函數get,這個函數是會執行的。

// 並不會拋出錯誤,因爲 x 屬性只是被定義,但沒執行
let aWithXGetter = {
  ...a,
  get x() {
    throw new Error('not throw yet');
  }
};

// 會拋出錯誤,因爲 x 屬性被執行了
let runtimeError = {
  ...a,
  ...{
    get x() {
      throw new Error('throw now');
    }
  }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章