ECMA2015(ES6)簡單入門-9-對象-對象的擴展-對象的新增方法

對象的創建

  1. 使用Object構造函數來創建一個對象
    在這裏插入圖片描述
  2. 使用對象字面量創建一個對象
    在這裏插入圖片描述
  3. 工廠模式創建對象
    在這裏插入圖片描述
  4. 構造函數模式創建對象
    在這裏插入圖片描述
  5. 原型模式創建對象
    在這裏插入圖片描述
    本身也有缺陷,就是實例共享了引用類型friends,從下面的代碼執行結果可以看到,
    兩個實例的friends的值是一樣的,這可能不是我們所期望的。
  6. 原型模式和構造函數組合使用創建對象
    使用最廣泛、認同度最高的創建對象的方法。
    在這裏插入圖片描述
  7. 動態原型模式創建對象
    這個模式的好處在於看起來更像傳統的面向對象編程,
    具有更好的封裝性,因爲在構造函數裏完成了對原型創建。
    這也是一個推薦的創建對象的方法。
    在這裏插入圖片描述

對象的擴展

對象(object)是 JavaScript 最重要的數據結構。ES6 對它進行了重大升級,
數據結構本身發生了改變。

1、屬性的簡潔表示法
ES6 允許直接寫入變量和函數,作爲對象的

屬性簡寫
在這裏插入圖片描述
上面代碼表明,ES6 允許在對象之中,直接寫變量。
方法簡寫。

在這裏插入圖片描述
下面是一個實際的例子。
在這裏插入圖片描述

// 屬性簡寫
var a = "first";
var b = {a};
console.log(b)  // {a:"first"}
//方法簡寫
var obj = {
  method (){
    retrun "hello world";
  }
} 
//等同於
var obj = {
  method: function (){
    retrun "hello world";
  }
}

2、屬性名錶達式

let propName = "value";
let obj = {
  [propName]:true,
  ["one"+"child"]:1323
}
obj[propName]  // true
obj[onechild]  // 1323

重點內容
① 如果屬性名錶達式不能與簡潔表示法同時使用。否則會報錯
② 屬性名錶達式如果是一個對象,默認情況下會自動將對象轉爲字符串,如果有多個屬性名對象會產生覆蓋。

3、方法的 name 屬性

函數的name屬性,返回函數名。對象方法也是函數,因此也有name屬性。

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

上面代碼中,方法的name屬性返回函數名(即方法名)。

如果對象的方法使用了取值函數(getter)和存值函數(setter),
則name屬性不是在該方法上面,而是該方法的屬性的描述對象的get和set屬性上面,
返回值是方法名前加上get和set。

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

有兩種特殊情況:bind方法創造的函數,name屬性返回bound加上原函數的名字;Function構造函數創造的函數,name屬性返回anonymous。


4、對象屬性的可枚舉性和遍歷

可枚舉性
Object.getOwnPropertyDescriptor方法可以獲取該屬性的描述對象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述對象的enumerable屬性,稱爲“可枚舉性”,如果該屬性爲false,就表示某些操作會忽略當前屬性。

目前,有四個操作會忽略enumerable爲false的屬性。

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

這四個操作之中,前三個是 ES5 就有的,最後一個Object.assign()是 ES6 新增的。
其中,只有for…in會返回繼承的屬性,其他三個方法都會忽略繼承的屬性,只處理對象自身的屬性。
實際上,引入“可枚舉”(enumerable)這個概念的最初目的,就是讓某些屬性可以規避掉for…in操作,
不然所有內部屬性和方法都會被遍歷到。
比如,對象原型的toString方法,以及數組的length屬性,就通過“可枚舉性”,從而避免被for…in遍歷到。

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false

因此for…in不會遍歷到這兩個繼承自原型的屬性。

另外,ES6 規定,所有 Class 的原型的方法都是不可枚舉的。

Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable//false

大多數時候,我們只關心對象自身的屬性。所以,儘量不要用for…in循環,而用Object.keys()代替。


屬性的遍歷

ES6 一共有 5 種方法可以遍歷對象的屬性。

(1)for…in:循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)。
在這裏插入圖片描述
(2)Object.keys(obj):返回一個數組,包括對象自身的(不含繼承的)所有可枚舉屬性(不含 Symbol 屬性)的鍵名。
附:Object.values(obj)(這個是遍歷對象的值)
參數:obj:要返回其枚舉自身屬性的對象
在這裏插入圖片描述

(3)Object.getOwnPropertyNames(obj):返回一個數組,包含對象自身的所有屬性(包含不可枚舉屬性,不含 Symbol 屬性)的鍵名。
返回一個數組,包含對象自身的所有屬性
遍歷可以獲取key和value

使用Object.keys方法將對象的鍵名生成一個數組,然後遍歷這個數組
在這裏插入圖片描述
(4)Object.getOwnPropertySymbols(obj):返回一個數組,包含對象自身的所有 Symbol 屬性的鍵名。
使用 Generator 函數將對象重新包裝一下

function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

(5)Reflect.ownKeys(obj)返回一個數組,包含對象自身的所有鍵名,不管鍵名是 Symbol 或字符串,也不管是否可枚舉。
在這裏插入圖片描述
上面代碼中,Reflect.ownKeys方法返回一個數組,包含了參數對象的所有屬性。
這個數組的屬性次序是這樣的,
首先是數值屬性2和10,其次是字符串屬性b和a,最後是 Symbol 屬性。

以上的 5 種方法遍歷對象的鍵名,都遵守同樣的屬性遍歷的次序規則。

首先遍歷所有數值鍵,按照數值升序排列。
其次遍歷所有字符串鍵,按照加入時間升序排列。
最後遍歷所有 Symbol 鍵,按照加入時間升序排列。


5、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的用法都會報錯,因爲對於 js 引擎來說,這裏的super都沒有用在對象的方法之中。
第一種寫法是super用在屬性裏面,
第二種和第三種寫法是super用在一個函數裏面,然後賦值給foo屬性。
目前,只有對象方法的簡寫法可以讓 JavaScript 引擎確認,定義的是對象的方法。
JavaScript 引擎內部,super.foo等同於Object.getPrototypeOf(this).foo(屬性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};
const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"

上面代碼中,super.foo指向原型對象proto的foo方法,但是綁定的this卻還是當前對象obj,因此輸出的就是world。


** 6. 對象的擴展運算符**

解構賦值
對象的解構賦值用於從一個對象取值,相當於將目標對象自身的所有可遍歷的可枚舉的,
尚未被讀取的屬性,分配到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面。

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; // 句法錯誤

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

上面代碼中,x是解構賦值所在的對象,拷貝了對象obj的a屬性。
a屬性引用了一個對象,修改這個對象的值,會影響到解構賦值對它的引用。

另外,擴展運算符的解構賦值,不能複製繼承自原型對象的屬性。
在這裏插入圖片描述
上面代碼中,對象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

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

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

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

如果擴展運算符後面是一個空對象,則沒有任何效果。

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

如果擴展運算符後面不是對象,則會自動將其轉爲對象。

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

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

下面的例子都是類似的道理。

// 等同於 {...Object(true)}
{...true} // {}
// 等同於 {...Object(undefined)}
{...undefined} // {}
// 等同於 {...Object(null)}
{...null} // {}

但是,如果擴展運算符後面是字符串,它會自動轉成一個類似數組的對象,返回的不是空對象。

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

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

let new = { ...a };
// 等同於
let new = 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,
}

對象的新增方法Object.is()

ES5 比較兩個值是否相等,只有兩個運算符:相等運算符()和嚴格相等運算符(=)。它們都有缺點,前者會自動轉換數據類型,後者的NaN不等於自身,以及+0等於-0。

ES6 提出“Same-value equality”(同值相等)算法,Object.is就是部署這個算法用來解決這個問題。。
它用來比較兩個值是否嚴格相等,與嚴格比較運算符(===)的行爲基本一致。
不同之處只有兩個:一是+0不等於-0,二是NaN等於自身。

在這裏插入圖片描述

Object.assign()

基本用法
Object.assign方法用於對象的合併將源對象的所有可枚舉屬性,複製到目標對象(target)。

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);

assign
Object.assign方法的第一個參數是目標對象後面的參數都是源對象。

注意,如果目標對象與源對象有同名屬性,或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性。

const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

在這裏插入圖片描述
如果只有一個參數,Object.assign會直接返回該參數。

const obj = {a: 1};
Object.assign(obj) === obj // true

如果該參數不是對象,則會先轉成對象,然後返回。

typeof Object.assign(2) // "object"

由於undefined和null無法轉成對象,所以如果它們作爲參數,就會報錯。

Object.assign(undefined) // 報錯
Object.assign(null) // 報錯

如果非對象參數出現在源對象的位置(即非首參數),那麼處理規則有所不同。首先,這些參數都會轉成對象,如果無法轉成對象,就會跳過。
這意味着,如果undefined和null不在首參數,就不會報錯。

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

其他類型的值(即數值、字符串和布爾值)不在首參數,也不會報錯。但是,除了字符串會以數組形式,拷貝入目標對象,其他值都不會產生效果,因爲只有字符串的包裝對象,會產生可枚舉屬性

Object(true) // {[[PrimitiveValue]]: true}
Object(10)  //  {[[PrimitiveValue]]: 10}
Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}

上面代碼中,布爾值、數值、字符串分別轉成對應的包裝對象,可以看到它們的原始值都在包裝對象的內部屬性[[PrimitiveValue]]上面,這個屬性是不會被Object.assign拷貝的。
只有字符串的包裝對象,會產生可枚舉的實義屬性,那些屬性則會被拷貝。

Object.assign拷貝的屬性是有限制的,只拷貝源對象的自身屬性(不拷貝繼承屬性),也不拷貝不可枚舉的屬性(enumerable: false)

Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }

(1)淺拷貝

Object.assign方法實行的是淺拷貝,而不是深拷貝。也就是說,如果源對象某個屬性的值是對象,那麼目標對象拷貝得到的是這個對象的引用。

源對象obj1的a屬性的值是一個對象,Object.assign拷貝得到的是這個對象的引用。
這個對象的任何變化,都會反映到目標對象上面。
在這裏插入圖片描述
(2)同名屬性的替換

對於這種嵌套的對象,一旦遇到同名屬性,Object.assign的處理方法是替換,而不是添加。

const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }

(3)數組的處理

Object.assign可以用來處理數組,但是會把數組視爲對象。
在這裏插入圖片描述
上面代碼中,Object.assign把數組視爲屬性名爲 0、1、2 的對象,
因此源數組的 0 號屬性4覆蓋了目標數組的 0 號屬性1。

(4)取值函數的處理

Object.assign只能進行值的複製,如果要複製的值是一個取值函數,那麼將求值後再複製。

const source = {
  get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
上面代碼中,source對象的foo屬性是一個取值函數,Object.assign不會複製這個取值函數,只會拿到值以後,將這個值複製過去。

常見用途
Object.assign方法有很多用處。

(1)爲對象添加屬性,通過Object.assign方法,將x屬性和y屬性添加到Point類的對象實例。

class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

(2)給對象添加方法

使用了對象屬性的簡潔表示法,直接將兩個函數放在大括號中,再使用assign方法添加到SomeClass.prototype之中。

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同於下面的寫法

SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

(3)克隆對象

function clone(origin) {
  return Object.assign({}, origin);
}

上面代碼將原始對象拷貝到一個空對象,就得到了原始對象的克隆。

這種方法克隆,只能克隆原始對象自身的值,不能克隆它繼承的值。如果想要保持繼承鏈,可以採用下面的代碼。

function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}

(4)合併多個對象

將多個對象合併到某個對象。

const merge =
  (target, ...sources) => Object.assign(target, ...sources);
如果希望合併後返回一個新對象,可以改寫上面函數,對一個空對象合併。

const merge =
  (...sources) => Object.assign({}, ...sources);

(5)爲屬性指定默認值

Object.getOwnPropertyDescriptors()
ES5 的Object.getOwnPropertyDescriptor()方法會返回某個對象屬性的描述對象(descriptor)。
ES6 引入了Object.getOwnPropertyDescriptors()方法,返回指定對象所有自身屬性(非繼承屬性)的描述對象。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

上面代碼中,Object.getOwnPropertyDescriptors()方法返回一個對象,所有原對象的屬性名都是該對象的屬性名,對應的屬性值就是該屬性的描述對象。

該方法的實現非常容易。

function getOwnPropertyDescriptors(obj) {
  const result = {};
  for (let key of Reflect.ownKeys(obj)) {
    result[key] = Object.getOwnPropertyDescriptor(obj, key);
  }
  return result;
}

該方法的引入目的,主要是爲了解決Object.assign()無法正確拷貝get屬性和set屬性的問題。
在這裏插入圖片描述

上面代碼中,source對象的foo屬性的值是一個賦值函數,Object.assign方法將這個屬性拷貝給target1對象,結果該屬性的值變成了undefined。
這是因爲Object.assign方法總是拷貝一個屬性的值,而不會拷貝它背後的賦值方法或取值方法。

這時,Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法,就可以實現正確拷貝。

__proto__屬性,Object.setPrototypeOf(),Object.getPrototypeOf()

JavaScript 語言的對象繼承是通過原型鏈實現的。ES6 提供了更多原型對象的操作方法。

__proto__屬性
用來讀取或設置當前對象的prototype對象。目前,所有瀏覽器(包括 IE11)都部署了這個屬性。

// es5 的寫法
const obj = {
  method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es6 的寫法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

該屬性沒有寫入 ES6 的正文,而是寫入了附錄,原因是__proto__前後的雙下劃線,說明它本質上是一個內部屬性,而不是一個正式的對外的 API,所以不要使用這個屬性,而是使用下面的Object.setPrototypeOf()(寫操作)、Object.getPrototypeOf()(讀操作)、Object.create()(生成操作)代替。

實現上,__proto__調用的是Object.prototype.proto,具體實現如下。

Object.defineProperty(Object.prototype, '__proto__', {
  get() {
    let _thisObj = Object(this);
    return Object.getPrototypeOf(_thisObj);
  },
  set(proto) {
    if (this === undefined || this === null) {
      throw new TypeError();
    }
    if (!isObject(this)) {
      return undefined;
    }
    if (!isObject(proto)) {
      return undefined;
    }
    let status = Reflect.setPrototypeOf(this, proto);
    if (!status) {
      throw new TypeError();
    }
  },
});

function isObject(value) {
  return Object(value) === value;
}

如果一個對象本身部署了__proto__屬性,該屬性的值就是對象的原型。

Object.getPrototypeOf({ __proto__: null })
// null
Object.setPrototypeOf()

Object.setPrototypeOf方法的作用與__proto__相同,用來設置一個對象的prototype對象,返回參數對象本身。
它是 ES6 正式推薦的設置原型對象的方法。

// 格式
Object.setPrototypeOf(object, prototype)

// 用法
const o = Object.setPrototypeOf({}, null);
該方法等同於下面的函數。

function setPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

下面是一個例子。

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);

proto.y = 20;
proto.z = 40;

obj.x // 10
obj.y // 20
obj.z // 40

上面代碼將proto對象設爲obj對象的原型,所以從obj對象可以讀取proto對象的屬性。

如果第一個參數不是對象,會自動轉爲對象。但是由於返回的還是第一個參數,所以這個操作不會產生任何效果。

Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true
由於undefined和null無法轉爲對象,所以如果第一個參數是undefined或null,就會報錯。

Object.setPrototypeOf(undefined, {})
// TypeError: Object.setPrototypeOf called on null or undefined

Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined
Object.getPrototypeOf()
該方法與Object.setPrototypeOf方法配套,用於讀取一個對象的原型對象。

Object.getPrototypeOf(obj);

下面是一個例子。

function Rectangle() {
  // ...
}

const rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

如果參數不是對象,會被自動轉爲對象。

// 等同於 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}

// 等同於 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo')
// String {length: 0, [[PrimitiveValue]]: ""}

// 等同於 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true)
// Boolean {[[PrimitiveValue]]: false}

Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true

如果參數是undefined或null,它們無法轉爲對象,所以會報錯。

Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object

Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object
Object.keys(),Object.values(),Object.entries()
Object.keys()

ES5 引入了Object.keys方法,返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名。

var obj = { foo: ‘bar’, baz: 42 };
Object.keys(obj)
// [“foo”, “baz”]
ES2017 引入了跟Object.keys配套的Object.values和Object.entries,作爲遍歷一個對象的補充手段,供for…of循環使用。
在這裏插入圖片描述

Object.values()返回對象自身的可遍歷屬性。

const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []

上面代碼中,Object.create方法的第二個參數添加的對象屬性(屬性p),如果不顯式聲明,默認是不可遍歷的,因爲p的屬性描述對象的enumerable默認是false,Object.values不會返回這個屬性。只要把enumerable改成true,Object.values就會返回屬性p的值。

const obj = Object.create({}, {p:
  {
    value: 42,
    enumerable: true
  }
});
Object.values(obj) // [42]
Object.values會過濾屬性名爲 Symbol 值的屬性。

Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']

如果Object.values方法的參數是一個字符串,會返回各個字符組成的一個數組。

Object.values('foo')
// ['f', 'o', 'o']

上面代碼中,字符串會先轉成一個類似數組的對象。字符串的每個字符,就是該對象的一個屬性。因此,Object.values返回每個屬性的鍵值,就是各個字符組成的一個數組。

如果參數不是對象,Object.values會先將其轉爲對象。由於數值和布爾值的包裝對象,都不會爲實例添加非繼承的屬性。所以,Object.values會返回空數組。

Object.values(42) // []
Object.values(true) // []
Object.entries()
Object.entries()方法返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值對數組。

Object.entries的基本用途是遍歷對象的屬性。

et obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
  console.log(
    `${JSON.stringify(k)}: ${JSON.stringify(v)}`
  );
}
// "one": 1
// "two": 2

Object.entries方法的另一個用處是,將對象轉爲真正的Map結構。

const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }

自己實現Object.entries方法,非常簡單。

// Generator函數的版本
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

// 非Generator函數的版本
function entries(obj) {
  let arr = [];
  for (let key of Object.keys(obj)) {
    arr.push([key, obj[key]]);
  }
  return arr;
}

Object.fromEntries()
Object.fromEntries()方法是Object.entries()的逆操作,用於將一個鍵值對數組轉爲對象。

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

該方法的主要目的,是將鍵值對的數據結構還原爲對象,因此特別適合將 Map 結構轉爲對象。

// 例一
const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }

// 例二

const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }

該方法的一個用處是配合URLSearchParams對象,將查詢字符串轉爲對象。

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章