ECMAScript 新特性整理匯總?cè)?/h1>

來源 | 致前端 - https://www.zhiqianduan.com
1、ECMAScript 概述
ECMAScript他也是一門腳本語言,一般縮寫為ES,通常會把他看作為JavaScript的標準規(guī)范。 但實際上JavaScript是ECMAScript的擴展語言,因為ECMAScript只是提供了最基本的語法,通俗點來說只是約定了代碼的如何編寫,例如該怎么樣定義變量或函數(shù),怎樣去實現(xiàn)分支或者循環(huán)之類的語句,這只是停留在語言層面,并不能完成應用中的實際功能的開發(fā)。 JavaScript實現(xiàn)了標準開發(fā),在語言基礎上做了一定的擴展,使得可以在瀏覽器環(huán)境中操作DOM和BOM,在Node環(huán)境可以讀寫文件等操作。 總的來說瀏覽器環(huán)境中的JavaScript等于ECMAScript加上web所提供的API,也就是DOM和BOM。 在Node環(huán)境中所使用的JavaScript,實際上就等于是ECMAScript加上Node所提供的一系列API。例如fs或者net這樣的內(nèi)置模塊。 2015年開始ECMAScript就保持每年一個大版本的迭代。JavaScript這門語言的變得越來越高級,越來越便捷。 2、 let 與塊級作用域
在ES2015之前,ECMAScript當中只有全局作用域和函數(shù)作用域兩種類型的作用域。在ES2015中又新增了一個塊級作用域。
塊指的就是代碼中用一對{}所包裹起來的范圍,例如if語句和for語句中的{}都會產(chǎn)生這里所說的塊。
if (true) { consoel.log('yd');}
for (var i = 0; i < 10; i++) { console.log('yd');}
在ECMAScript2015以前,塊是沒有單獨的作用域的,這就導致在塊中定義的成員外部也可以訪問到。
例如在if當中去定義了一個foo的變量,然后在if的外面打印這個foo,結(jié)果也是可以正常打印出來的。
if (true) { var foo = 'yd';}console.log(foo); // yd
這一點對于復雜代碼是非常不利的,也是不安全的,有了塊級作用域之后,可以在代碼當中通過let這個新的關(guān)鍵詞聲明變量。
他的用法和var是一樣的,只不過通過let聲明的變量他只能在所聲明的這個代碼塊中被訪問到。
if (true) { let foo = 'yd';}console.log(foo); // foo is not defined
在快級內(nèi)定義的成員,外部是無法訪問的。
這樣一個特性非常適合聲明for循環(huán)當中的計數(shù)器。
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) { console.log(i); }}
3、const
他可以用來去聲明一個只讀的恒量或者叫常量,他的特點就是在let的基礎上多了一個只讀特性。
所謂只讀指的就是變量一旦聲明過后就不能夠再被修改,如果在聲明過后再去修改這個成員就會出現(xiàn)錯誤。
const name = 'yd';
name = 'zd'; // 錯誤
這里要注意的問題const所聲明的成員不能被修改,并不是說不允許修改恒量中的屬性成員。下面這種情況是允許的。
const obj = {}obj.name = 'yd';
4、 數(shù)組的解構(gòu)
ECMAScript2015新增了從數(shù)組或?qū)ο笾蝎@取指定元素的一種快捷方式,這是一種新的語法,這種新語法叫做解構(gòu)。
const arr = [100, 200, 300];const [foo, bar, baz] = arr;console.log(foo, bar, baz);
如果只是想獲取其中某個位置所對應的成員,例如只獲取第三個成員, 這里可以把前兩個成員都刪掉。但是需要保留對應的逗號。確保解構(gòu)位置的格式與數(shù)組是一致的。這樣的話就能夠提取到指定位置的成員。
const [, , baz] = arr;console.log(baz);
除此之外還可以在解構(gòu)位置的變量名之前添加...表示提取從當前位置開始往后的所有成員,最終所有的結(jié)果會放在一個數(shù)組當中。
const [foo, ...rest] = arr;console.log(rest);
三個點的用法只能在解構(gòu)位置的最后一個成員上使用,另外如果解構(gòu)位置的成員個數(shù)小于被解構(gòu)的數(shù)組長度,就會按照從前到后的順序去提取,多出來的成員就不會被提取。
反之如果解構(gòu)位置的成員大于數(shù)組長度,那么提取到的就是undefined。這和訪問數(shù)組當中一個不存在的下標是一樣的。
const [foo, bar, baz, more] = arr;console.log(more); // undefined
如果需要給提取到的成員設置默認值,這種語法也是支持的,只需要在解構(gòu)變量的后面跟上一個等號,然后后面寫上一個默認值。
const [foo, bar, baz, more = 'default value'] = arr;console.log(more);
5、 對象的解構(gòu)
對象也同樣可以被解構(gòu),不過對象的結(jié)構(gòu)需要去根據(jù)屬性名去匹配提取,而不是位置。
const obj = { name: 'yd', age: 18 };
解構(gòu)他里面的成員就是在以前變量位置去使用一個對象字面量的{}, 然后在{}里同樣也是提取出來的數(shù)據(jù)所存放的變量名,不過這里的變量名還有一個很重要的作用就是去匹配被解構(gòu)對象中的成員,從而去提取指定成員的值。
const obj = { name: 'yd', age: 18 };
const { name } = obj;
解構(gòu)對象的其他特點基本上和解構(gòu)數(shù)組是完全一致的。未匹配到的成員返回undefined,也可以設置默認值。
在對象當中有一個特殊的情況,解構(gòu)的變量名是被解構(gòu)對象的屬性名,所以說當前作用域中如果有這個名稱就會產(chǎn)生沖突。這個時候可以使用重命名的方式去解決這個問題。
const obj = { name: 'yd', age: 18 };
const { name: name1 } = obj;
console.log(name1);
解構(gòu)對象的應用場景比較多,不過大部分的場景都是為了簡化代碼,比如代碼中如果大量用到了console對象的方法,可以先把這個對象單獨解構(gòu)出來,然后再去使用獨立的log方法。
const { log } = console;log('1');
6、模板字符串
傳統(tǒng)定義字符串的方式需要通過'或者是"來標識。ES2015新增了模板字符串,使用反引號`聲明,。如果在字符串中需要使用`,可以使用斜線去轉(zhuǎn)譯。
在模板字符串當中可以支持多行字符串。
const str = `123456`
模板字符串當中還支持通過插值表達式的方式在字符串中去嵌入所對應的數(shù)值,在字符串中可以使用${name}就可以在字符串當中嵌入name變量中的值。
const name = 'yd';const age = 18;
const str = `my name is ${name}, I am ${age} years old`;
那種方式會比之前字符串拼接方式要方便的多頁更直觀一點,不容易寫錯,事實上${}里面的內(nèi)容就是標準的JavaScript也就是說這里不僅僅可以嵌入變量,還可以嵌入任何標準的js語句。
7、 帶標簽的模板字符串
模板字符串還有一個更高級的用法,就是在定義模板字符串的時候可以在前面添加一個標簽,這個標簽實際上是一個特殊的函數(shù)。添加標簽會調(diào)用這個函數(shù)。
const name = 'yd';const age = 18;
const result = tag`My name is ${name}, I am ${age} years old`;
函數(shù)可以接收到一個數(shù)組參數(shù),是模板字符串內(nèi)容分割過后的結(jié)果。
const tag = (params) => { consoel.log(params); // ['My name is ', ' I am ', ' years old'];}
除了這個數(shù)組以外,還可以接收到所有在這個模板字符串中出現(xiàn)的表達式的返回值。
const tag = (params, name, age) => { consoel.log(params, name, age); // ['My name is ', ' I am ', ' years old']; 'yd' 18}
const str = tag`hello ${'world'}`;
8、字符串擴展方法
字符串對象存在幾個非常常用的方法。分別是includes,startsWith和endsWith。
1). startWith
如果想要知道這個字符串是否以Error開頭。
console.log(message.startsWith('Error')); // true
2). endsWith
同理如果想要知道這個字符串是否以.結(jié)尾。
console.log(message.endsWith('.')); // true
3). includes
如果需要明確的是字符串中間是否包含某個內(nèi)容。
console.log(message.includes('foo')); // true
9、 函數(shù)參數(shù)默認值
以前想要為函數(shù)中的參數(shù)去定義默認值需要在函數(shù)體中通過邏輯代碼來實現(xiàn)。
function foo (enable) { enable = enable === undefined ? true : enable; console.log(enable); // false}
foo(false);
有了參數(shù)默認值這個新功能以后,可以直接在形參的后面直接通過等號去設置一個默認值。
function foo (enable = true) { console.log(enable); // false}
foo(false);
如果有多個參數(shù)的話,帶有默認值的這種形參一定要出現(xiàn)在參數(shù)列表的最后。
function foo (bar, enable = true) { console.log(enable); // false}
foo(false);
10、剩余參數(shù)
對于未知個數(shù)的參數(shù),以前都是使用arguments對象去獲取,arguments對象實際上是一個偽數(shù)組,在ES2015當中新增了一個...操作符,也就是剩余操作符,可以在函數(shù)的形參前面加上..., 此時這個形參args就會以數(shù)組的形式去接收從當前這個參數(shù)的位置開始往后所有的實參。
// function foo() {// console.log(arguments); // 參數(shù)集合// }
function foo (...args) => { console.log(args); // 參數(shù)集合}
foo(1, 2, 3, 4);
因為接收的是所有的參數(shù),所以這種操作符只能出現(xiàn)在形參列表的最后一位,并且只可以使用一次。
11、展開數(shù)組
...操作符除了可以收起剩余數(shù)據(jù)這還有一種spread的用法,意思就是展開。
例如這里有一個數(shù)組,想要把數(shù)組當中的每一個成員按照次序傳遞給console.log方法,最原始的辦法是通過下標一個一個去找到數(shù)組當中的每一個元素,分別傳入到console.log方法當中。
在ES2015當中就沒有必要這么麻煩了,可以直接去調(diào)用console的log方法,然后通過...的操作符展開這里的數(shù)組。...操作符會把數(shù)組當中的每一個成員按照次序傳遞到列表當中。
console.log( ...arr );
12、 箭頭函數(shù)
在ECMAScript當中簡化了函數(shù)表達式的定義方式允許使用=>這種類似箭頭的符號來去定義函數(shù),那這種函數(shù)一來簡化了函數(shù)的定義,二來多了一些特性具體來看。
傳統(tǒng)來定義一個函數(shù)需要使用function關(guān)鍵詞,現(xiàn)在可以使用ES2015來去定義一個完全相同的函數(shù)。
function inc (number) { return number + 1;}
const inc = n => n + 1;
此時你會發(fā)現(xiàn),相比于普通的函數(shù),剪頭函數(shù)確實大大簡化了所定義函數(shù)這樣一些相關(guān)的代碼。
剪頭函數(shù)的左邊是參數(shù)列表,如果有多個參數(shù)的話可以使用()包裹起來,剪頭的右邊是函數(shù)體。
如果在這個函數(shù)的函數(shù)體內(nèi)需要執(zhí)行多條語句,同樣可以使用{}去包裹。如果只有一句代碼可以省略{}。
const inc = (n , m) => { return n + 1;};
對比普通函數(shù)和剪頭函數(shù)的寫法你會發(fā)現(xiàn),使用剪頭函數(shù)會讓代碼更簡短,而且更易讀。
1. this
相比普通函數(shù),箭頭函數(shù)有一個很重要的變化就是不會改變this的指向。
定義一個person對象,然后在這個對象當中去定義一個name屬性,然后再去定義一個sayHi的方法,這個方法中可以使用this去獲取當前對象。
const person = { name: 'yd', sayHi: function() { console.log(this.name); }}
person.sayHi(); // yd
這里把sayHi改為箭頭函數(shù)的方式。這個時候打印出來的name就是undefined
const person = { name: 'yd', sayHi: () => { console.log(this.name); }}
person.sayHi(); // undefined
這就是箭頭函數(shù)和普通函數(shù)最重要的區(qū)別,在剪頭函數(shù)當中沒有this的機制。所以說不會改變this的指向。
也就是說在剪頭函數(shù)的外面this是什么,在里面拿到的就是什么,任何情況下都不會發(fā)生改變。
13. 對象字面量的增強
傳統(tǒng)的字面量要求必須在{}里面使用屬性名: 屬性值這種語法。即便說屬性的值是一個變量,那也必須是屬性名: 變量名, 而現(xiàn)在如果變量名與添加到對象中的屬性名是一樣的,可以省略掉:變量名。
const bar = '123';const obj = { key: 'value', bar}
除此之外如果需要為對象添加一個普通的方法,現(xiàn)在可以省略里面的:function。
const obj = { method1 () { console.log('method1'); }}
console.log(obj)
需要注意的是這種方法的背后他實際上就是普通的function,也就是說如果通過對象去調(diào)用這個方法,那么內(nèi)部的this就會指向當前對象。
14. 動態(tài)屬性名
另外對象字面量還有一個很重要的變化就是,他可以使用表達式的返回值作為對象的屬性名。以前如果說要為對象添加一個動態(tài)的屬性名,只能在對象創(chuàng)建過后,然后通過索引器的方式也就是[]來去動態(tài)添加。
const obj = {};
obj[Math.random()] = 123;
在ES2015過后,對象字面量的屬性名直接可以通過[]直接去使用動態(tài)的值了,這樣一個特性叫做計算屬性名,具體的用法就是在屬性名的位置用[]包起來。
在里面就可以使用任意的表達式了。這個表達式的執(zhí)行結(jié)果將會作為這個對象的屬性名。
const obj = { [Math.random()]: 123,}
15. Object.assign
這個方法可以將多個源對象當中的屬性復制到一個目標對象當中,如果對象當中有相同的屬性,那么源對象當中的屬性就會覆蓋掉目標對象的屬性。
這里所說的源對象和目標對象他們都是普通的對象,只不過用處不同,是從源對象當中取,然后往目標對象當中放。
例如這里先定義一個source1對象,在這個對象當中定義一個a屬性和一個b屬性。然后再來定義一個target對象,這個對象當中也定義一個a屬性,還有一個c屬性。
const source1 = { a: 123, b: 123,}
const target = { a: 456, c: 456}
Object.assign支持傳入任意個數(shù)的參數(shù),其中第一個參數(shù)就是目標對象,也就是說所有源對象當中的屬性都會復制到目標對象當中。這個方法的返回值也就是這個目標對象。
const result = Object.assign(target, source1);
console.log(target, result === target); {a: 123, c: 456, b: 123 }// true
Object.assign用來為options對象參數(shù)設置默認值也是一個非常常見的應用場景。
const default = { name: 'yd', age: 18}
const options = Object.assign(default, opt);
16. Object.is
is方法用來判斷兩個值是否相等。在此之前ECMAScript當中去判斷兩個值是否相等可以使用==運算符。或者是===嚴格相等運算符。
這兩者是區(qū)別是==會在比較之前自動轉(zhuǎn)換數(shù)據(jù)類型,那也就會導致0 == false這種情況是成立的。
而===就是嚴格去對比兩者之間的數(shù)值是否相同。因為0和false他們之間的類型不同所以說他們是不會嚴格相等的。
但是嚴格相等運算符他也有兩個特殊情況,首先就是對于數(shù)字0,他的正負是沒有辦法區(qū)分的。
其次對于NaN, 兩個NaN在===比較時是不相等的。以前認為NaN是一個非數(shù)字,也就是說他有無限種可能,所以兩個NaN他是不相等的,但在今天看來,NaN他實際上就是一個特別的值,所以說兩個NaN他應該是完全相等的。
所以在ES2015中就提出了一種新的同值比較的算法來解決這個問題,通過Obejct.is正負零就可以被區(qū)分開,而且NaN也是等于NaN的。
Object.is(+0, -0); // falseObject.is(NaN, NaN); // true
不過一般情況下根本不會用到這個方法,大多時候還是使用嚴格相等運算符,也就是===。
17. Proxy
專門為對象設置訪問代理器的,那如果你不理解什么是代理可以想象成門衛(wèi),也就是說不管你進去那東西還是往里放東西都必須要經(jīng)過這樣一個代理。
通過Proxy就可以輕松監(jiān)視到對象的讀寫過程,相比于defineProperty,Proxy的功能要更為強大甚至使用起來也更為方便。
通過new Proxy的方式創(chuàng)建代理對象。
Proxy構(gòu)造函數(shù)的第一個參數(shù)就是需要代理的對象,第二個參數(shù)是代理對象處理對象,這可以通過get方法來去監(jiān)視屬性的訪問,通過set方法來截取對象當中設置屬性的過程。
const person = { name: 'yd', age: 18}
const personProxy = new Proxy(person, { get() {}, set() {}})
get方法可以接收兩個參數(shù),第一個就是所代理的目標對象,第二個就是外部所訪問的這個屬性的屬性名。。
{ get(target, property) { console.log(target, property); return property in target ? target[property] : undefined; }}
set方法接收三個參數(shù), 分別是代理目標對象,以及要寫入的屬性名和屬性值。
{ set(target, property, value) { console.log(target, property, value); if (property === 'age') { if (!Number.isInteger(value)) { throw new TypeError(``${value} must be a integer); } } target[property] = value; }}
1. Proxy 對比 defineProperty
相比于Object.defineProperty,Proxy到底有哪些優(yōu)勢。
Object.defineProperty只能監(jiān)聽到對象屬性的讀取或?qū)懭耄琍roxy除讀寫外還可以監(jiān)聽對象中屬性的刪除,對對象當中方法調(diào)用等。
const person = { name: 'yd', age: 18}const personProxy = new Proxy(person, { deleteProperty(target, property) { console.log(target, property); delete target[property]; },})
除了delete以外, 還有很多其他的對象操作都能夠被監(jiān)視到,列舉如下。
get: 讀取某個屬性
set: 寫入某個屬性
has:in操作符調(diào)用
deleteProperty:delete操作符調(diào)用
getProperty:Object.getPropertypeOf()setProperty:Object.setProtoTypeOf()isExtensible:Object.isExtensible()preventExtensions:Object.preventExtensions()getOwnPropertyDescriptor:Object.getOwnPropertyDescriptor()defineProperty:Object.defineProperty()ownKeys:Object.keys(),Object.getOwnPropertyNames(),Object.getOwnPropertSymbols()
apply: 調(diào)用一個函數(shù)
construct: 用new調(diào)用一個函數(shù)。
第二點是對于數(shù)組對象進行監(jiān)視更容易。
通常想要監(jiān)視數(shù)組的變化,基本要依靠重寫數(shù)組方法,這也是Vue的實現(xiàn)方式,Proxy可以直接監(jiān)視數(shù)組的變化。
const list = [];const listProxy = new Proxy(list, { set(target, property, value) { console.log(target, property, value); target[property] = value; return true; // 寫入成功 }});
listProxy.push(100);
Proxy內(nèi)部會自動根據(jù)push操作推斷出來他所處的下標,每次添加或者設置都會定位到對應的下標property。數(shù)組其他的也謝操作方式都是類似的。
最后Proxy是以非入侵的方式監(jiān)管了對象的讀寫,那也就是說一個已經(jīng)定義好的對象不需要對對象本身去做任何的操作,就可以監(jiān)視到他內(nèi)部成員的讀寫,而defineProperty的方式就要求必須按特定的方式單獨去定義對象當中那些被監(jiān)視的屬性。
對于一個已經(jīng)存在的對象要想去監(jiān)視他的屬性需要做很多額外的操作。這個優(yōu)勢實際上需要有大量的使用然后在這個過程當中去慢慢的體會。
18. Reflect
如果按照java或者c#這類語言的說法,Reflect屬于一個靜態(tài)類,也就是說他不能通過new的方式去構(gòu)建一個實例對象。只能夠去調(diào)用這個靜態(tài)類中的靜態(tài)方法。
這一點應該并不陌生,因為在javascript中的Math對象也是相同的,Reflect內(nèi)部封裝了一系列對對象的底層操作,具體一共提供了14個靜態(tài)方法,其中有1個已經(jīng)被廢棄掉了,那還剩下13個,仔細去查看Reflect的文檔會發(fā)現(xiàn)這13個方法的方法名與Proxy的處理對象里面的方法成員是完全一致的。
其實這些方法就是Proxy處理對象那些方法內(nèi)部的默認實現(xiàn).
const obj = { foo: '123', bar: '456',}
const Proxy = new Proxy(obj, { get(target, property) { console.log('實現(xiàn)監(jiān)視邏輯'); return Reflect.get(target, property); }})
這也就表明在實現(xiàn)自定義的get或者set這樣的邏輯時更標準的做法是先去實現(xiàn)自己所需要的監(jiān)視邏輯,最后再去返回通過Reflect中對應的方法的一個調(diào)用結(jié)果。
個人認為Reflect對象最大的意義就是他提供了一套統(tǒng)一操作Object的API,因為在這之前去操作對象時有可能使用Object對象上的方法,也有可能使用像delete或者是in這樣的操作符,這些對于新手來說實在是太亂了,并沒有什么規(guī)律。
Reflect對象就很好的解決了這樣一個問題,他統(tǒng)一了對象的操作方式。
19. Promise
Promise提供了一種全新的異步編程解決方案,通過鏈式調(diào)用的方式解決了在傳統(tǒng)異步編程過程中回調(diào)函數(shù)嵌套過深的問題。
關(guān)于Promise的細節(jié)有很多內(nèi)容,所以說這里先不做詳細介紹在JavaScript異步編程的文章中已經(jīng)專門針對Promise進行了詳細的分析。
20. class類
以前ECMAScript中都是通過定義函數(shù)以及函數(shù)的原型對象來去實現(xiàn)的類型,在構(gòu)造函數(shù)中可以通過this去訪問當前的實例對象,如果需要在這個類型所有的實例間去共享一些成員,可以借助于函數(shù)對象的prototype, 也就是原型去實現(xiàn)。
function Person (name) { this.name = name;}
Person.prototype.say = function() { console.log(this.name);}
ECMAScript2015可以使用一個叫做class的關(guān)鍵詞來聲明類型,這種獨立定義類型的語法,要更容易理解,結(jié)構(gòu)也會更加清晰。
class Person {
}
這種語法與一些老牌面向?qū)ο笳Z言當中class非常相似的。如果需要在構(gòu)造函數(shù)當中做一些額外的邏輯,可以添加一個constructor方法,這個方法就是構(gòu)造函數(shù)。
同樣可以在這個函數(shù)中使用this去訪問當前類型的實例對象。
class Person { constructor (name) { this.name = name; } say() { console.log(this.name); }}
1. 靜態(tài)方法
類型中的方法分為實例方法和靜態(tài)方法,實例方法就是需要通過這個類型構(gòu)造的實例對象去調(diào)用,靜態(tài)方法是直接通過類型本身去調(diào)用。可以通過static關(guān)鍵字定義。
class Person { constructor (name) { this.name = name; } say() { console.log(this.name); } static create (name) { return new Person(name); }}
調(diào)用靜態(tài)方法是直接通過類型然后通過成員操作符調(diào)用方法名字。
const yd = Person.create('yd');
注意:因為靜態(tài)方法是掛載到類型上面的,所以說在靜態(tài)方法內(nèi)部他不會指向某一個實例對象,而是當前的類型。
2. 類的繼承
繼承是面向?qū)ο螽斨幸粋€非常重要的特性,通過繼承這種特性能抽象出來相似類型之間重復的地方, 可以通過關(guān)鍵詞extends實現(xiàn)繼承。
super對象指向父類, 調(diào)用它就是調(diào)用了父類的構(gòu)造函數(shù)。
class Student extends Person { constructor(name, number) { super(name); this.number = number; } hello () { super.say(); console.log(this.number); }}
const s = new Student('yd', '100');
s.hello();
21. Set
可以把他理解為集合,他與傳統(tǒng)的數(shù)組非常類似,不過Set內(nèi)部的成員是不允許重復的。那也就是說每一個值在同一個Set中都是唯一的。
通過這個類型構(gòu)造的實例就用來存放不同的數(shù)據(jù)。可以通過這個實例的add方法向集合當中去添加數(shù)據(jù),由于add方法他會返回集合對象本身,所以可以鏈式調(diào)用。如果在這個過程中添加了之前已經(jīng)存在的值那所添加的這個值就會被忽略掉。
const s = new Set();
s.add(1).add(2).add(3).add(2);
想要遍歷集合當中的數(shù)據(jù),可以使用集合對象的forEach方法去傳遞一個回調(diào)函數(shù)。
s.forEach(i => console.log(i));
也可以使用for...of循環(huán)。
for (let i of s) { console.log(i);}
可以通過size屬性來去獲取整個集合的長度。
console.log(s.size);
has方法就用來判斷集合當中是否存在某一個特定的值。
console.log(s.has(100)); // false
delete方法用來刪除集合當中指定的值,刪除成功將會返回一個true。
console.log(s.delete(3)); // true
clear方法用于清除當前集合當中的全部內(nèi)容。
s.clear()
22. Map
Map結(jié)構(gòu)與對象非常類似,本質(zhì)上他們都是鍵值對集合但是這種對象結(jié)構(gòu)中的鍵,只能是字符串類型,如果說用其他類型作為鍵會被轉(zhuǎn)換成字符串,出現(xiàn)[object object]。
不同的對象轉(zhuǎn)換成字符串可能會變成相同的鍵名[object object]導致數(shù)據(jù)覆蓋丟失。
Map類型才算是嚴格上的鍵值對類型,用來映射兩個任意類型之間鍵值對的關(guān)系。可以使用這個對象的set方法去存數(shù)據(jù)。鍵可以是任意類型的數(shù)據(jù)。不需要擔心他會被轉(zhuǎn)換為字符串。
const m = new Map();
const key = {};
m.set(key, 18);
console.log(m);
可以使用get方法獲取數(shù)據(jù),has方法判斷他里面是否存在某個鍵。delete方法刪除某個鍵。clear方法清空所有的鍵值。
console.log(m.get(key));
console.log(m.has(key));
m.delete(key);
m.clear();
可以使用forEach方法遍歷。在這個方法的回調(diào)函數(shù)當中第一個參數(shù)就是被遍歷的值,第二個參數(shù)是被遍歷的鍵。
m.forEach((value, key) => { console.log(value, key);})
23. Symbol
在ECMAScript2015之前對象的屬性名都是字符串,而字符串是有可能會重復的。如果重復的話就會產(chǎn)生沖突,比如在使用第三方模塊時,如果需要擴展第三方模塊,而這時就有可能把第三方模塊的方法覆蓋掉,導致代碼執(zhí)行異常。
ES2015提供了一種全新的原始數(shù)據(jù)類型Symbol,翻譯過來的意思叫做符號,翻譯過來就是表示一個獨一無二的值。
通過Symbol函數(shù)就可以創(chuàng)建一個Symbol類型的數(shù)據(jù),而且這種類型的數(shù)據(jù)typeof的結(jié)果也是symbol,那這也就表示他確實是一個全新的類型。
const s = Symbol();typeof s; // symbol類型
這種類型最大的特點就是獨一無二,通過Symbol函數(shù)創(chuàng)建的每一個值都是唯一的永遠不會重復。
Symbol() === Symbol(); // false
Symbol創(chuàng)建時允許接收一個字符串,作為這個值的描述文本, 對于多次使用Symbol時就可以區(qū)分出是哪一個Symbol,這個參數(shù)僅是描述作用,相同的描述字段生成的值仍是不同的。
const s1 = Symbol('foo');const s2 = Symbol('foo');
s1 === s2; // false
ES2015開始對象允許使用Symbol作為屬性名。那也就是說現(xiàn)在對象的屬性名可以是兩種類型,string和Symbol。
const person = { [Symbol()]: 123, [Symbol()]: 456}
Symbol除了用在對象中避免重復以外,還可以借助這種類型的特點來模擬實現(xiàn)對象的私有成員。以前私有成員都是通過約定,例如約定使用下劃線開頭就表示是私有成員。約定外界不允許訪問下劃線開頭的成員。
現(xiàn)在有了Symbol就可以使用Symbol去作為私有成員的屬性名了。在這個對象的內(nèi)部可以使用創(chuàng)建屬性時的Symbol。去拿到對應的屬性成員。
const name = Symbol();const person = { [name]: 'yd', say() { return this[name]; }}
截止到2020標準,ECMAScript一共定義了8種數(shù)據(jù)類型。
如果需要在全局去復用一個相同的Symbol值,可以使用全局變量的方式去實現(xiàn),或者是使用Symbol類型提供的一個靜態(tài)方法for,這個方法接收一個字符串作為參數(shù),相同的參數(shù)一定對應相同的值。
const s1 = Symbol.for('foo');const s2 = Symbol.for('foo');
s1 === s2; // true
這個方法維護了一個全局的注冊表,為字符串和Symbol提供了一個對應關(guān)系。需要注意的是,在內(nèi)部維護的是字符串和Symbol的關(guān)系,那也就是說如參數(shù)不是字符串,會轉(zhuǎn)換為字符串。
const s1 = Symbol.for('true');const s2 = Symbol.for(true);
s1 === s2; // true
1. 常量
在Symbol內(nèi)部提供了很多內(nèi)置的Symbol常量,用來去作為內(nèi)部方法的標識,可以讓自定義對象去實現(xiàn)一些js內(nèi)置的接口。
如果想要自定義對象的toString標簽,ECMAScript要求使用Symbol的值來去實現(xiàn)這樣一個接口。
obj[Symbol.toStringTag] = 'test'obj.toString(); // [object test];
這里的toStringTag就是內(nèi)置的一個Symbol常量,這種Symbol在后面為對象去實現(xiàn)迭代器時會經(jīng)常遇到。
使用Symbol的值去作為對象的屬性名那這個屬性通過傳統(tǒng)的for in循環(huán)是無法拿到的。
而且通過Object.keys方法也是獲取不到這樣Symbol類型的屬性名。JSON.stringify去序列化,Symbol屬性也會被隱藏掉。
const obj = { [Symbol()]: 'symbol value', foo: 'normal value'}
for (var key in obj) { console.log(key);}
Object.keys(obj);
總之這些特性都使得Symbol屬性,特別適合作為對象的私有屬性,當然想要獲取這種類型的屬性名可以使用Object.getOwnPropertySymbols(obj)方法。
Object.getOwnPropertySymbols(obj)
這個方法的作用類似于Object.keys, 所不同的是Object.keys他只能獲取對象當中字符串屬性名,而Object.getOwnPropertySymbols方法他獲取到的全是Symbol類型的屬性名。
24. for…of
for...of是ECMAScript2015之后新增的遍歷方式。未來會作為遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一方式。
const arr = [1, 2, 3, 4];for (const item of arr) { console.log(item); // 1, 2,3,4 // break; // 終止循環(huán)}
for…of循環(huán)拿到的就是數(shù)組中的每一個元素,而不是對應的下標。這種循環(huán)方式就可以取代之前常用的數(shù)組實例當中的forEach方法。
for...of循環(huán)可以使用break關(guān)鍵詞隨時去終止循環(huán)。
除了數(shù)組可以直接被for...of循環(huán)去遍歷,一些偽數(shù)組對象也是可以直接被for...of去遍歷的,例如arguments,set,map。
for...of在遍歷Map時, 可以直接拿到鍵和值。鍵和值是直接以數(shù)組的形式返回的,也就是說數(shù)組的第一個元素就是當前的鍵名,第二個元素就是值。這就可以配合數(shù)組的解構(gòu)語法,直接拿到鍵和值。
const m = new Map();m.set('foo', '123');m.set('bar', '345');
for (const item if m) { console.log(item); // ['foo', '123'];}
for (const [key, value] if m) { console.log(key, value); // 'foo', '123'}
for...of是不能直接遍歷普通對象的,他要求被遍歷的對象必須存在一個叫做Iterable的接口。
1. 可迭代接口
可迭代接口就是一種可以被for...of循環(huán)統(tǒng)一遍歷訪問的規(guī)格標準,換句話說只要這個數(shù)據(jù)結(jié)構(gòu)實現(xiàn)了可迭代接口他就能夠被for...of循環(huán)遍歷,那這也就是說之前嘗試的那些能夠直接被for...of循環(huán)去遍歷的數(shù)據(jù)類型他都已經(jīng)在內(nèi)部實現(xiàn)了這個接口。
iterable約定對象中必須要掛載一個叫做Symbol.iterable的方法,這個方法返回一個對象,對象上存在一個next方法,next方法也返回一個對象,對象中存在value和done兩個屬性,value是當前遍歷到的值,done為是否為最后一個。
每調(diào)用一次next就會后移一位。
總結(jié)起來就是所有被for...of遍歷的數(shù)據(jù)類型必須包含一個叫做iterable的接口,也就是內(nèi)部必須掛載一個Symbol.iterable方法,這個方法需要返回一個帶有next方法的對象,不斷調(diào)用這個next方法就可以實現(xiàn)對內(nèi)部所有成員的遍歷。
這就是for...of循環(huán)的內(nèi)部原理。
2. 實現(xiàn)可迭代接口
在這個對象中放一個數(shù)組store用來存放值得被遍歷的數(shù)據(jù),然后在next方法中去迭代這個數(shù)組,需要去維護一個下標index,讓他默認等于0;
由于next中的函數(shù)并不是obj對象,所以使用self去存儲一下當前的this供下面使用,在next方法中value就是self.store[index],done就是index >= self.store.length。完成以后需要讓index++, 也就是讓指針后移一位。
const obj = { store: [1, 2, 3, 4, 5], [Symbol.iterable]: function() { let index = 0; const self = this; return { next: function() { const result = { value: self.store[index], done: index >= self.store.length } index++; return result; } } }}
for (const item of obj) { console.log(item); // 1, 2, 3, 4, 5}
25. 生成器
ECMAScript2015中還新增了一種生成器函數(shù)generator,是為了能夠在復雜的異步編程中減少回調(diào)函數(shù)嵌套產(chǎn)生的問題,從而去提供更好的異步編程解決方案。
定義生成器函數(shù)就是在普通的函數(shù)function后面添加一個*,這樣函數(shù)就變成了一個生成器函數(shù)。函數(shù)執(zhí)行之后會返回一個生成器對象。
function * foo() { return 100;}const result = foo();console.log(result);
在這個對象上也和迭代器一樣有一個next方法,實際上生成器函數(shù)也實現(xiàn)了iterable接口,也就是迭代器接口協(xié)議。
生成器函數(shù)在使用會配合一個叫做yield的關(guān)鍵字,yield關(guān)鍵詞與return關(guān)鍵詞類似,但是又有很大的不同。
生成器函數(shù)會自動返回一個生成器對象,調(diào)用這個生成器的next會讓這個函數(shù)的函數(shù)體開始執(zhí)行,執(zhí)行過程中一旦遇到y(tǒng)ield關(guān)鍵詞函數(shù)的執(zhí)行就會被暫停下來,而且yield的值將會被作為next的結(jié)果返回,繼續(xù)調(diào)用next函數(shù)就會從暫停的位置繼續(xù)向下執(zhí)行到下一個yield直到這個函數(shù)完全結(jié)束。
const * foo() { console.log(1111); yield 100; console.log(2222); yield 200; console.log(3333); yield 300;}
生成器函數(shù)最大的特點就是惰性執(zhí)行,每調(diào)用一次next就會執(zhí)行一次yield。
1. 生成器應用
了解了生成器函數(shù)的基本用法來看一個簡單的應用場景實現(xiàn)一個發(fā)號器。
在實際業(yè)務開發(fā)過程中經(jīng)常需要用到自增的id,而且每次調(diào)用這個id都需要在原有的基礎上去+1,這里如果使用生成器函數(shù)去實現(xiàn)這樣一個功能是最合適的了。
首先定義一個createId生成器函數(shù),然后定義一個初始的id等于1,然后通過一個死循環(huán)不斷的去yield id++。這里不需要擔心死循環(huán)的問題,因為每次在yield過后這個方法會被暫停,循環(huán)自然也就會被暫停。直到下一次調(diào)用next再次去執(zhí)行一次又會被暫停下來。
這樣在外部就可以通過這個方法去創(chuàng)建一個生成器對象id,每次調(diào)用一下這個生成器的next方法就能夠獲取到自增的value,也就是id。
function * createId() { let id = 1; while(true) { yield id++; }}const id = createId();
id.next().value;
實現(xiàn)發(fā)號器是一個非常簡單的需求,還可以使用生成器函數(shù)實現(xiàn)對象的iterator方法,因為生成器也實現(xiàn)了對象的iterator接口,而且使用生成器函數(shù)去實現(xiàn)iterator方法會比之前的方式簡單很多。
26. ES Modules
ES Modules是ECMAScript2015中標準化的一套語言層面的模塊化標準規(guī)范,我之前寫過一篇模塊化發(fā)展歷程的文章,里面有詳細的介紹,里面和CommonJs以及其他標準做了統(tǒng)一的對比,感興趣的可以翻閱一下那篇文章。
27. include方法
這個方法檢查數(shù)組中是否存在某個元素。在這之前如果需要檢查數(shù)組中是否包含某個元素都是使用indexOf方法。
但是indexOf不能查詢到數(shù)組中的NaN,現(xiàn)在有了includes方法之后直接可以判斷數(shù)組當中是否存在某個指定的元素了,并且他返回的是一個布爾值,而且也可以判斷NaN
28. **指數(shù)運算符
以前需要進行指數(shù)運算需要借助Math對象的pow方法來去實現(xiàn)。例如去求2的10次方。
Math.pow(2, 10); // 表示2的10次方。
指數(shù)運算符,他就是語言本身的運算符,就像是之前所使用的加減乘除運算符一樣,使用起來也非常簡單。
2**10; // 2的10次方
29. Object對象新增三個擴展方法
Object.keys返回的是所有的鍵組成的數(shù)組,Object.values返回的是所有值組成的數(shù)組。
Object.entries將對象轉(zhuǎn)成數(shù)組,每個元素是鍵值對的數(shù)組,可以快速將對象轉(zhuǎn)為Map
const l = Object.entries({a: 1, b: 2});const m = new Map(l);
30. Object.getOwnPropertyDescriptors
獲取對象的描述信息
Object.assign復制時,將對象的屬性和方法當做普通屬性來復制,并不會復制完整的描述信息,比如this等.
const p1 = { a: 'y', b: 'd', get name() { return `${this.a} ${this.b}`; }}
const p2 = Object.assign({}, p1);p2.a = 'z';p2.name; // y d; 發(fā)現(xiàn)并沒有修改到a的值,是因為this仍舊指向p1
使用Object.getOwnPropertyDescriptors獲取完整描述信息
const description = Object.getOwnPropertyDescriptors(p1);const p2 = Object.defineProperty({}, description);p2.a = 'z';p2.name; // z d
31. String.prototype.String.prototype.padStart
用給定的字符串在尾部拼接到指定長度
'abc'.padEnd(5, '1'); // abc11;
用給定的字符串在首部拼接到指定長度
'abc'.padStart(5, '1'); // 11abc;
32. 允許對象和數(shù)組在最后添加一個逗號
[1, 2, 3,]
{a: 1, b: 2, }
33. async + await
在函數(shù)聲明時加入async關(guān)鍵字,則函數(shù)會變?yōu)楫惒胶瘮?shù),當使用await調(diào)用時,只有等到被await的promise返回,函數(shù)才會向下執(zhí)行。
const as = async () => { const data = await ajax();}
34. 收集剩余屬性
將對象或者數(shù)組的剩余屬性收集到新對象中
const data = {a: 1, b: 2, c: 3, d: 4};const {a, b, ...arg} = data;console.log(arg); // {c: 3, d: 4};
事實上 Map、Set、String 同樣支持該能力。
35. for of 支持異步迭代
在此之前想要實現(xiàn)異步迭代想要在for of外層嵌套一個async函數(shù)
async function () { for (const fn of actions) { await fn(); }}
ES2018提供了一種新的書寫方式。
async function() { for await (const fn of actions) { fn(); }}
36. JSON 成為 ECMAScript 的完全子集
在以前,行分隔符\u2028和段分隔符\u2029會導致JSON.parse拋出語法錯誤異常。
ECMAScript優(yōu)化了這個功能。
JSON.stringify也做了改進,對于超出Unicode范圍的轉(zhuǎn)義序列,JSON.stringify()會輸出未知字符:
JSON.stringify('\uDEAD'); // '"?"'
37. 修正Function.prototpye.toString()
在以前,返回的內(nèi)容中function關(guān)鍵字和函數(shù)名之間的注釋,以及函數(shù)名和參數(shù)列表左括號之間的空格,是不會被顯示出來的。現(xiàn)在會精確返回這些內(nèi)容,函數(shù)如何定義的,這就會如何顯示。
38. Array.prorptype.flat()、Array.prorptype.flatMap()
flat()用于對數(shù)組進行降維,它可以接收一個參數(shù),用于指定降多少維,默認為1。降維最多降到一維。
const array = [1, [2, [3]]]array.flat() // [1, 2, [3]]array.flat(1) // [1, 2, [3]],默認降 1 維array.flat(2) // [1, 2, 3]array.flat(3) // [1, 2, 3],最多降到一維
flatMap()允許在對數(shù)組進行降維之前,先進行一輪映射,用法和map()一樣。然后再將映射的結(jié)果降低一個維度。可以說arr.flatMap(fn)等效于arr.map(fn).flat(1)。但是根據(jù)MDN的說法,flatMap()在效率上略勝一籌,誰知道呢。
flatMap()也可以等效為reduce()和concat()的組合,下面這個案例來自MDN,但是這不是一個map就能搞定的事么?
var arr1 = [1, 2, 3, 4];
arr1.flatMap(x => [x * 2]);// 等價于arr1.reduce((acc, x) => acc.concat([x * 2]), []);// [2, 4, 6, 8]
39. String.prototype.trimStart()、String.prototype.trimEnd()
用過字符串trim()的都知道這兩個函數(shù)各自負責只去掉單邊的多余空格。
40. Object.fromEntries()
從名字就能看出來,這是Object.entries()的逆過程。Object.fromEntries()可以將數(shù)組轉(zhuǎn)化為對象。
41. description of Symbol
Symbol是新的原始類型,通常在創(chuàng)建Symbol時會附加一段描述。只有把這個Symbol轉(zhuǎn)成String才能看到這段描述,而且外層還套了個 'Symbol()' 字樣。ES2019為Symbol新增了description屬性,專門用于查看這段描述。
const sym = Symbol('The description');String(sym) // 'Symbol(The description)'sym.description // 'The description'
42. try…catch
過去,catch后面必須有一組括號,里面用一個變量代表錯誤信息對象。現(xiàn)在這部分是可選的了,如果異常處理部分不需要錯誤信息,可以把它省略,像寫if...else一樣寫try...catch。
try { throw new Error('Some Error')} catch { handleError() // 這里沒有用到錯誤信息,可以省略 catch 后面的 (e)。}
43. Array.prototype.sort
JavaScript中內(nèi)置的數(shù)組排序算法使用的是不穩(wěn)定的排序算法,也就是說在每一次執(zhí)行后,對于相同數(shù)據(jù)來說,它們的相對位置是不一致的。
var arr1 = [{a: 1, b: 2}, {a: 2, b: 2}, {a: 1, b: 3}, {a: 2, b: 4}, {a: 5, b: 3}];arr1.sort((a, b) => a.a - b.a);
返回的結(jié)果第一次可能是這樣的:[{a: 1, b: 2}, {a: 1, b: 3}...]
但是第二次就變成:[{a:1,b:3}, {a:1, b: 2}....]
那么在es2019中,JavaScript內(nèi)部放棄了不穩(wěn)定的快排算法,而選擇使用Tim Sort這種穩(wěn)定的排序算法。優(yōu)化了這個功能。
44. 類的私有屬性
你可能不希望類內(nèi)部的所有內(nèi)容都是全局可用的。通過在變量或函數(shù)前面添加一個#可以將它們完全保留為類內(nèi)部使用
class Message { greet() { console.log(this.#message) }}
const greeting = new Message()
greeting.greet() // Howdy 內(nèi)部可以訪問console.log(greeting.#message) // Private name #message is not defined 不能直接被訪問
45. Promise.allSettled
當處理多個Promise時,特別是當它們相互依賴時,記錄每個Promise所發(fā)生的事情來調(diào)試錯誤是很有必要的。
通過Promise.allSettled可以創(chuàng)建一個新的Promise,它只在所有傳遞給它的Promise都完成時返回一個數(shù)組,其中包含每個Promise的數(shù)據(jù)。
const p1 = new Promise((res, rej) => setTimeout(res, 1000));
const p2 = new Promise((res, rej) => setTimeout(rej, 1000));
Promise.allSettled([p1, p2]).then(data => console.log(data));
// [// Object { status: "fulfilled", value: undefined},// Object { status: "rejected", reason: undefined}// ]
Promise.all是當多個promise全部成功,或出現(xiàn)第一個失敗就會結(jié)束。Promise.allSettled是所有都執(zhí)行完成,無論成功失敗。
46. 合并空運算符 ??
假設變量不存在,希望給系統(tǒng)一個默認值,一般會使用||運算符。但是在javascript中空字符串,0,false都會執(zhí)行||運算符,ECMAScript2020引入合并空運算符解決該問題,只允許在值為null或undefined時使用默認值。
const name = '';
console.log(name || 'yd'); // yd;console.log(name ?? 'yd'); // '';
47. 可選鏈運算符 ?.
業(yè)務代碼中經(jīng)常會遇到這樣的情況,a對象有個屬性b,b也是一個對象有個屬性c。
const a = { b: { c: 123, }}
訪問c,經(jīng)常會寫成a.b.c,但是如果b不存在時,就會出錯。
ECMAScript2020定義可選鏈運算符解決該問題,在.之前添加一個?將鍵名變成可選。
let person = {};console.log(person?.profile?.age ?? 18); // 18
48. bigInt
JavaScript可以處理的最大數(shù)字是2的53次方 - 1,可以在可以在Number.MAX_SAFE_INTEGER中看到。
更大的數(shù)字則無法處理,ECMAScript2020引入BigInt數(shù)據(jù)類型來解決這個問題。通過把字母n放在末尾, 可以運算大數(shù)據(jù)。通過常規(guī)操作進行加、減、乘、除、余數(shù)和冪等運算。
它可以由數(shù)字和十六進制或二進制字符串構(gòu)造。此外它還支持AND、OR、NOT和XOR之類的按位運算。唯一無效的位運算是零填充右移運算符。
const bigNum = 100000000000000000000000000000n;console.log(bigNum * 2n); // 200000000000000000000000000000n
const bigInt = BigInt(1);console.log(bigInt); // 1n;
const bigInt2 = BigInt('2222222222222222222');console.log(bigInt2); // 2222222222222222222n;
BigInt是一個大整數(shù),不能存儲小數(shù)。
49. 動態(tài)導入import
import('./a.js')返回一個Promise對象。
const a = 123;export { a };
import('./a.js').then(data => { console.log(data.a); // 123;})
50. globalThis
標準化方式訪問全局對象,globalThis在瀏覽器中window作為全局對象,在node中g(shù)lobal作為全局對象,ECMAScript2020提供globalThis作為語言的全局對象,方便代碼移植到不同環(huán)境中運行。
51. String.prototype.replaceAll()
為了方便字符串的全局替換,ES2021將支持String.prototype.replaceAll()方法,可以不用寫正則表達式就可以完成字符串的全局替換
'abc111'.replaceAll('1', '2'); // abc222
52. Promise.any
只要有一個promise是fulfilled時,則返回一個resolved promise;所有的promise都是rejected時,則返回一個rejected promise
Promise.any([ Promise.reject(1), Promise.resolve(2) ]) .then(result => console.log('result:', result)) .catch(error => console.error('error:', error)); // result: 2
53. 邏輯賦值運算符
邏輯賦值運算符由邏輯運算符和賦值表達式組合而成:
a ||= b;// 與 a ||= b 等價a || (a = b);// 與 a ||= b 等價if (!a) { a = b;}
a &&= b;// 與 a &&= b 等價a && (a = b);// 與 a &&= b 等價if (a) { a = b;}
a ??= b;// 與 a ??= b 等價a ?? (a = b);// 與 a ??= b 等價if (a === null || a === undefined) { a = b;}
注意:
a = a || b; // 與 a ||= b 不等價a = a && b; // 與 a &&= b 不等價a = a ?? b; // 與 a ??= b 不等價
54. 數(shù)字分隔符
使用_對數(shù)字進行分割,提高數(shù)字的可讀性,例如在日常生活中數(shù)字通常是每三位數(shù)字之間會用`, 分割,以方便人快速識別數(shù)字。在代碼中,也需要程序員較便捷的對數(shù)字進行辨識。
// 1000000000 不易辨識const count1 = 1000000000;
// 1_000_000_000 很直觀const count2 = 1_000_000_000;
console.log(count1 === count2); // true
55. WeakRefs
WeakRef實例可以作為對象的弱引用,對象的弱引用是指當該對象應該被GC回收時不會阻止GC的回收行為。而與此相反的,一個普通的引用(默認是強引用)會將與之對應的對象保存在內(nèi)存中。
只有當該對象沒有任何的強引用時,JavaScript引擎GC才會銷毀該對象并且回收該對象所占的內(nèi)存空間。因此,訪問弱引用指向的對象時,很有可能會出現(xiàn)該對象已經(jīng)被回收。
const ref = new WeakRef({ name: 'koofe' });let obj = ref.deref();if (obj) { console.log(obj.name); // koofe}
對于WeakRef對象的使用要慎重考慮,能不使用就盡量不要使用。
學習更多技能
請點擊下方公眾號


瀏覽
70

來源 | 致前端 - https://www.zhiqianduan.com
1、ECMAScript 概述
2、 let 與塊級作用域
在ES2015之前,ECMAScript當中只有全局作用域和函數(shù)作用域兩種類型的作用域。在ES2015中又新增了一個塊級作用域。
塊指的就是代碼中用一對{}所包裹起來的范圍,例如if語句和for語句中的{}都會產(chǎn)生這里所說的塊。
if (true) {consoel.log('yd');}for (var i = 0; i < 10; i++) {console.log('yd');}
在ECMAScript2015以前,塊是沒有單獨的作用域的,這就導致在塊中定義的成員外部也可以訪問到。
例如在if當中去定義了一個foo的變量,然后在if的外面打印這個foo,結(jié)果也是可以正常打印出來的。
if (true) {var foo = 'yd';}console.log(foo); // yd
這一點對于復雜代碼是非常不利的,也是不安全的,有了塊級作用域之后,可以在代碼當中通過let這個新的關(guān)鍵詞聲明變量。
他的用法和var是一樣的,只不過通過let聲明的變量他只能在所聲明的這個代碼塊中被訪問到。
if (true) {let foo = 'yd';}console.log(foo); // foo is not defined
在快級內(nèi)定義的成員,外部是無法訪問的。
這樣一個特性非常適合聲明for循環(huán)當中的計數(shù)器。
for (let i = 0; i < 3; i++) {for (let i = 0; i < 3; i++) {console.log(i);}}
3、const
他可以用來去聲明一個只讀的恒量或者叫常量,他的特點就是在let的基礎上多了一個只讀特性。
所謂只讀指的就是變量一旦聲明過后就不能夠再被修改,如果在聲明過后再去修改這個成員就會出現(xiàn)錯誤。
const name = 'yd';name = 'zd'; // 錯誤
這里要注意的問題const所聲明的成員不能被修改,并不是說不允許修改恒量中的屬性成員。下面這種情況是允許的。
const obj = {}obj.name = 'yd';
4、 數(shù)組的解構(gòu)
ECMAScript2015新增了從數(shù)組或?qū)ο笾蝎@取指定元素的一種快捷方式,這是一種新的語法,這種新語法叫做解構(gòu)。
const arr = [100, 200, 300];const [foo, bar, baz] = arr;console.log(foo, bar, baz);
如果只是想獲取其中某個位置所對應的成員,例如只獲取第三個成員, 這里可以把前兩個成員都刪掉。但是需要保留對應的逗號。確保解構(gòu)位置的格式與數(shù)組是一致的。這樣的話就能夠提取到指定位置的成員。
const [, , baz] = arr;console.log(baz);
除此之外還可以在解構(gòu)位置的變量名之前添加...表示提取從當前位置開始往后的所有成員,最終所有的結(jié)果會放在一個數(shù)組當中。
const [foo, ...rest] = arr;console.log(rest);
三個點的用法只能在解構(gòu)位置的最后一個成員上使用,另外如果解構(gòu)位置的成員個數(shù)小于被解構(gòu)的數(shù)組長度,就會按照從前到后的順序去提取,多出來的成員就不會被提取。
反之如果解構(gòu)位置的成員大于數(shù)組長度,那么提取到的就是undefined。這和訪問數(shù)組當中一個不存在的下標是一樣的。
const [foo, bar, baz, more] = arr;console.log(more); // undefined
如果需要給提取到的成員設置默認值,這種語法也是支持的,只需要在解構(gòu)變量的后面跟上一個等號,然后后面寫上一個默認值。
const [foo, bar, baz, more = 'default value'] = arr;console.log(more);
5、 對象的解構(gòu)
對象也同樣可以被解構(gòu),不過對象的結(jié)構(gòu)需要去根據(jù)屬性名去匹配提取,而不是位置。
const obj = { name: 'yd', age: 18 };解構(gòu)他里面的成員就是在以前變量位置去使用一個對象字面量的{}, 然后在{}里同樣也是提取出來的數(shù)據(jù)所存放的變量名,不過這里的變量名還有一個很重要的作用就是去匹配被解構(gòu)對象中的成員,從而去提取指定成員的值。
const obj = { name: 'yd', age: 18 };const { name } = obj;
解構(gòu)對象的其他特點基本上和解構(gòu)數(shù)組是完全一致的。未匹配到的成員返回undefined,也可以設置默認值。
在對象當中有一個特殊的情況,解構(gòu)的變量名是被解構(gòu)對象的屬性名,所以說當前作用域中如果有這個名稱就會產(chǎn)生沖突。這個時候可以使用重命名的方式去解決這個問題。
const obj = { name: 'yd', age: 18 };const { name: name1 } = obj;console.log(name1);
解構(gòu)對象的應用場景比較多,不過大部分的場景都是為了簡化代碼,比如代碼中如果大量用到了console對象的方法,可以先把這個對象單獨解構(gòu)出來,然后再去使用獨立的log方法。
const { log } = console;log('1');
6、模板字符串
傳統(tǒng)定義字符串的方式需要通過'或者是"來標識。ES2015新增了模板字符串,使用反引號`聲明,。如果在字符串中需要使用`,可以使用斜線去轉(zhuǎn)譯。
在模板字符串當中可以支持多行字符串。
const str = `123456`
模板字符串當中還支持通過插值表達式的方式在字符串中去嵌入所對應的數(shù)值,在字符串中可以使用${name}就可以在字符串當中嵌入name變量中的值。
const name = 'yd';const age = 18;const str = `my name is ${name}, I am ${age} years old`;
那種方式會比之前字符串拼接方式要方便的多頁更直觀一點,不容易寫錯,事實上${}里面的內(nèi)容就是標準的JavaScript也就是說這里不僅僅可以嵌入變量,還可以嵌入任何標準的js語句。
7、 帶標簽的模板字符串
模板字符串還有一個更高級的用法,就是在定義模板字符串的時候可以在前面添加一個標簽,這個標簽實際上是一個特殊的函數(shù)。添加標簽會調(diào)用這個函數(shù)。
const name = 'yd';const age = 18;const result = tag`My name is ${name}, I am ${age} years old`;
函數(shù)可以接收到一個數(shù)組參數(shù),是模板字符串內(nèi)容分割過后的結(jié)果。
const tag = (params) => {consoel.log(params); // ['My name is ', ' I am ', ' years old'];}
除了這個數(shù)組以外,還可以接收到所有在這個模板字符串中出現(xiàn)的表達式的返回值。
const tag = (params, name, age) => {consoel.log(params, name, age); // ['My name is ', ' I am ', ' years old']; 'yd' 18}const str = tag`hello ${'world'}`;
8、字符串擴展方法
字符串對象存在幾個非常常用的方法。分別是includes,startsWith和endsWith。
1). startWith
如果想要知道這個字符串是否以Error開頭。
console.log(message.startsWith('Error')); // true2). endsWith
同理如果想要知道這個字符串是否以.結(jié)尾。
console.log(message.endsWith('.')); // true3). includes
如果需要明確的是字符串中間是否包含某個內(nèi)容。
console.log(message.includes('foo')); // true9、 函數(shù)參數(shù)默認值
以前想要為函數(shù)中的參數(shù)去定義默認值需要在函數(shù)體中通過邏輯代碼來實現(xiàn)。
function foo (enable) {enable = enable === undefined ? true : enable;console.log(enable); // false}foo(false);
有了參數(shù)默認值這個新功能以后,可以直接在形參的后面直接通過等號去設置一個默認值。
function foo (enable = true) {console.log(enable); // false}foo(false);
如果有多個參數(shù)的話,帶有默認值的這種形參一定要出現(xiàn)在參數(shù)列表的最后。
function foo (bar, enable = true) {console.log(enable); // false}foo(false);
10、剩余參數(shù)
對于未知個數(shù)的參數(shù),以前都是使用arguments對象去獲取,arguments對象實際上是一個偽數(shù)組,在ES2015當中新增了一個...操作符,也就是剩余操作符,可以在函數(shù)的形參前面加上..., 此時這個形參args就會以數(shù)組的形式去接收從當前這個參數(shù)的位置開始往后所有的實參。
// function foo() {// console.log(arguments); // 參數(shù)集合// }function foo (...args) => {console.log(args); // 參數(shù)集合}foo(1, 2, 3, 4);
因為接收的是所有的參數(shù),所以這種操作符只能出現(xiàn)在形參列表的最后一位,并且只可以使用一次。
11、展開數(shù)組
...操作符除了可以收起剩余數(shù)據(jù)這還有一種spread的用法,意思就是展開。
例如這里有一個數(shù)組,想要把數(shù)組當中的每一個成員按照次序傳遞給console.log方法,最原始的辦法是通過下標一個一個去找到數(shù)組當中的每一個元素,分別傳入到console.log方法當中。
在ES2015當中就沒有必要這么麻煩了,可以直接去調(diào)用console的log方法,然后通過...的操作符展開這里的數(shù)組。...操作符會把數(shù)組當中的每一個成員按照次序傳遞到列表當中。
console.log( ...arr );12、 箭頭函數(shù)
在ECMAScript當中簡化了函數(shù)表達式的定義方式允許使用=>這種類似箭頭的符號來去定義函數(shù),那這種函數(shù)一來簡化了函數(shù)的定義,二來多了一些特性具體來看。
傳統(tǒng)來定義一個函數(shù)需要使用function關(guān)鍵詞,現(xiàn)在可以使用ES2015來去定義一個完全相同的函數(shù)。
function inc (number) {return number + 1;}const inc = n => n + 1;
此時你會發(fā)現(xiàn),相比于普通的函數(shù),剪頭函數(shù)確實大大簡化了所定義函數(shù)這樣一些相關(guān)的代碼。
剪頭函數(shù)的左邊是參數(shù)列表,如果有多個參數(shù)的話可以使用()包裹起來,剪頭的右邊是函數(shù)體。
如果在這個函數(shù)的函數(shù)體內(nèi)需要執(zhí)行多條語句,同樣可以使用{}去包裹。如果只有一句代碼可以省略{}。
const inc = (n , m) => {return n + 1;};
對比普通函數(shù)和剪頭函數(shù)的寫法你會發(fā)現(xiàn),使用剪頭函數(shù)會讓代碼更簡短,而且更易讀。
1. this
相比普通函數(shù),箭頭函數(shù)有一個很重要的變化就是不會改變this的指向。
定義一個person對象,然后在這個對象當中去定義一個name屬性,然后再去定義一個sayHi的方法,這個方法中可以使用this去獲取當前對象。
const person = {name: 'yd',sayHi: function() {console.log(this.name);}}person.sayHi(); // yd
這里把sayHi改為箭頭函數(shù)的方式。這個時候打印出來的name就是undefined
const person = {name: 'yd',sayHi: () => {console.log(this.name);}}person.sayHi(); // undefined
這就是箭頭函數(shù)和普通函數(shù)最重要的區(qū)別,在剪頭函數(shù)當中沒有this的機制。所以說不會改變this的指向。
也就是說在剪頭函數(shù)的外面this是什么,在里面拿到的就是什么,任何情況下都不會發(fā)生改變。
13. 對象字面量的增強
傳統(tǒng)的字面量要求必須在{}里面使用屬性名: 屬性值這種語法。即便說屬性的值是一個變量,那也必須是屬性名: 變量名, 而現(xiàn)在如果變量名與添加到對象中的屬性名是一樣的,可以省略掉:變量名。
const bar = '123';const obj = {key: 'value',bar}
除此之外如果需要為對象添加一個普通的方法,現(xiàn)在可以省略里面的:function。
const obj = {method1 () {console.log('method1');}}console.log(obj)
需要注意的是這種方法的背后他實際上就是普通的function,也就是說如果通過對象去調(diào)用這個方法,那么內(nèi)部的this就會指向當前對象。
14. 動態(tài)屬性名
另外對象字面量還有一個很重要的變化就是,他可以使用表達式的返回值作為對象的屬性名。以前如果說要為對象添加一個動態(tài)的屬性名,只能在對象創(chuàng)建過后,然后通過索引器的方式也就是[]來去動態(tài)添加。
const obj = {};obj[Math.random()] = 123;
在ES2015過后,對象字面量的屬性名直接可以通過[]直接去使用動態(tài)的值了,這樣一個特性叫做計算屬性名,具體的用法就是在屬性名的位置用[]包起來。
在里面就可以使用任意的表達式了。這個表達式的執(zhí)行結(jié)果將會作為這個對象的屬性名。
const obj = {[Math.random()]: 123,}
15. Object.assign
這個方法可以將多個源對象當中的屬性復制到一個目標對象當中,如果對象當中有相同的屬性,那么源對象當中的屬性就會覆蓋掉目標對象的屬性。
這里所說的源對象和目標對象他們都是普通的對象,只不過用處不同,是從源對象當中取,然后往目標對象當中放。
例如這里先定義一個source1對象,在這個對象當中定義一個a屬性和一個b屬性。然后再來定義一個target對象,這個對象當中也定義一個a屬性,還有一個c屬性。
const source1 = {a: 123,b: 123,}const target = {a: 456,c: 456}
Object.assign支持傳入任意個數(shù)的參數(shù),其中第一個參數(shù)就是目標對象,也就是說所有源對象當中的屬性都會復制到目標對象當中。這個方法的返回值也就是這個目標對象。
const result = Object.assign(target, source1);console.log(target, result === target); {a: 123, c: 456, b: 123 }// true
Object.assign用來為options對象參數(shù)設置默認值也是一個非常常見的應用場景。
const default = {name: 'yd',age: 18}const options = Object.assign(default, opt);
16. Object.is
is方法用來判斷兩個值是否相等。在此之前ECMAScript當中去判斷兩個值是否相等可以使用==運算符。或者是===嚴格相等運算符。
這兩者是區(qū)別是==會在比較之前自動轉(zhuǎn)換數(shù)據(jù)類型,那也就會導致0 == false這種情況是成立的。
而===就是嚴格去對比兩者之間的數(shù)值是否相同。因為0和false他們之間的類型不同所以說他們是不會嚴格相等的。
但是嚴格相等運算符他也有兩個特殊情況,首先就是對于數(shù)字0,他的正負是沒有辦法區(qū)分的。
其次對于NaN, 兩個NaN在===比較時是不相等的。以前認為NaN是一個非數(shù)字,也就是說他有無限種可能,所以兩個NaN他是不相等的,但在今天看來,NaN他實際上就是一個特別的值,所以說兩個NaN他應該是完全相等的。
所以在ES2015中就提出了一種新的同值比較的算法來解決這個問題,通過Obejct.is正負零就可以被區(qū)分開,而且NaN也是等于NaN的。
Object.is(+0, -0); // falseObject.is(NaN, NaN); // true
不過一般情況下根本不會用到這個方法,大多時候還是使用嚴格相等運算符,也就是===。
17. Proxy
專門為對象設置訪問代理器的,那如果你不理解什么是代理可以想象成門衛(wèi),也就是說不管你進去那東西還是往里放東西都必須要經(jīng)過這樣一個代理。
通過Proxy就可以輕松監(jiān)視到對象的讀寫過程,相比于defineProperty,Proxy的功能要更為強大甚至使用起來也更為方便。
通過new Proxy的方式創(chuàng)建代理對象。
Proxy構(gòu)造函數(shù)的第一個參數(shù)就是需要代理的對象,第二個參數(shù)是代理對象處理對象,這可以通過get方法來去監(jiān)視屬性的訪問,通過set方法來截取對象當中設置屬性的過程。
const person = {name: 'yd',age: 18}const personProxy = new Proxy(person, {get() {},set() {}})
get方法可以接收兩個參數(shù),第一個就是所代理的目標對象,第二個就是外部所訪問的這個屬性的屬性名。。
{get(target, property) {console.log(target, property);return property in target ? target[property] : undefined;}}
set方法接收三個參數(shù), 分別是代理目標對象,以及要寫入的屬性名和屬性值。
{set(target, property, value) {console.log(target, property, value);if (property === 'age') {if (!Number.isInteger(value)) {throw new TypeError(``${value} must be a integer);}}target[property] = value;}}
1. Proxy 對比 defineProperty
相比于Object.defineProperty,Proxy到底有哪些優(yōu)勢。
Object.defineProperty只能監(jiān)聽到對象屬性的讀取或?qū)懭耄琍roxy除讀寫外還可以監(jiān)聽對象中屬性的刪除,對對象當中方法調(diào)用等。
const person = {name: 'yd',age: 18}const personProxy = new Proxy(person, {deleteProperty(target, property) {console.log(target, property);delete target[property];},})
除了delete以外, 還有很多其他的對象操作都能夠被監(jiān)視到,列舉如下。
get: 讀取某個屬性
set: 寫入某個屬性
has:in操作符調(diào)用
deleteProperty:delete操作符調(diào)用
getProperty:Object.getPropertypeOf()setProperty:Object.setProtoTypeOf()isExtensible:Object.isExtensible()preventExtensions:Object.preventExtensions()getOwnPropertyDescriptor:Object.getOwnPropertyDescriptor()defineProperty:Object.defineProperty()ownKeys:Object.keys(),Object.getOwnPropertyNames(),Object.getOwnPropertSymbols()
apply: 調(diào)用一個函數(shù)
construct: 用new調(diào)用一個函數(shù)。
第二點是對于數(shù)組對象進行監(jiān)視更容易。
通常想要監(jiān)視數(shù)組的變化,基本要依靠重寫數(shù)組方法,這也是Vue的實現(xiàn)方式,Proxy可以直接監(jiān)視數(shù)組的變化。
const list = [];const listProxy = new Proxy(list, {set(target, property, value) {console.log(target, property, value);target[property] = value;return true; // 寫入成功}});listProxy.push(100);
Proxy內(nèi)部會自動根據(jù)push操作推斷出來他所處的下標,每次添加或者設置都會定位到對應的下標property。數(shù)組其他的也謝操作方式都是類似的。
最后Proxy是以非入侵的方式監(jiān)管了對象的讀寫,那也就是說一個已經(jīng)定義好的對象不需要對對象本身去做任何的操作,就可以監(jiān)視到他內(nèi)部成員的讀寫,而defineProperty的方式就要求必須按特定的方式單獨去定義對象當中那些被監(jiān)視的屬性。
對于一個已經(jīng)存在的對象要想去監(jiān)視他的屬性需要做很多額外的操作。這個優(yōu)勢實際上需要有大量的使用然后在這個過程當中去慢慢的體會。
18. Reflect
如果按照java或者c#這類語言的說法,Reflect屬于一個靜態(tài)類,也就是說他不能通過new的方式去構(gòu)建一個實例對象。只能夠去調(diào)用這個靜態(tài)類中的靜態(tài)方法。
這一點應該并不陌生,因為在javascript中的Math對象也是相同的,Reflect內(nèi)部封裝了一系列對對象的底層操作,具體一共提供了14個靜態(tài)方法,其中有1個已經(jīng)被廢棄掉了,那還剩下13個,仔細去查看Reflect的文檔會發(fā)現(xiàn)這13個方法的方法名與Proxy的處理對象里面的方法成員是完全一致的。
其實這些方法就是Proxy處理對象那些方法內(nèi)部的默認實現(xiàn).
const obj = {foo: '123',bar: '456',}const Proxy = new Proxy(obj, {get(target, property) {console.log('實現(xiàn)監(jiān)視邏輯');return Reflect.get(target, property);}})
這也就表明在實現(xiàn)自定義的get或者set這樣的邏輯時更標準的做法是先去實現(xiàn)自己所需要的監(jiān)視邏輯,最后再去返回通過Reflect中對應的方法的一個調(diào)用結(jié)果。
個人認為Reflect對象最大的意義就是他提供了一套統(tǒng)一操作Object的API,因為在這之前去操作對象時有可能使用Object對象上的方法,也有可能使用像delete或者是in這樣的操作符,這些對于新手來說實在是太亂了,并沒有什么規(guī)律。
Reflect對象就很好的解決了這樣一個問題,他統(tǒng)一了對象的操作方式。
19. Promise
Promise提供了一種全新的異步編程解決方案,通過鏈式調(diào)用的方式解決了在傳統(tǒng)異步編程過程中回調(diào)函數(shù)嵌套過深的問題。
關(guān)于Promise的細節(jié)有很多內(nèi)容,所以說這里先不做詳細介紹在JavaScript異步編程的文章中已經(jīng)專門針對Promise進行了詳細的分析。
20. class類
以前ECMAScript中都是通過定義函數(shù)以及函數(shù)的原型對象來去實現(xiàn)的類型,在構(gòu)造函數(shù)中可以通過this去訪問當前的實例對象,如果需要在這個類型所有的實例間去共享一些成員,可以借助于函數(shù)對象的prototype, 也就是原型去實現(xiàn)。
function Person (name) {this.name = name;}Person.prototype.say = function() {console.log(this.name);}
ECMAScript2015可以使用一個叫做class的關(guān)鍵詞來聲明類型,這種獨立定義類型的語法,要更容易理解,結(jié)構(gòu)也會更加清晰。
class Person {}
這種語法與一些老牌面向?qū)ο笳Z言當中class非常相似的。如果需要在構(gòu)造函數(shù)當中做一些額外的邏輯,可以添加一個constructor方法,這個方法就是構(gòu)造函數(shù)。
同樣可以在這個函數(shù)中使用this去訪問當前類型的實例對象。
class Person {constructor (name) {this.name = name;}say() {console.log(this.name);}}
1. 靜態(tài)方法
類型中的方法分為實例方法和靜態(tài)方法,實例方法就是需要通過這個類型構(gòu)造的實例對象去調(diào)用,靜態(tài)方法是直接通過類型本身去調(diào)用。可以通過static關(guān)鍵字定義。
class Person {constructor (name) {this.name = name;}say() {console.log(this.name);}static create (name) {return new Person(name);}}
調(diào)用靜態(tài)方法是直接通過類型然后通過成員操作符調(diào)用方法名字。
const yd = Person.create('yd');注意:因為靜態(tài)方法是掛載到類型上面的,所以說在靜態(tài)方法內(nèi)部他不會指向某一個實例對象,而是當前的類型。
2. 類的繼承
繼承是面向?qū)ο螽斨幸粋€非常重要的特性,通過繼承這種特性能抽象出來相似類型之間重復的地方, 可以通過關(guān)鍵詞extends實現(xiàn)繼承。
super對象指向父類, 調(diào)用它就是調(diào)用了父類的構(gòu)造函數(shù)。
class Student extends Person {constructor(name, number) {super(name);this.number = number;}hello () {super.say();console.log(this.number);}}const s = new Student('yd', '100');s.hello();
21. Set
可以把他理解為集合,他與傳統(tǒng)的數(shù)組非常類似,不過Set內(nèi)部的成員是不允許重復的。那也就是說每一個值在同一個Set中都是唯一的。
通過這個類型構(gòu)造的實例就用來存放不同的數(shù)據(jù)。可以通過這個實例的add方法向集合當中去添加數(shù)據(jù),由于add方法他會返回集合對象本身,所以可以鏈式調(diào)用。如果在這個過程中添加了之前已經(jīng)存在的值那所添加的這個值就會被忽略掉。
const s = new Set();s.add(1).add(2).add(3).add(2);
想要遍歷集合當中的數(shù)據(jù),可以使用集合對象的forEach方法去傳遞一個回調(diào)函數(shù)。
s.forEach(i => console.log(i));也可以使用for...of循環(huán)。
for (let i of s) {console.log(i);}
可以通過size屬性來去獲取整個集合的長度。
console.log(s.size);has方法就用來判斷集合當中是否存在某一個特定的值。
console.log(s.has(100)); // falsedelete方法用來刪除集合當中指定的值,刪除成功將會返回一個true。
console.log(s.delete(3)); // trueclear方法用于清除當前集合當中的全部內(nèi)容。
s.clear()22. Map
Map結(jié)構(gòu)與對象非常類似,本質(zhì)上他們都是鍵值對集合但是這種對象結(jié)構(gòu)中的鍵,只能是字符串類型,如果說用其他類型作為鍵會被轉(zhuǎn)換成字符串,出現(xiàn)[object object]。
不同的對象轉(zhuǎn)換成字符串可能會變成相同的鍵名[object object]導致數(shù)據(jù)覆蓋丟失。
Map類型才算是嚴格上的鍵值對類型,用來映射兩個任意類型之間鍵值對的關(guān)系。可以使用這個對象的set方法去存數(shù)據(jù)。鍵可以是任意類型的數(shù)據(jù)。不需要擔心他會被轉(zhuǎn)換為字符串。
const m = new Map();const key = {};m.set(key, 18);console.log(m);
可以使用get方法獲取數(shù)據(jù),has方法判斷他里面是否存在某個鍵。delete方法刪除某個鍵。clear方法清空所有的鍵值。
console.log(m.get(key));console.log(m.has(key));m.delete(key);m.clear();
可以使用forEach方法遍歷。在這個方法的回調(diào)函數(shù)當中第一個參數(shù)就是被遍歷的值,第二個參數(shù)是被遍歷的鍵。
m.forEach((value, key) => {console.log(value, key);})
23. Symbol
在ECMAScript2015之前對象的屬性名都是字符串,而字符串是有可能會重復的。如果重復的話就會產(chǎn)生沖突,比如在使用第三方模塊時,如果需要擴展第三方模塊,而這時就有可能把第三方模塊的方法覆蓋掉,導致代碼執(zhí)行異常。
ES2015提供了一種全新的原始數(shù)據(jù)類型Symbol,翻譯過來的意思叫做符號,翻譯過來就是表示一個獨一無二的值。
通過Symbol函數(shù)就可以創(chuàng)建一個Symbol類型的數(shù)據(jù),而且這種類型的數(shù)據(jù)typeof的結(jié)果也是symbol,那這也就表示他確實是一個全新的類型。
const s = Symbol();typeof s; // symbol類型
這種類型最大的特點就是獨一無二,通過Symbol函數(shù)創(chuàng)建的每一個值都是唯一的永遠不會重復。
Symbol() === Symbol(); // falseSymbol創(chuàng)建時允許接收一個字符串,作為這個值的描述文本, 對于多次使用Symbol時就可以區(qū)分出是哪一個Symbol,這個參數(shù)僅是描述作用,相同的描述字段生成的值仍是不同的。
const s1 = Symbol('foo');const s2 = Symbol('foo');s1 === s2; // false
ES2015開始對象允許使用Symbol作為屬性名。那也就是說現(xiàn)在對象的屬性名可以是兩種類型,string和Symbol。
const person = {[Symbol()]: 123,[Symbol()]: 456}
Symbol除了用在對象中避免重復以外,還可以借助這種類型的特點來模擬實現(xiàn)對象的私有成員。以前私有成員都是通過約定,例如約定使用下劃線開頭就表示是私有成員。約定外界不允許訪問下劃線開頭的成員。
現(xiàn)在有了Symbol就可以使用Symbol去作為私有成員的屬性名了。在這個對象的內(nèi)部可以使用創(chuàng)建屬性時的Symbol。去拿到對應的屬性成員。
const name = Symbol();const person = {[name]: 'yd',say() {return this[name];}}
截止到2020標準,ECMAScript一共定義了8種數(shù)據(jù)類型。
如果需要在全局去復用一個相同的Symbol值,可以使用全局變量的方式去實現(xiàn),或者是使用Symbol類型提供的一個靜態(tài)方法for,這個方法接收一個字符串作為參數(shù),相同的參數(shù)一定對應相同的值。
const s1 = Symbol.for('foo');const s2 = Symbol.for('foo');s1 === s2; // true
這個方法維護了一個全局的注冊表,為字符串和Symbol提供了一個對應關(guān)系。需要注意的是,在內(nèi)部維護的是字符串和Symbol的關(guān)系,那也就是說如參數(shù)不是字符串,會轉(zhuǎn)換為字符串。
const s1 = Symbol.for('true');const s2 = Symbol.for(true);s1 === s2; // true
1. 常量
在Symbol內(nèi)部提供了很多內(nèi)置的Symbol常量,用來去作為內(nèi)部方法的標識,可以讓自定義對象去實現(xiàn)一些js內(nèi)置的接口。
如果想要自定義對象的toString標簽,ECMAScript要求使用Symbol的值來去實現(xiàn)這樣一個接口。
obj[Symbol.toStringTag] = 'test'obj.toString(); // [object test];
這里的toStringTag就是內(nèi)置的一個Symbol常量,這種Symbol在后面為對象去實現(xiàn)迭代器時會經(jīng)常遇到。
使用Symbol的值去作為對象的屬性名那這個屬性通過傳統(tǒng)的for in循環(huán)是無法拿到的。
而且通過Object.keys方法也是獲取不到這樣Symbol類型的屬性名。JSON.stringify去序列化,Symbol屬性也會被隱藏掉。
const obj = {[Symbol()]: 'symbol value',foo: 'normal value'}for (var key in obj) {console.log(key);}Object.keys(obj);
總之這些特性都使得Symbol屬性,特別適合作為對象的私有屬性,當然想要獲取這種類型的屬性名可以使用Object.getOwnPropertySymbols(obj)方法。
Object.getOwnPropertySymbols(obj)這個方法的作用類似于Object.keys, 所不同的是Object.keys他只能獲取對象當中字符串屬性名,而Object.getOwnPropertySymbols方法他獲取到的全是Symbol類型的屬性名。
24. for…of
for...of是ECMAScript2015之后新增的遍歷方式。未來會作為遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一方式。
const arr = [1, 2, 3, 4];for (const item of arr) {console.log(item); // 1, 2,3,4// break; // 終止循環(huán)}
for…of循環(huán)拿到的就是數(shù)組中的每一個元素,而不是對應的下標。這種循環(huán)方式就可以取代之前常用的數(shù)組實例當中的forEach方法。
for...of循環(huán)可以使用break關(guān)鍵詞隨時去終止循環(huán)。
除了數(shù)組可以直接被for...of循環(huán)去遍歷,一些偽數(shù)組對象也是可以直接被for...of去遍歷的,例如arguments,set,map。
for...of在遍歷Map時, 可以直接拿到鍵和值。鍵和值是直接以數(shù)組的形式返回的,也就是說數(shù)組的第一個元素就是當前的鍵名,第二個元素就是值。這就可以配合數(shù)組的解構(gòu)語法,直接拿到鍵和值。
const m = new Map();m.set('foo', '123');m.set('bar', '345');for (const item if m) {console.log(item); // ['foo', '123'];}for (const [key, value] if m) {console.log(key, value); // 'foo', '123'}
for...of是不能直接遍歷普通對象的,他要求被遍歷的對象必須存在一個叫做Iterable的接口。
1. 可迭代接口
可迭代接口就是一種可以被for...of循環(huán)統(tǒng)一遍歷訪問的規(guī)格標準,換句話說只要這個數(shù)據(jù)結(jié)構(gòu)實現(xiàn)了可迭代接口他就能夠被for...of循環(huán)遍歷,那這也就是說之前嘗試的那些能夠直接被for...of循環(huán)去遍歷的數(shù)據(jù)類型他都已經(jīng)在內(nèi)部實現(xiàn)了這個接口。
iterable約定對象中必須要掛載一個叫做Symbol.iterable的方法,這個方法返回一個對象,對象上存在一個next方法,next方法也返回一個對象,對象中存在value和done兩個屬性,value是當前遍歷到的值,done為是否為最后一個。
每調(diào)用一次next就會后移一位。
總結(jié)起來就是所有被for...of遍歷的數(shù)據(jù)類型必須包含一個叫做iterable的接口,也就是內(nèi)部必須掛載一個Symbol.iterable方法,這個方法需要返回一個帶有next方法的對象,不斷調(diào)用這個next方法就可以實現(xiàn)對內(nèi)部所有成員的遍歷。
這就是for...of循環(huán)的內(nèi)部原理。
2. 實現(xiàn)可迭代接口
在這個對象中放一個數(shù)組store用來存放值得被遍歷的數(shù)據(jù),然后在next方法中去迭代這個數(shù)組,需要去維護一個下標index,讓他默認等于0;
由于next中的函數(shù)并不是obj對象,所以使用self去存儲一下當前的this供下面使用,在next方法中value就是self.store[index],done就是index >= self.store.length。完成以后需要讓index++, 也就是讓指針后移一位。
const obj = {store: [1, 2, 3, 4, 5],[Symbol.iterable]: function() {let index = 0;const self = this;return {next: function() {const result = {value: self.store[index],done: index >= self.store.length}index++;return result;}}}}for (const item of obj) {console.log(item); // 1, 2, 3, 4, 5}
25. 生成器
ECMAScript2015中還新增了一種生成器函數(shù)generator,是為了能夠在復雜的異步編程中減少回調(diào)函數(shù)嵌套產(chǎn)生的問題,從而去提供更好的異步編程解決方案。
定義生成器函數(shù)就是在普通的函數(shù)function后面添加一個*,這樣函數(shù)就變成了一個生成器函數(shù)。函數(shù)執(zhí)行之后會返回一個生成器對象。
function * foo() {return 100;}const result = foo();console.log(result);
在這個對象上也和迭代器一樣有一個next方法,實際上生成器函數(shù)也實現(xiàn)了iterable接口,也就是迭代器接口協(xié)議。
生成器函數(shù)在使用會配合一個叫做yield的關(guān)鍵字,yield關(guān)鍵詞與return關(guān)鍵詞類似,但是又有很大的不同。
生成器函數(shù)會自動返回一個生成器對象,調(diào)用這個生成器的next會讓這個函數(shù)的函數(shù)體開始執(zhí)行,執(zhí)行過程中一旦遇到y(tǒng)ield關(guān)鍵詞函數(shù)的執(zhí)行就會被暫停下來,而且yield的值將會被作為next的結(jié)果返回,繼續(xù)調(diào)用next函數(shù)就會從暫停的位置繼續(xù)向下執(zhí)行到下一個yield直到這個函數(shù)完全結(jié)束。
const * foo() {console.log(1111);yield 100;console.log(2222);yield 200;console.log(3333);yield 300;}
生成器函數(shù)最大的特點就是惰性執(zhí)行,每調(diào)用一次next就會執(zhí)行一次yield。
1. 生成器應用
了解了生成器函數(shù)的基本用法來看一個簡單的應用場景實現(xiàn)一個發(fā)號器。
在實際業(yè)務開發(fā)過程中經(jīng)常需要用到自增的id,而且每次調(diào)用這個id都需要在原有的基礎上去+1,這里如果使用生成器函數(shù)去實現(xiàn)這樣一個功能是最合適的了。
首先定義一個createId生成器函數(shù),然后定義一個初始的id等于1,然后通過一個死循環(huán)不斷的去yield id++。這里不需要擔心死循環(huán)的問題,因為每次在yield過后這個方法會被暫停,循環(huán)自然也就會被暫停。直到下一次調(diào)用next再次去執(zhí)行一次又會被暫停下來。
這樣在外部就可以通過這個方法去創(chuàng)建一個生成器對象id,每次調(diào)用一下這個生成器的next方法就能夠獲取到自增的value,也就是id。
function * createId() {let id = 1;while(true) {yield id++;}}const id = createId();id.next().value;
實現(xiàn)發(fā)號器是一個非常簡單的需求,還可以使用生成器函數(shù)實現(xiàn)對象的iterator方法,因為生成器也實現(xiàn)了對象的iterator接口,而且使用生成器函數(shù)去實現(xiàn)iterator方法會比之前的方式簡單很多。
26. ES Modules
ES Modules是ECMAScript2015中標準化的一套語言層面的模塊化標準規(guī)范,我之前寫過一篇模塊化發(fā)展歷程的文章,里面有詳細的介紹,里面和CommonJs以及其他標準做了統(tǒng)一的對比,感興趣的可以翻閱一下那篇文章。
27. include方法
這個方法檢查數(shù)組中是否存在某個元素。在這之前如果需要檢查數(shù)組中是否包含某個元素都是使用indexOf方法。
但是indexOf不能查詢到數(shù)組中的NaN,現(xiàn)在有了includes方法之后直接可以判斷數(shù)組當中是否存在某個指定的元素了,并且他返回的是一個布爾值,而且也可以判斷NaN
28. **指數(shù)運算符
以前需要進行指數(shù)運算需要借助Math對象的pow方法來去實現(xiàn)。例如去求2的10次方。
Math.pow(2, 10); // 表示2的10次方。指數(shù)運算符,他就是語言本身的運算符,就像是之前所使用的加減乘除運算符一樣,使用起來也非常簡單。
2**10; // 2的10次方29. Object對象新增三個擴展方法
Object.keys返回的是所有的鍵組成的數(shù)組,Object.values返回的是所有值組成的數(shù)組。
Object.entries將對象轉(zhuǎn)成數(shù)組,每個元素是鍵值對的數(shù)組,可以快速將對象轉(zhuǎn)為Map
const l = Object.entries({a: 1, b: 2});const m = new Map(l);
30. Object.getOwnPropertyDescriptors
獲取對象的描述信息
Object.assign復制時,將對象的屬性和方法當做普通屬性來復制,并不會復制完整的描述信息,比如this等.
const p1 = {a: 'y',b: 'd',get name() {return `${this.a} ${this.b}`;}}const p2 = Object.assign({}, p1);p2.a = 'z';p2.name; // y d; 發(fā)現(xiàn)并沒有修改到a的值,是因為this仍舊指向p1
使用Object.getOwnPropertyDescriptors獲取完整描述信息
const description = Object.getOwnPropertyDescriptors(p1);const p2 = Object.defineProperty({}, description);p2.a = 'z';p2.name; // z d
31. String.prototype.String.prototype.padStart
用給定的字符串在尾部拼接到指定長度
'abc'.padEnd(5, '1'); // abc11;用給定的字符串在首部拼接到指定長度
'abc'.padStart(5, '1'); // 11abc;32. 允許對象和數(shù)組在最后添加一個逗號
[1, 2, 3,]{a: 1, b: 2, }
33. async + await
在函數(shù)聲明時加入async關(guān)鍵字,則函數(shù)會變?yōu)楫惒胶瘮?shù),當使用await調(diào)用時,只有等到被await的promise返回,函數(shù)才會向下執(zhí)行。
const as = async () => {const data = await ajax();}
34. 收集剩余屬性
將對象或者數(shù)組的剩余屬性收集到新對象中
const data = {a: 1, b: 2, c: 3, d: 4};const {a, b, ...arg} = data;console.log(arg); // {c: 3, d: 4};
事實上 Map、Set、String 同樣支持該能力。
35. for of 支持異步迭代
在此之前想要實現(xiàn)異步迭代想要在for of外層嵌套一個async函數(shù)
async function () {for (const fn of actions) {await fn();}}
ES2018提供了一種新的書寫方式。
async function() {for await (const fn of actions) {fn();}}
36. JSON 成為 ECMAScript 的完全子集
在以前,行分隔符\u2028和段分隔符\u2029會導致JSON.parse拋出語法錯誤異常。
ECMAScript優(yōu)化了這個功能。
JSON.stringify也做了改進,對于超出Unicode范圍的轉(zhuǎn)義序列,JSON.stringify()會輸出未知字符:
JSON.stringify('\uDEAD'); // '"?"'37. 修正Function.prototpye.toString()
在以前,返回的內(nèi)容中function關(guān)鍵字和函數(shù)名之間的注釋,以及函數(shù)名和參數(shù)列表左括號之間的空格,是不會被顯示出來的。現(xiàn)在會精確返回這些內(nèi)容,函數(shù)如何定義的,這就會如何顯示。
38. Array.prorptype.flat()、Array.prorptype.flatMap()
flat()用于對數(shù)組進行降維,它可以接收一個參數(shù),用于指定降多少維,默認為1。降維最多降到一維。
const array = [1, [2, [3]]]array.flat() // [1, 2, [3]]array.flat(1) // [1, 2, [3]],默認降 1 維array.flat(2) // [1, 2, 3]array.flat(3) // [1, 2, 3],最多降到一維
flatMap()允許在對數(shù)組進行降維之前,先進行一輪映射,用法和map()一樣。然后再將映射的結(jié)果降低一個維度。可以說arr.flatMap(fn)等效于arr.map(fn).flat(1)。但是根據(jù)MDN的說法,flatMap()在效率上略勝一籌,誰知道呢。
flatMap()也可以等效為reduce()和concat()的組合,下面這個案例來自MDN,但是這不是一個map就能搞定的事么?
var arr1 = [1, 2, 3, 4];arr1.flatMap(x => [x * 2]);// 等價于arr1.reduce((acc, x) => acc.concat([x * 2]), []);// [2, 4, 6, 8]
39. String.prototype.trimStart()、String.prototype.trimEnd()
用過字符串trim()的都知道這兩個函數(shù)各自負責只去掉單邊的多余空格。
40. Object.fromEntries()
從名字就能看出來,這是Object.entries()的逆過程。Object.fromEntries()可以將數(shù)組轉(zhuǎn)化為對象。
41. description of Symbol
Symbol是新的原始類型,通常在創(chuàng)建Symbol時會附加一段描述。只有把這個Symbol轉(zhuǎn)成String才能看到這段描述,而且外層還套了個 'Symbol()' 字樣。ES2019為Symbol新增了description屬性,專門用于查看這段描述。
const sym = Symbol('The description');String(sym) // 'Symbol(The description)'sym.description // 'The description'
42. try…catch
過去,catch后面必須有一組括號,里面用一個變量代表錯誤信息對象。現(xiàn)在這部分是可選的了,如果異常處理部分不需要錯誤信息,可以把它省略,像寫if...else一樣寫try...catch。
try {throw new Error('Some Error')} catch {handleError() // 這里沒有用到錯誤信息,可以省略 catch 后面的 (e)。}
43. Array.prototype.sort
JavaScript中內(nèi)置的數(shù)組排序算法使用的是不穩(wěn)定的排序算法,也就是說在每一次執(zhí)行后,對于相同數(shù)據(jù)來說,它們的相對位置是不一致的。
var arr1 = [{a: 1, b: 2}, {a: 2, b: 2}, {a: 1, b: 3}, {a: 2, b: 4}, {a: 5, b: 3}];arr1.sort((a, b) => a.a - b.a);
返回的結(jié)果第一次可能是這樣的:[{a: 1, b: 2}, {a: 1, b: 3}...]
但是第二次就變成:[{a:1,b:3}, {a:1, b: 2}....]
那么在es2019中,JavaScript內(nèi)部放棄了不穩(wěn)定的快排算法,而選擇使用Tim Sort這種穩(wěn)定的排序算法。優(yōu)化了這個功能。
44. 類的私有屬性
你可能不希望類內(nèi)部的所有內(nèi)容都是全局可用的。通過在變量或函數(shù)前面添加一個#可以將它們完全保留為類內(nèi)部使用
class Message {greet() { console.log(this.#message) }}const greeting = new Message()greeting.greet() // Howdy 內(nèi)部可以訪問console.log(greeting.#message) // Private name #message is not defined 不能直接被訪問
45. Promise.allSettled
當處理多個Promise時,特別是當它們相互依賴時,記錄每個Promise所發(fā)生的事情來調(diào)試錯誤是很有必要的。
通過Promise.allSettled可以創(chuàng)建一個新的Promise,它只在所有傳遞給它的Promise都完成時返回一個數(shù)組,其中包含每個Promise的數(shù)據(jù)。
const p1 = new Promise((res, rej) => setTimeout(res, 1000));const p2 = new Promise((res, rej) => setTimeout(rej, 1000));Promise.allSettled([p1, p2]).then(data => console.log(data));// [// Object { status: "fulfilled", value: undefined},// Object { status: "rejected", reason: undefined}// ]
Promise.all是當多個promise全部成功,或出現(xiàn)第一個失敗就會結(jié)束。Promise.allSettled是所有都執(zhí)行完成,無論成功失敗。
46. 合并空運算符 ??
假設變量不存在,希望給系統(tǒng)一個默認值,一般會使用||運算符。但是在javascript中空字符串,0,false都會執(zhí)行||運算符,ECMAScript2020引入合并空運算符解決該問題,只允許在值為null或undefined時使用默認值。
const name = '';console.log(name || 'yd'); // yd;console.log(name ?? 'yd'); // '';
47. 可選鏈運算符 ?.
業(yè)務代碼中經(jīng)常會遇到這樣的情況,a對象有個屬性b,b也是一個對象有個屬性c。
const a = {b: {c: 123,}}
訪問c,經(jīng)常會寫成a.b.c,但是如果b不存在時,就會出錯。
ECMAScript2020定義可選鏈運算符解決該問題,在.之前添加一個?將鍵名變成可選。
let person = {};console.log(person?.profile?.age ?? 18); // 18
48. bigInt
JavaScript可以處理的最大數(shù)字是2的53次方 - 1,可以在可以在Number.MAX_SAFE_INTEGER中看到。
更大的數(shù)字則無法處理,ECMAScript2020引入BigInt數(shù)據(jù)類型來解決這個問題。通過把字母n放在末尾, 可以運算大數(shù)據(jù)。通過常規(guī)操作進行加、減、乘、除、余數(shù)和冪等運算。
它可以由數(shù)字和十六進制或二進制字符串構(gòu)造。此外它還支持AND、OR、NOT和XOR之類的按位運算。唯一無效的位運算是零填充右移運算符。
const bigNum = 100000000000000000000000000000n;console.log(bigNum * 2n); // 200000000000000000000000000000nconst bigInt = BigInt(1);console.log(bigInt); // 1n;const bigInt2 = BigInt('2222222222222222222');console.log(bigInt2); // 2222222222222222222n;
BigInt是一個大整數(shù),不能存儲小數(shù)。
49. 動態(tài)導入import
import('./a.js')返回一個Promise對象。
const a = 123;export { a };
import('./a.js').then(data => {console.log(data.a); // 123;})
50. globalThis
標準化方式訪問全局對象,globalThis在瀏覽器中window作為全局對象,在node中g(shù)lobal作為全局對象,ECMAScript2020提供globalThis作為語言的全局對象,方便代碼移植到不同環(huán)境中運行。
51. String.prototype.replaceAll()
為了方便字符串的全局替換,ES2021將支持String.prototype.replaceAll()方法,可以不用寫正則表達式就可以完成字符串的全局替換
'abc111'.replaceAll('1', '2'); // abc22252. Promise.any
只要有一個promise是fulfilled時,則返回一個resolved promise;所有的promise都是rejected時,則返回一個rejected promise
Promise.any([ Promise.reject(1), Promise.resolve(2) ]).then(result => console.log('result:', result)).catch(error => console.error('error:', error)); // result: 2
53. 邏輯賦值運算符
邏輯賦值運算符由邏輯運算符和賦值表達式組合而成:
a ||= b;// 與 a ||= b 等價a || (a = b);// 與 a ||= b 等價if (!a) {a = b;}a &&= b;// 與 a &&= b 等價a && (a = b);// 與 a &&= b 等價if (a) {a = b;}
a ??= b;// 與 a ??= b 等價a ?? (a = b);// 與 a ??= b 等價if (a === null || a === undefined) {a = b;}
注意:
a = a || b; // 與 a ||= b 不等價a = a && b; // 與 a &&= b 不等價a = a ?? b; // 與 a ??= b 不等價
54. 數(shù)字分隔符
使用_對數(shù)字進行分割,提高數(shù)字的可讀性,例如在日常生活中數(shù)字通常是每三位數(shù)字之間會用`, 分割,以方便人快速識別數(shù)字。在代碼中,也需要程序員較便捷的對數(shù)字進行辨識。
// 1000000000 不易辨識const count1 = 1000000000;// 1_000_000_000 很直觀const count2 = 1_000_000_000;console.log(count1 === count2); // true
55. WeakRefs
WeakRef實例可以作為對象的弱引用,對象的弱引用是指當該對象應該被GC回收時不會阻止GC的回收行為。而與此相反的,一個普通的引用(默認是強引用)會將與之對應的對象保存在內(nèi)存中。
只有當該對象沒有任何的強引用時,JavaScript引擎GC才會銷毀該對象并且回收該對象所占的內(nèi)存空間。因此,訪問弱引用指向的對象時,很有可能會出現(xiàn)該對象已經(jīng)被回收。
const ref = new WeakRef({ name: 'koofe' });let obj = ref.deref();if (obj) {console.log(obj.name); // koofe}
對于WeakRef對象的使用要慎重考慮,能不使用就盡量不要使用。
學習更多技能
請點擊下方公眾號
![]()

