補充學習一下 ES6 對象擴充 方面的知識,包含以下内容:
1、對象屬性的簡潔表示法,屬性名可以使用表達式(方括号表示);
2、對象方法的name屬性,遇到setter和getter時需要通過Object.getOwnPropertyDescriptor方法取得方法的屬性的描述對象,再去得到name屬性,除此之外還有bind建立和Function構造函數兩種特殊情況;
3、屬性的周遊(結合Symbol更新了規則);
4、super關鍵字;
5、擴充運算符;
6、?. 和 ?? 運算符。
文章目錄
- ES6
- 對象的擴充
- 1、屬性的簡潔表示法
- 2、屬性名表達式
- 3、方法的 name 屬性
- 方法的name屬性的兩種特殊情況
- 4、屬性的可枚舉性和周遊
- 可枚舉性
- 屬性的周遊
- (1)for...in
- (2)Object.keys(obj)
- (3)Object.getOwnPropertyNames(obj)
- (4)Object.getOwnPropertySymbols(obj)
- (5)Reflect.ownKeys(obj)
- 5、super 關鍵字
- 6、對象的擴充運算符
- 解構指派
- 擴充運算符
- 7、鍊判斷運算符
- 注意點
- (1)短路機制
- (2)delete 運算符
- (3)括号的影響
- (4)報錯場合
- (5)右側不得為十進制數值
- 8、Null 判斷運算符
ES6
對象的擴充
1、屬性的簡潔表示法
ES6 允許在大括号裡面,直接寫入變量和函數,作為對象的屬性和方法。這樣的書寫更加簡潔。下面是一個實際的例子。
let birth = '2000/01/01';
const Person = {
name: '張三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
這種寫法用于函數的傳回值,将會非常友善。CommonJS 子產品輸出一組變量,就非常合适使用簡潔寫法。
let ms = {};
function getItem (key) {
return key in ms ? ms[key] : null;
}
function setItem (key, value) {
ms[key] = value;
}
function clear () {
ms = {};
}
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
屬性的指派器(setter)和取值器(getter),事實上也是采用這種寫法。
const cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('數值太小了!');
}
this._wheels = value;
}
}
注意,簡寫的對象方法不能用作構造函數,會報錯。
const obj = {
f() {
this.foo = 'bar';
}
};
new obj.f() // 報錯
上面代碼中,f是一個簡寫的對象方法,是以obj.f不能當作構造函數使用。
2、屬性名表達式
JavaScript 定義對象的屬性,有兩種方法。
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
上面代碼的方法一是直接用辨別符作為屬性名,方法二是用表達式作為屬性名,這時要将表達式放在方括号之内。
但是,如果使用字面量方式定義對象(使用大括号),在 ES5 中隻能使用方法一(辨別符)定義屬性。
var obj = {
foo: true,
abc: 123
};
ES6 允許字面量定義對象時,用方法二(表達式)作為對象的屬性名,即把表達式放在方括号内。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
下面是另一個例子。同時,表達式還可以用于定義方法名。
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world',
['h' + 'ello']() {
return 'hi';
}
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
a.hello() // hi
注意,屬性名表達式與簡潔表示法,不能同時使用,會報錯。
// 報錯
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
注意,屬性名表達式如果是一個對象,預設情況下會自動将對象轉為字元串[object Object],這一點要特别小心。
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}
上面代碼中,[keyA]和[keyB]得到的都是[object Object],是以[keyB]會把[keyA]覆寫掉,而myObject最後隻有一個[object Object]屬性。
3、方法的 name 屬性
函數的name屬性,傳回函數名。對象方法也是函數,是以也有name屬性。
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"
如果對象的方法使用了取值函數(getter)和存值函數(setter),則name屬性不是在該方法上面(不存在name屬性),而是該方法的屬性的描述對象(通過Object.getOwnPropertyDescriptor得到對象的方法的描述對象)的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');
// obj.foo的描述對象
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
方法的name屬性的兩種特殊情況
方法的name屬性還有兩種特殊情況:
1、bind方法創造的函數,name屬性傳回bound加上原函數的名字;
2、Function構造函數創造的函數,name屬性傳回anonymous(匿名)。
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
如果對象的方法是一個 Symbol 值,那麼name屬性傳回的是這個 Symbol 值的描述。
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
上面代碼中,key1對應的 Symbol 值有描述,key2沒有。
4、屬性的可枚舉性和周遊
可枚舉性
對象的每個屬性都有一個描述對象(Descriptor),用來控制該屬性的行為。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
上面代碼中,toString和length屬性的enumerable都是false,是以for…in不會周遊到這兩個繼承自原型的屬性。
另外,ES6 規定,所有 Class 的原型的方法都是不可枚舉的。
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
總的來說,操作中引入繼承的屬性會讓問題複雜化,大多數時候,我們隻關心對象自身的屬性。是以,盡量不要用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 種方法周遊對象的鍵名,都遵守同樣的屬性周遊的次序規則。
1、首先周遊所有數值鍵,按照數值升序排列。
2、其次周遊所有字元串鍵,按照加入時間升序排列。
3、最後周遊所有 Symbol 鍵,按照加入時間升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
上面代碼中,Reflect.ownKeys方法傳回一個數組,包含了參數對象的所有屬性。這個數組的屬性次序是這樣的,首先是數值屬性2和10,其次是字元串屬性b和a,最後是 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的用法都會報錯,因為對于 JavaScript 引擎來說,這裡的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、對象的擴充運算符
解構指派
對象的解構指派之前已經了解過了,隻做簡單叙述。
對象的解構指派用于從一個對象取值,相當于将目标對象自身的所有可周遊的(enumerable)、但尚未被讀取的屬性,配置設定到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
需要注意的有:
1、等号右邊必須是一個對象,如果是undefined或者null,就會報錯,因為它們無法轉為對象。
2、等号左邊,擴充運算符形式的解構指派必須是最後一個參數,否則會報錯。
3、解構指派的拷貝是淺拷貝,是以當一個鍵的值是引用類型值(數組、對象、函數、Date等等),那麼拷貝到的将是這個值的引用,而非這個值的副本。
擴充運算符
對象的擴充運算符用于取出參數對象的所有可周遊屬性(enumerable==true),拷貝到目前對象之中。同樣,數組是特殊的對象,是以也能應用于數組。
如果擴充運算符後面跟的不是對象,則會自動轉為對象。比如說擴充運算符後面跟了字面量的整數1,會轉為數值的包裝對象Number(1),但因為該對象沒有自身屬性,是以會傳回一個空對象。
加入是字元串,則會轉為類數組對象:
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
對象的擴充運算符屬于淺拷貝,其效果等同于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__屬性在非浏覽器不一定部署,是以推薦使用寫法二和三。
7、鍊判斷運算符
程式設計實務中,如果讀取對象内部的某個屬性,往往需要判斷一下該對象是否存在。比如,要讀取message.body.user.firstName,安全的寫法是寫成下面這樣。
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
或者使用三元運算符?:,判斷一個對象是否存在。
const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
這樣的層層判斷非常麻煩,是以 ES2020 引入了“鍊判斷運算符”(optional chaining operator)?.,簡化上面的寫法。
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
上面代碼使用了?.運算符,直接在鍊式調用的時候判斷,左側的對象是否為null或undefined。如果是的,就不再往下運算,而是傳回undefined。
鍊判斷運算符有三種用法。
obj?.prop // 對象屬性
obj?.[expr] // 同上
func?.(...args) // 函數或對象方法的調用
下面是判斷對象方法是否存在,如果存在就立即執行的例子。
上面代碼中,iterator.return如果有定義,就會調用該方法,否則直接傳回undefined。
對于那些可能沒有實作的方法,這個運算符尤其有用。
if (myForm.checkValidity?.() === false) {
// 表單校驗失敗
return;
}
上面代碼中,老式浏覽器的表單可能沒有checkValidity這個方法,這時?.運算符就會傳回undefined,判斷語句就變成了undefined === false,是以就會跳過下面的代碼。
下面是這個運算符常見的使用形式,以及不使用該運算符時的等價形式。
a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()
上面代碼中,特别注意後兩種形式,如果a?.b()裡面的a.b不是函數,不可調用,那麼a?.b()是會報錯的。a?.()也是如此,如果a不是null或undefined,但也不是函數,那麼a?.()會報錯。
注意點
使用這個運算符,有幾個注意點。
(1)短路機制
a?.[++x]
// 等同于
a == null ? undefined : a[++x]
上面代碼中,如果a是undefined或null,那麼x不會進行遞增運算。也就是說,鍊判斷運算符一旦為真,右側的表達式就不再求值。
(2)delete 運算符
delete a?.b
// 等同于
a == null ? undefined : delete a.b
上面代碼中,如果a是undefined或null,會直接傳回undefined,而不會進行delete運算。
(3)括号的影響
如果屬性鍊有圓括号,鍊判斷運算符對圓括号外部沒有影響,隻對圓括号内部有影響。
(a?.b).c
// 等價于
(a == null ? undefined : a.b).c
上面代碼中,?.對圓括号外部沒有影響,不管a對象是否存在,圓括号後面的.c總是會執行。
一般來說,使用?.運算符的場合,不應該使用圓括号。
(4)報錯場合
以下寫法是禁止的,會報錯。
// 構造函數
new a?.()
new a?.b()
// 鍊判斷運算符的右側有模闆字元串
a?.`{b}`
a?.b`{c}`
// 鍊判斷運算符的左側是 super
super?.()
super?.foo
// 鍊運算符用于指派運算符左側
a?.b = c
(5)右側不得為十進制數值
為了保證相容以前的代碼,允許foo?.3:0被解析成foo ? .3 : 0,是以規定如果?.後面緊跟一個十進制數字,那麼?.不再被看成是一個完整的運算符,而會按照三元運算符進行處理,也就是說,那個小數點會歸屬于後面的十進制數字,形成一個小數。
8、Null 判斷運算符
讀取對象屬性的時候,如果某個屬性的值是null或undefined,有時候需要為它們指定預設值。常見做法是通過||運算符指定預設值。
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
上面的三行代碼都通過||運算符指定預設值,但是這樣寫是錯的。開發者的原意是,隻要屬性的值為null或undefined,預設值就會生效,但是屬性的值如果為空字元串或false或0,預設值也會生效。
為了避免這種情況,ES2020 引入了一個新的 Null 判斷運算符 ??。它的行為類似||,但是隻有運算符左側的值為null或undefined時,才會傳回右側的值。
const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
上面代碼中,預設值隻有在屬性值為null或undefined時,才會生效。
這個運算符的一個目的,就是跟鍊判斷運算符 ?. 配合使用,為null或undefined的值設定預設值。
上面代碼中,response.settings如果是null或undefined,就會傳回預設值300。
這個運算符很适合判斷函數參數是否指派。
function Component(props) {
const enable = props.enabled ?? true;
// …
}
上面代碼判斷props參數的enabled屬性是否指派,等同于下面的寫法。
function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}
??有一個運算優先級問題,它與&&和||的優先級孰高孰低。現在的規則是,如果多個邏輯運算符一起使用,必須用括号表明優先級,否則會報錯。
// 報錯
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
上面四個表達式都會報錯,必須加入表明優先級的括号,如下:
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);
(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);
(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
完
如果對你産生了幫助,請點個贊給我吧!感謝觀看!