一、Symbol
- 為啥需要Symbol?
ES5里面對象的屬性名都是字符串,如果你需要使用一個(gè)別人提供的對象,你對這個(gè)對象有哪些屬性也不是很清楚,但又想為這個(gè)對象新增一些屬性,那么你新增的屬性名就很可能和原來的屬性名發(fā)送沖突,顯然我們是不希望這種情況發(fā)生的。所以,我們需要確保每個(gè)屬性名都是獨(dú)一無二的,這樣就可以防止屬性名的沖突了。因此,ES6里就引入了Symbol,用它來產(chǎn)生一個(gè)獨(dú)一無二的值。 - Symbol是什么?
Symbol實(shí)際上是ES6引入的一種原始數(shù)據(jù)類型,除了Symbol,JavaScript還有其他6種數(shù)據(jù)類型,分別是Undefined、Null、Boolean、String、Number、對象,這6種數(shù)據(jù)類型都是ES5中就有的。 - 怎么生成一個(gè)Symbol類型的值?
Symbol值是通過Symbol函數(shù)生成的。
let s = Symbol();
console.log(s); // Symbol()
typeof s; // "symbol"
- Symbol函數(shù)前不能用new。
Symbol函數(shù)不是一個(gè)構(gòu)造函數(shù),前面不能用new操作符。所以Symbol類型的值也不是一個(gè)對象,不能添加任何屬性,它只是一個(gè)類似于字符型的數(shù)據(jù)類型。如果強(qiáng)行在Symbol函數(shù)前加上new操作符,會(huì)報(bào)錯(cuò),如下:
let s = new Symbol();
// Uncaught TypeError: Symbol is not a constructor(…)
- Symbol函數(shù)的參數(shù)。
- 字符串作為參數(shù)。
用上面的方法生成的Symbol值不好進(jìn)行區(qū)分,Symbol函數(shù)還可以接受一個(gè)字符串參數(shù),來對產(chǎn)生的Symbol值進(jìn)行描述,方便我們區(qū)分不同的Symbol值。
let s1 = Symbol('s1');
let s2 = Symbol('s2');
console.log(s1); // Symbol(s1)
console.log(s2); // Symbol(s2)
s1 === s2; // false
let s3 = Symbol('s2');
s2 === s3; // false
給Symbol函數(shù)加了參數(shù)之后,控制臺輸出的時(shí)候可以區(qū)分到底是哪一個(gè)值。Symbol函數(shù)的參數(shù)只是對當(dāng)前Symbol值的描述,因此相同參數(shù)的Symbol函數(shù)返回值是不相等的。
- 對象作為參數(shù)。
如果Symbol函數(shù)的參數(shù)是一個(gè)對象,就會(huì)調(diào)用該對象的toString方法,將其轉(zhuǎn)化為一個(gè)字符串,然后才生成一個(gè)Symbol值。所以,說到底,Symbol函數(shù)的參數(shù)只能是字符串。 - Symbol值不可以進(jìn)行運(yùn)算。
既然Symbol是一種數(shù)據(jù)類型,那我們一定想知道Symbol值是否能進(jìn)行運(yùn)算。告訴你,Symbol值是不能進(jìn)行運(yùn)算的,不僅不能和Symbol值進(jìn)行運(yùn)算,也不能和其他類型的值進(jìn)行運(yùn)算,否則會(huì)報(bào)錯(cuò)。
Symbol值可以顯式轉(zhuǎn)化為字符串和布爾值,但是不能轉(zhuǎn)為數(shù)值。
var mysym1 = Symbol('my symbol');
mysym1.toString() // 'Symbol('my symbol')'
String(mysym1) // 'Symbol('my symbol')'
var mysym2 = Symbol();
Boolean(mysym2); // true
Number(mysym2) // TypeError: Cannot convert a Symbol value to a number(…)
- Symbol作屬性名。
Symbol就是為對象的屬性名而生,那么Symbol值怎么作為對象的屬性名呢?有下面幾種寫法:
let a = {};
let s4 = Symbol();
// 第一種寫法
a[s4] = 'mySymbol';
// 第二種寫法
a = {
[s4]: 'mySymbol'
}
// 第三種寫法
Object.defineProperty(a, s4, {value: 'mySymbol1'});
a.s4; // undefined
a.s4 = 'mySymbol2';
a[s4] // mySymbol1
a['s4'] // 'mySymbol2'
使用對象的Symbol值作為屬性名時(shí),獲取相應(yīng)的屬性值不能用點(diǎn)運(yùn)算符;
如果用點(diǎn)運(yùn)算符來給對象的屬性賦Symbol類型的值,實(shí)際上屬性名會(huì)變成一個(gè)字符串,而不是一個(gè)Symbol值;
在對象內(nèi)部,使用Symbol值定義屬性時(shí),Symbol值必須放在方括號之中,否則只是一個(gè)字符串。
- Symbol值作為屬性名的遍歷。
使用for...in和for...of都無法遍歷到Symbol值的屬性,Symbol值作為對象的屬性名,也無法通過Object.keys()、Object.getOwnPropertyNames()來獲取了。但是,不同擔(dān)心,這種平常的需求肯定是會(huì)有解決辦法的。我們可以使用Object.getOwnPropertySymbols()方法獲取一個(gè)對象上的Symbol屬性名。也可以使用Reflect.ownKeys()返回所有類型的屬性名,包括常規(guī)屬性名和 Symbol屬性名。
let s5 = Symbol('s5');
let s6 = Symbol('s6');
let a = {
[s5]: 's5',
[s6]: 's6'
}
Object.getOwnPropertySymbols(a); // [Symbol(s5), Symbol(s6)]
a.hello = 'hello';
Reflect.ownKeys(a); // ["hello", Symbol(s5), Symbol(s6)]
利用Symbol值作為對象屬性的名稱時(shí),不會(huì)被常規(guī)方法遍歷到這一特性,可以為對象定義一些非私有的但是又希望只有內(nèi)部可用的方法。
- Symbol.for()和Symbol.keyFor()。
Symbol.for()函數(shù)也可以用來生成Symbol值,但該函數(shù)有一個(gè)特殊的用處,就是可以重復(fù)使用一個(gè)Symbol值。
let s1 = Symbol.for("s11");
let s2 = Symbol.for("s22");
console.log(s1===s2)//false
let s3 = Symbol("s33");
let s4 = Symbol("s33");
console.log(s3===s4)//false
console.log(Symbol.keyFor(s3))//undefined
console.log(Symbol.keyFor(s2))//"s22"
console.log(Symbol.keyFor(s1))//"s11"
Symbol.for()函數(shù)要接受一個(gè)字符串作為參數(shù),先搜索有沒有以該參數(shù)作為名稱的Symbol值,如果有,就直接返回這個(gè)Symbol值,否則就新建并返回一個(gè)以該字符串為名稱的Symbol值。
Symbol.keyFor()函數(shù)是用來查找一個(gè)Symbol值的登記信息的,Symbol()寫法沒有登記機(jī)制,所以返回undefined;而Symbol.for()函數(shù)會(huì)將生成的Symbol值登記在全局環(huán)境中,所以Symbol.keyFor()函數(shù)可以查找到用Symbol.for()函數(shù)生成的Symbol值。
- 內(nèi)置Symbol值。
ES6提供了11個(gè)內(nèi)置的Symbol值,分別是Symbol.hasInstance 、Symbol.isConcatSpreadable 、Symbol.species 、Symbol.match 、Symbol.replace 、Symbol.search 、Symbol.split 、Symbol.iterator 、Symbol.toPrimitive 、Symbol.toStringTag 、Symbol.unscopables 等。
二、Set和Map數(shù)據(jù)結(jié)構(gòu)
- Set
es6提供了新的數(shù)據(jù)結(jié)構(gòu)Set,它類似于數(shù)組,但是成員的值都是唯一的,沒有重復(fù)的值。Set本身是一個(gè)構(gòu)造函數(shù),用來生成Set數(shù)據(jù)結(jié)構(gòu)。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
//通過add方法向 Set 結(jié)構(gòu)加入成員,結(jié)果表明 Set 結(jié)構(gòu)不會(huì)添加重復(fù)的值。
Set 函數(shù)可以接受一個(gè)數(shù)組作為參數(shù),用來初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
function divs () {
return [...document.querySelectorAll('div')];
}
const set = new Set(divs());
set.size // 56
// 類似于
divs().forEach(div => set.add(div));
set.size // 56
利用Set數(shù)據(jù)結(jié)構(gòu)的成員都是唯一的這個(gè)特性,可以輕松對數(shù)組去重。
// 去除數(shù)組的重復(fù)成員
[...new Set(array)]
向 Set 加入值的時(shí)候,不會(huì)發(fā)生類型轉(zhuǎn)換,所以5和"5"是兩個(gè)不同的值。
Set 內(nèi)部判斷兩個(gè)值是否不同,使用的算法叫做“Same-value equality”,它類似于精確相等運(yùn)算符(===),主要的區(qū)別是NaN等于自身,而精確相等運(yùn)算符認(rèn)為NaN不等于自身。
兩個(gè)對象總是不相等的。
Set 結(jié)構(gòu)的實(shí)例有以下屬性:
-
Set.prototype.constructor:構(gòu)造函數(shù),默認(rèn)就是Set函數(shù)。 -
Set.prototype.size:返回Set實(shí)例的成員總數(shù)。
Set 實(shí)例的方法分為兩大類:操作方法(用于操作數(shù)據(jù))和遍歷方法(用于遍歷成員)。
四個(gè)操作方法:
-
add(value):添加某個(gè)值,返回 Set 結(jié)構(gòu)本身。 -
delete(value):刪除某個(gè)值,返回一個(gè)布爾值,表示刪除是否成功。 -
has(value):返回一個(gè)布爾值,表示該值是否為Set的成員。 -
clear():清除所有成員,沒有返回值。
Array.from()可以將 Set 結(jié)構(gòu)轉(zhuǎn)為數(shù)組。
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
Set 結(jié)構(gòu)的實(shí)例有四個(gè)遍歷方法,可以用于遍歷成員。
-
keys():返回鍵名的遍歷器 -
values():返回鍵值的遍歷器 -
entries():返回鍵值對的遍歷器 -
forEach():使用回調(diào)函數(shù)遍歷每個(gè)成員
keys方法、values方法、entries方法返回的都是遍歷器對象。由于 Set 結(jié)構(gòu)沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個(gè)值),所以keys方法和values方法的行為完全一致。
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
Set 結(jié)構(gòu)的實(shí)例默認(rèn)可遍歷,它的默認(rèn)遍歷器生成函數(shù)就是它的values方法。
Set 結(jié)構(gòu)的實(shí)例與數(shù)組一樣,也擁有forEach方法,用于對每個(gè)成員執(zhí)行某種操作,沒有返回值。
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
forEach方法還可以有第二個(gè)參數(shù),表示綁定處理函數(shù)內(nèi)部的this對象。
- WeakSet
WeakSet 結(jié)構(gòu)與 Set 類似,也是不重復(fù)的值的集合。但是,它與 Set 有兩個(gè)區(qū)別:
- WeakSet 的成員只能是對象,而不能是其他類型的值。
- WeakSet 中的對象都是弱引用,即垃圾回收機(jī)制不考慮 WeakSet 對該對象的引用,也就是說,如果其他對象都不再引用該對象,那么垃圾回收機(jī)制會(huì)自動(dòng)回收該對象所占用的內(nèi)存,不考慮該對象還存在于 WeakSet 之中。
由于上面這個(gè)特點(diǎn),WeakSet 的成員是不適合引用的,因?yàn)樗鼤?huì)隨時(shí)消失。另外,由于 WeakSet 內(nèi)部有多少個(gè)成員,取決于垃圾回收機(jī)制有沒有運(yùn)行,運(yùn)行前后很可能成員個(gè)數(shù)是不一樣的,而垃圾回收機(jī)制何時(shí)運(yùn)行是不可預(yù)測的,因此 ES6 規(guī)定 WeakSet 不可遍歷。
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
//下面的寫法不行
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
WeakSet 結(jié)構(gòu)有以下三個(gè)方法。
-
WeakSet.prototype.add(value):向 WeakSet 實(shí)例添加一個(gè)新成員。 -
WeakSet.prototype.delete(value):清除 WeakSet 實(shí)例的指定成員。 -
WeakSet.prototype.has(value):返回一個(gè)布爾值,表示某個(gè)值是否在 WeakSet 實(shí)例之中。
WeakSet 沒有size屬性,沒有辦法遍歷它的成員。
- Map
JavaScript 的對象(Object),本質(zhì)上是鍵值對的集合(Hash 結(jié)構(gòu)),但是傳統(tǒng)上只能用字符串當(dāng)作鍵。這給它的使用帶來了很大的限制。
為了解決這個(gè)問題,ES6 提供了 Map 數(shù)據(jù)結(jié)構(gòu)。它類似于對象,也是鍵值對的集合,但是“鍵”的范圍不限于字符串,各種類型的值(包括對象)都可以當(dāng)作鍵。也就是說,Object 結(jié)構(gòu)提供了“字符串—值”的對應(yīng),Map 結(jié)構(gòu)提供了“值—值”的對應(yīng),是一種更完善的 Hash 結(jié)構(gòu)實(shí)現(xiàn)。如果你需要“鍵值對”的數(shù)據(jù)結(jié)構(gòu),Map 比 Object 更合適。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
作為構(gòu)造函數(shù),Map 也可以接受一個(gè)數(shù)組作為參數(shù)。該數(shù)組的成員是一個(gè)個(gè)表示鍵值對的數(shù)組。
const map = new Map([
['name', '張三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"
只有對同一個(gè)對象的引用,Map 結(jié)構(gòu)才將其視為同一個(gè)鍵。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
如果 Map 的鍵是一個(gè)簡單類型的值(數(shù)字、字符串、布爾值),則只要兩個(gè)值嚴(yán)格相等,Map 將其視為一個(gè)鍵,比如0和-0就是一個(gè)鍵,布爾值true和字符串true則是兩個(gè)不同的鍵。另外,undefined和null也是兩個(gè)不同的鍵。雖然NaN不嚴(yán)格相等于自身,但 Map 將其視為同一個(gè)鍵。
實(shí)例的屬性和操作方法
- size屬性 返回成員總數(shù)
- set(key,value) 設(shè)置鍵值對,返回Map結(jié)構(gòu)
- get(key) 讀取key對應(yīng)的值,找不到就是undefined
- has(key) 返回布爾值,表示key是否在Map中
- delete(key) 刪除某個(gè)鍵,返回true,失敗返回false
- clear() 清空所有成員,沒有返回值
Map 結(jié)構(gòu)原生提供三個(gè)遍歷器生成函數(shù)和一個(gè)遍歷方法。 -
keys():返回鍵名的遍歷器。 -
values():返回鍵值的遍歷器。 -
entries():返回所有成員的遍歷器。 -
forEach():遍歷 Map 的所有成員。
Map 的遍歷順序就是插入順序。遍歷行為基本與set的一致。
Map可以轉(zhuǎn)為數(shù)組。
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap] //[[true, 7], [{foo: 3},["abc"]]]
數(shù)組也可以轉(zhuǎn)為Map
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
如果所有 Map 的鍵都是字符串,它可以轉(zhuǎn)為對象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
對象也可以轉(zhuǎn)為Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
Map可以轉(zhuǎn)為JSON,但是要分兩種情況。
一種情況是,Map 的鍵名都是字符串,這時(shí)可以選擇轉(zhuǎn)為對象 JSON。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一種情況是,Map 的鍵名有非字符串,這時(shí)可以選擇轉(zhuǎn)為數(shù)組 JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
JSON可以轉(zhuǎn)為Map
正常情況下,所有鍵名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
但是,有一種特殊情況,整個(gè) JSON 就是一個(gè)數(shù)組,且每個(gè)數(shù)組成員本身,又是一個(gè)有兩個(gè)成員的數(shù)組。這時(shí),它可以一一對應(yīng)地轉(zhuǎn)為 Map。這往往是數(shù)組轉(zhuǎn)為 JSON 的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
- WeakMap
WeakMap結(jié)構(gòu)與Map結(jié)構(gòu)類似,也是用于生成鍵值對的集合。
WeakMap與Map的區(qū)別有兩點(diǎn):
-
WeakMap只接受對象作為鍵名(null除外),不接受其他類型的值作為鍵名。 -
WeakMap的鍵名所指向的對象,不計(jì)入垃圾回收機(jī)制。
WeakMap的設(shè)計(jì)目的在于,有時(shí)我們想在某個(gè)對象上面存放一些數(shù)據(jù),但是這會(huì)形成對于這個(gè)對象的引用。
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
//e1和e2是兩個(gè)對象,我們通過arr數(shù)組對這兩個(gè)對象添加一些文字說明。這就形成了arr對e1和e2的引用。
//一旦不再需要這兩個(gè)對象,我們就必須手動(dòng)刪除這個(gè)引用,否則垃圾回收機(jī)制就不會(huì)釋放e1和e2占用的內(nèi)存。
WeakMap 就是為了解決這個(gè)問題而誕生的,它的鍵名所引用的對象都是弱引用,即垃圾回收機(jī)制不將該引用考慮在內(nèi)。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
WeakMap只有四個(gè)方法可用:get()、set()、has()、delete()。
無法被遍歷,因?yàn)闆]有size。無法被清空,因?yàn)闆]有clear(),跟WeakSet相似。
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
上面代碼中,myElement是一個(gè) DOM 節(jié)點(diǎn),每當(dāng)發(fā)生click事件,就更新一下狀態(tài)。我們將這個(gè)狀態(tài)作為鍵值放在 WeakMap 里,對應(yīng)的鍵名就是myElement。一旦這個(gè) DOM 節(jié)點(diǎn)刪除,該狀態(tài)就會(huì)自動(dòng)消失,不存在內(nèi)存泄漏風(fēng)險(xiǎn)。
三、Proxy
- Proxy 用于修改某些操作的默認(rèn)行為,等同于在語言層面做出修改,所以屬于一種“元編程”,即對編程語言進(jìn)行編程。
Proxy 可以理解成,在目標(biāo)對象之前架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對外界的訪問進(jìn)行過濾和改寫。Proxy 這個(gè)詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
//上面代碼對一個(gè)空對象架設(shè)了一層攔截,重定義了屬性的讀取(get)和設(shè)置(set)行為
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上面代碼說明,Proxy 實(shí)際上重載(overload)了點(diǎn)運(yùn)算符,即用自己的定義覆蓋了語言的原始定義。
ES6 原生提供 Proxy 構(gòu)造函數(shù),用來生成 Proxy 實(shí)例。
let proxy = new Proxy(target, handler);
Proxy 對象的所有用法,都是上面這種形式,不同的只是handler參數(shù)的寫法。其中,new Proxy()表示生成一個(gè)Proxy實(shí)例,target參數(shù)表示所要攔截的目標(biāo)對象,handler參數(shù)也是一個(gè)對象,用來定制攔截行為。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
如果handler沒有設(shè)置任何攔截,那就等同于直接通向原對象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代碼中,handler是一個(gè)空對象,沒有任何攔截效果,訪問proxy就等同于訪問target。
同一個(gè)攔截器函數(shù),可以設(shè)置攔截多個(gè)操作。
對于可以設(shè)置、但沒有設(shè)置攔截的操作,則直接落在目標(biāo)對象上,按照原先的方式產(chǎn)生結(jié)果。
下面是 Proxy 支持的攔截操作一覽,一共 13 種:
-
get(target, propKey, receiver):攔截對象屬性的讀取,比如
proxy.foo和proxy['foo']。 -
set(target, propKey, value, receiver):攔截對象屬性的設(shè)置,比如
proxy.foo = v或proxy['foo'] = v,返回一個(gè)布爾值。 -
has(target, propKey):攔截
propKey in proxy的操作,返回一個(gè)布爾值。 -
deleteProperty(target, propKey):攔截
delete proxy[propKey]的操作,返回一個(gè)布爾值。 -
ownKeys(target):攔截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一個(gè)數(shù)組。該方法返回目標(biāo)對象所有自身的屬性的屬性名,而Object.keys()的返回結(jié)果僅包括目標(biāo)對象自身的可遍歷屬性。 -
getOwnPropertyDescriptor(target, propKey):攔截
Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。 -
defineProperty(target, propKey, propDesc):攔截
Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個(gè)布爾值。 -
preventExtensions(target):攔截
Object.preventExtensions(proxy),返回一個(gè)布爾值。 -
getPrototypeOf(target):攔截
Object.getPrototypeOf(proxy),返回一個(gè)對象。 -
isExtensible(target):攔截
Object.isExtensible(proxy),返回一個(gè)布爾值。 -
setPrototypeOf(target, proto):攔截
Object.setPrototypeOf(proxy, proto),返回一個(gè)布爾值。如果目標(biāo)對象是函數(shù),那么還有兩種額外操作可以攔截。 -
apply(target, object, args):攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作,比如
proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 -
construct(target, args):攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作,比如
new proxy(...args)。
deleteProperty方法用于攔截delete操作,如果這個(gè)方法拋出錯(cuò)誤或者返回false,當(dāng)前屬性就無法被delete命令刪除。
apply方法攔截函數(shù)的調(diào)用、call和apply操作。
get方法用于攔截某個(gè)屬性的讀取操作。
let obj2 = new Proxy(obj,{
get(target,property,a){
//return 35;
/*console.log(target)
console.log(property)*/
let Num = ++wkMap.get(obj).getPropertyNum;
console.log(`當(dāng)前訪問對象屬性次數(shù)為:${Num}`)
return target[property]
},
deleteProperty(target,property){
return false;
},
apply(target,ctx,args){
return Reflect.apply(...[target,[],args]);;
}
})
-
Proxy.revocable方法返回一個(gè)可取消的 Proxy 實(shí)例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable方法返回一個(gè)對象,該對象的proxy屬性是Proxy實(shí)例,revoke屬性是一個(gè)函數(shù),可以取消Proxy實(shí)例。上面代碼中,當(dāng)執(zhí)行revoke函數(shù)之后,再訪問Proxy實(shí)例,就會(huì)拋出一個(gè)錯(cuò)誤。
Proxy.revocable的一個(gè)使用場景是,目標(biāo)對象不允許直接訪問,必須通過代理訪問,一旦訪問結(jié)束,就收回代理權(quán),不允許再次訪問。
- this問題。
雖然 Proxy 可以代理針對目標(biāo)對象的訪問,但它不是目標(biāo)對象的透明代理,即不做任何攔截的情況下,也無法保證與目標(biāo)對象的行為一致。主要原因就是在 Proxy 代理的情況下,目標(biāo)對象內(nèi)部的this關(guān)鍵字會(huì)指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
//一旦proxy代理target.m,后者內(nèi)部的this就是指向proxy,而不是target。
四、Reflect
Reflect對象與Proxy對象一樣,也是 ES6 為了操作對象而提供的新 API。
設(shè)計(jì)目的:
- 將
Object對象的一些明顯屬于語言內(nèi)部的方法(比如Object.defineProperty),放到Reflect對象上?,F(xiàn)階段,某些方法同時(shí)在Object和Reflect對象上部署,未來的新方法將只部署在Reflect對象上。 - 修改某些
Object方法的返回結(jié)果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時(shí),會(huì)拋出一個(gè)錯(cuò)誤,而Reflect.defineProperty(obj, name, desc)則會(huì)返回false。 - 讓
Object操作都變成函數(shù)行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數(shù)行為。 -
Reflect對象的方法與Proxy對象的方法一一對應(yīng),只要是Proxy對象的方法,就能在Reflect對象上找到對應(yīng)的方法。這就讓Proxy對象可以方便地調(diào)用對應(yīng)的Reflect方法,完成默認(rèn)行為,作為修改行為的基礎(chǔ)。也就是說,不管Proxy怎么修改默認(rèn)行為,你總可以在Reflect上獲取默認(rèn)行為。
五、Promise
- 概念。
Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。
Promise,簡單說就是一個(gè)容器,里面保存著某個(gè)未來才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。 - 特點(diǎn)。
- 對象的狀態(tài)不受外界影響。
- 一旦狀態(tài)改變,就不會(huì)再變,任何時(shí)候都可以得到這個(gè)結(jié)果。
- 狀態(tài)。
Promise對象代表一個(gè)異步操作,有三種狀態(tài):
pending(進(jìn)行中)、fulfilled(已成功)和rejected(已失敗)。
只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無法改變這個(gè)狀態(tài)。 - 缺點(diǎn)。
- 無法取消
Promise,一旦新建它就會(huì)立即執(zhí)行,無法中途取消。 - 如果不設(shè)置回調(diào)函數(shù),
Promise內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部。 - 當(dāng)處于
pending狀態(tài)時(shí),無法得知目前進(jìn)展到哪一個(gè)階段(剛剛開始還是即將完成)。
- 用法。
let p = new Promise((resolve,reject)=>{
//一些異步操作
setTimeout(()=>{
console.log("123")
resolve("abc");
},0)
})
.then(function(data){
//resolve狀態(tài)
console.log(data)
},function(err){
//reject狀態(tài)
})
//'123'
//'abc'
Promise實(shí)例生成以后,可以用then方法分別指定resolved狀態(tài)和rejected狀態(tài)的回調(diào)函數(shù)。
也就是說,狀態(tài)由實(shí)例化時(shí)的參數(shù)(函數(shù))執(zhí)行來決定的,根據(jù)不同的狀態(tài),看看需要走then的第一個(gè)參數(shù)還是第二個(gè)。
resolve()和reject()的參數(shù)會(huì)傳遞到對應(yīng)的回調(diào)函數(shù)的data或err。
- 鏈?zhǔn)讲僮鞯挠梅ā?br> 從表面上看,Promise只是能夠簡化層層回調(diào)的寫法,而實(shí)質(zhì)上,Promise的精髓是“狀態(tài)”,用維護(hù)狀態(tài)、傳遞狀態(tài)的方式來使得回調(diào)函數(shù)能夠及時(shí)調(diào)用,它比傳遞callback函數(shù)要簡單、靈活的多。所以使用Promise的正確場景是這樣的:
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
//異步任務(wù)1執(zhí)行完成
//隨便什么數(shù)據(jù)1
//異步任務(wù)2執(zhí)行完成
//隨便什么數(shù)據(jù)2
//異步任務(wù)3執(zhí)行完成
//隨便什么數(shù)據(jù)3
function runAsync1(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('異步任務(wù)1執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)1');
}, 1000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('異步任務(wù)2執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)2');
}, 2000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('異步任務(wù)3執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)3');
}, 2000);
});
return p;
}
在then方法中,你也可以直接return數(shù)據(jù)而不是Promise對象,在后面的then中也可以接收到數(shù)據(jù):
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return '直接返回?cái)?shù)據(jù)'; //這里直接返回?cái)?shù)據(jù)
})
.then(function(data){
console.log(data);
});
//異步任務(wù)1執(zhí)行完成
//隨便什么數(shù)據(jù)1
//異步任務(wù)2執(zhí)行完成
//隨便什么數(shù)據(jù)2
//直接返回?cái)?shù)據(jù)
- reject的用法。
前面的例子都是只有“執(zhí)行成功”的回調(diào),還沒有“失敗”的情況,reject的作用就是把Promise的狀態(tài)置為rejected,這樣我們在then中就能捕捉到,然后執(zhí)行“失敗”情況的回調(diào)。
let num = 10;
let p1 = function() {
return new Promise((resolve,reject)=>{
if (num <= 5) {
resolve("<=5,走resolce")
console.log('resolce不能結(jié)束Promise')
}else{
reject(">5,走reject")
console.log('reject不能結(jié)束Promise')
}
})
}
p1()
.then(function(data){
console.log(data)
},function(err){
console.log(err)
})
//reject不能結(jié)束Promise
//>5,走reject
resolve和reject永遠(yuǎn)會(huì)在當(dāng)前環(huán)境的最后執(zhí)行,所以后面的同步代碼會(huì)先執(zhí)行。
如果resolve和reject之后還有代碼需要執(zhí)行,最好放在then里。
然后在resolve和reject前面寫上return。
-
Promise.prototype.catch。
Promise.prototype.catch方法是.then(null, rejection)的別名,用于指定發(fā)生錯(cuò)誤時(shí)的回調(diào)函數(shù)。
p1()
.then(function(data){
console.log(data)
})
.catch(function(err){
console.log(err)
})
//reject不能結(jié)束Promise
//>5,走reject
-
Promise.all()。
Promise.all方法用于將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。
const p = Promise.all([p1, p2, p3]);
p的狀態(tài)由p1、p2、p3決定,分成兩種情況。
- 只有
p1、p2、p3的狀態(tài)都變成resolve,p的狀態(tài)才會(huì)變成resolve。 此時(shí)p1、p2、p3的返回值組成一個(gè)數(shù)組,傳遞給p的回調(diào)函數(shù)。 - 只要
p1、p2、p3之中有一個(gè)被rejected,p的狀態(tài)就變成rejected,此時(shí)第一個(gè)被reject的實(shí)例的返回值,會(huì)傳遞給p的回調(diào)函數(shù)。
promises是包含 3 個(gè) Promise 實(shí)例的數(shù)組,只有這 3 個(gè)實(shí)例的狀態(tài)都變成resolve,或者其中有一個(gè)變?yōu)?code>rejected,才會(huì)調(diào)用Promise.all方法后面的回調(diào)函數(shù)。
如果作為參數(shù)的 Promise 實(shí)例,自己定義了catch方法,那么它一旦被rejected,并不會(huì)觸發(fā)Promise.all()的catch方法,如果沒有參數(shù)沒有定義自己的catch,就會(huì)調(diào)用Promise.all()的catch方法。
-
Promise.race。
Promise.race方法同樣是將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。
const p = Promise.race([p1, p2, p3]);
上面代碼中,只要p1、p2、p3之中有一個(gè)實(shí)例率先改變狀態(tài),p的狀態(tài)就跟著改變。那個(gè)率先改變的 Promise 實(shí)例的返回值,就傳遞給p的回調(diào)函數(shù)。
-
Promise.resolve()。
有時(shí)需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象,Promise.resolve方法就起到這個(gè)作用。下面代碼將123轉(zhuǎn)為一個(gè) Promise 對象。
const jsPromise = Promise.resolve('123');
Promise.resolve等價(jià)于下面的寫法。
Promise.resolve('123')
// 等價(jià)于
new Promise(resolve => resolve('123'))
Promise.resolve方法的參數(shù)分成四種情況。
-
參數(shù)是一個(gè) Promise 實(shí)例。
如果參數(shù)是 Promise 實(shí)例,那么Promise.resolve將不做任何修改、原封不動(dòng)地返回這個(gè)實(shí)例。 -
參數(shù)是一個(gè)thenable對象。
thenable對象指的是具有then方法的對象,比如下面這個(gè)對象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve方法會(huì)將這個(gè)對象轉(zhuǎn)為 Promise 對象,然后就立即執(zhí)行thenable對象的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
上面代碼中,thenable對象的then方法執(zhí)行后,對象p1的狀態(tài)就變?yōu)?code>resolved,從而立即執(zhí)行最后那個(gè)then方法指定的回調(diào)函數(shù),輸出 42。
-
參數(shù)不是具有then方法的對象,或根本就不是對象
如果參數(shù)是一個(gè)原始值,或者是一個(gè)不具有then方法的對象,則Promise.resolve方法返回一個(gè)新的 Promise 對象,狀態(tài)為resolved。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代碼生成一個(gè)新的 Promise 對象的實(shí)例p。由于字符串Hello不屬于異步操作(判斷方法是字符串對象不具有 then 方法),返回 Promise 實(shí)例的狀態(tài)從一生成就是resolved,所以回調(diào)函數(shù)會(huì)立即執(zhí)行。Promise.resolve方法的參數(shù),會(huì)同時(shí)傳給回調(diào)函數(shù)。
-
不帶有任何參數(shù)
Promise.resolve方法允許調(diào)用時(shí)不帶參數(shù),直接返回一個(gè)resolved狀態(tài)的 Promise 對象。
所以,如果希望得到一個(gè) Promise 對象,比較方便的方法就是直接調(diào)用Promise.resolve方法。
const p = Promise.resolve();
p.then(function () {
// ...
});
上面代碼的變量p就是一個(gè) Promise 對象。
需要注意的是,立即resolve的 Promise 對象,是在本輪“事件循環(huán)”(event loop)的結(jié)束時(shí),而不是在下一輪“事件循環(huán)”的開始時(shí)。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
上面代碼中,setTimeout(fn, 0)在下一輪“事件循環(huán)”開始時(shí)執(zhí)行,Promise.resolve()在本輪“事件循環(huán)”結(jié)束時(shí)執(zhí)行,console.log('one')則是立即執(zhí)行,因此最先輸出。
-
Promise.reject()。
Promise.reject(reason)方法也會(huì)返回一個(gè)新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為rejected。
const p = Promise.reject('出錯(cuò)了');
// 等同于
const p = new Promise((resolve, reject) => reject('出錯(cuò)了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯(cuò)了
上面代碼生成一個(gè) Promise 對象的實(shí)例p,狀態(tài)為rejected,回調(diào)函數(shù)會(huì)立即執(zhí)行。
注意,Promise.reject()方法的參數(shù),會(huì)原封不動(dòng)地作為reject的理由,變成后續(xù)方法的參數(shù)。這一點(diǎn)與Promise.resolve方法不一致。
const thenable = {
then(resolve, reject) {
reject('出錯(cuò)了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代碼中,Promise.reject方法的參數(shù)是一個(gè)thenable對象,執(zhí)行以后,后面catch方法的參數(shù)不是reject拋出的“出錯(cuò)了”這個(gè)字符串,而是thenable對象。
六、Fetch
- 傳統(tǒng)Ajax指的是XMLHttpRequest(XHR),現(xiàn)在和將來會(huì)被Fetch取代。
XMLHttpRequest是一個(gè)設(shè)計(jì)粗糙的API,配置和調(diào)用方式非?;靵y,而且基于事件的異步模型寫起來也沒有現(xiàn)代的Promise,generator/yield,async/await友好。
Fetch的出現(xiàn)就是為了解決XHR的問題。但是Fetch API是基于Promise設(shè)計(jì),舊瀏覽器不支持Promise,需要使用polyfill es6-promise。
使用XHR發(fā)送一個(gè)json請求一般是這樣:
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function(){
console.log(xhr.response);
};
xhr.onerror = function(){
console.log("Oops, error");
};
xhr.send();
使用Fetch后:
fetch(url)
.then(function(response){
return response.json();
}).then(function(data){
console.log(data);
}).catch(function(e){
console.log("Oops, error");
});
使用ES6的箭頭函數(shù)后:
fetch(url)
.then(response => response.json())
.then(data => console.log(data))
.catch(e => console.log("Oops, error", e))
使用async/await來做最終優(yōu)化:
try{
let response = await fetch(url);
let data = await response.json();
console.log(data);
}catch(e){
console.log("Oops, error", e);
}
//注:這段代碼如果想運(yùn)行,外面需要包一個(gè)async function
- 用法
fetch(url, options).then(function(response){
//handle HTTP response
}, function(error){
//handle network error
})
url:定義要獲取的資源。這可能是:
- 一個(gè)
USVString字符串,包含要獲取資源的URL。 - 一個(gè)
Request對象。
options(可選)
一個(gè)配置項(xiàng)對象,包括所有對請求的設(shè)置,可選的參數(shù)有: -
method:請求使用的方法,如GET、POST。 -
headers:請求的頭信息,形式為Headers對象或ByteString。 -
body:請求的body信息,可能是一個(gè)Blob、BufferSource、FormData、URLSearchParams或者USVString對象。注意GET或HEAD方法的請求不能包含body信息。 -
mode:請求的模式,如cors、no-cors或者same-origin。 -
credentials:請求的credentials,如omit、same-origin或者include。 -
cache:請求的cache模式:default,no-store,reload,no-cache,force-cache,或者only-if-cached。
response:
一個(gè)Promise,resolve時(shí)回傳Response對象:
屬性:
- status(number)-HTTP請求結(jié)果參數(shù),在100~599范圍。
- statusText(String)-服務(wù)器返回的狀態(tài)報(bào)告。
- ok(boolean)-如果返回200表示請求成功則為true。
- headers(Headers)-返回頭部信息,下面詳細(xì)介紹。
- url(String)-請求的地址。
方法: - text() -以string的形式生成請求text。
- json() -生成JSON.parse(responseText)的結(jié)果。
- blob() -生成一個(gè)Blob。
- arrayBuffer() -生成一個(gè)ArrayBuffer。
- formData() -生成格式化的數(shù)據(jù),可用于其他的請求。
其他方法: - clone() -創(chuàng)建一個(gè)Response對象的克隆。
- Response.error() -返回一個(gè)綁定了網(wǎng)絡(luò)錯(cuò)誤的新的Response對象。
- Response.redirect() -用另一個(gè)url創(chuàng)建一個(gè)新的response。
response.headers: - has(name)(boolean) -判斷是否存在該信息頭。
- get(name)(String) -獲取信息頭的數(shù)據(jù)。
- getAll(name)(Array) -獲取所有頭部數(shù)據(jù)。
- set(name, value) -設(shè)置信息頭的參數(shù)。
append(name, value) -添加header的內(nèi)容。
delete(name) -刪除header的信息。
forEach(function(value, name){...},[thisContext]) -循環(huán)讀取header的信息。
get
fetch('/users.html')
.then(function(response){
return response.text();
}).then(function(body){
document.body.innerHTML = body
})
post
fetch('/users', {
method:'POST',
headers:{
'Accept':'application/json',
'Content-Type':'application/json'
},
body:JSON.stringify({
name:'Hubot',
login:'hubot',
})
})
- Fetch優(yōu)點(diǎn)主要有:
- 語法簡潔,更加語義化。
- 基于標(biāo)準(zhǔn)Promise實(shí)現(xiàn),支持async/await
- Fetch的原生支持率不高,需要引入下面的polyfill后才完美支持IE8+:
- 由于IE8是ES3,需要引入ES5的polyfill:es5-shim,es5-sham
- 引入Promise的polyfill:es6-promise
- 引入fetch探測庫:fetch-detector
- 引入fetch的polyfill:fetch-ie8
- 可選:如果還使用了jsonp,引入fetch-jsonp
- 可選:開啟Babel的runtime模式,現(xiàn)在就使用async/await