ECMAScript學習(二)
Symbol
ES6 引入了一種新的原始數據類型
Symbol
,表示獨一無二的值。它是 JavaScript 語言的第七種數據類型,前六種是:undefined
、null
、布爾值(Boolean)、字符串(String)、數值(Number)、對象(Object)
- Symbol的用法
let s = Symbol()
let ss = Symbol()
s===ss // false
// 這樣寫只是給Symbol的實例增加一個描述,爲了利於區分
let s = Symbol('foo')
let ss = Symbol('foo')
s===ss // false
注意:不能用new Symbol() ,因爲生成的 Symbol 是一個原始類型的值,不是對象,基本上,它是一種類似於字符串的數據類型。
let s = new Symbol()
// TypeError: Symbol is not a constructor
Symbol不能與其他類型的值進行運算
let s = Symbol('aaa')
'aaaaaaaaaaaa'+ s
// TypeError: Cannot convert a Symbol value to a string
`aaaaaaaaa${s}`
// TypeError: Cannot convert a Symbol value to a string
可以顯示的調用toString和String()進行轉換
String(s) // 'Symbol(aaa)'
s.toString() // 'Symbol(aaa)'
Symbol 值也可以轉爲布爾值,但是不能轉爲數值。
let s = Symbol();
Boolean(s) // true
!s // false
if (s) {
// ...
}
Number(s) // TypeError
s + 2 // TypeError
- Symbol用作屬性名
每一個 Symbol 值都是不相等的,這意味着 Symbol 值可以作爲標識符,用於對象的屬性名,就能保證不會出現同名的屬性
let mySymbol = Symbol();
// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
// 必須要[mySymbol]這樣寫,不加[] 就是以字符串mySymbol作爲屬性名
// 這也就以爲這不用通過點語法來獲取屬性值 a.mySymbol 這獲取的不是Symbol實例對應的值
let a = {
[mySymbol]: 'Hello!'
};
// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"
a.mySymbol // undefinde
const mySymbol = Symbol();
const a = {};
// 這是給字符串 mySymbol屬性賦值,而不是給Symbol賦值
a.mySymbol = 'Hello!';
a[mySymbol] // undefined 通過Symbol方式獲取
a['mySymbol'] // "Hello!"
- Symbol.for()
// 用 Symbol() 定義的值都是不一樣的
/*
有時,我們希望重新使用同一個 Symbol 值,Symbol.for方法可以做到這一點。
它接受一個字符串作爲參數,然後搜索有沒有以該參數作爲名稱的 Symbol 值。
如果有,就返回這個 Symbol 值,否則就新建並返回一個以該字符串爲名稱的 Symbol 值。
*/
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for()原理
Symbol.for()
會被登記在全局環境中供搜索,而Symbol() 不會。Symbol.for()
不會每次調用就返回一個新的 Symbol 類型的值,而是會先檢查給定的key
是否已經存在,如果不存在纔會新建一個值
ES6 還提供了 11 個內置的 Symbol 值,指向語言內部使用的方法。
- Symbol.hasInstance 當其他對象使用
instanceof
運算符,判斷是否爲該對象的實例時,會調用這個方法 - Symbol.isConcatSpreadable表示該對象用於
Array.prototype.concat()
時,是否可以展開,數組的默認行爲是可以true,僞數組默認是false - Symbol.species創建衍生對象時,會使用該屬性。
- Symbol.match當執行
str.match(myObject)
時,如果該屬性存在,會調用它,返回該方法的返回值。 - Symbol.replace 當該對象被
String.prototype.replace
方法調用時,會返回該方法的返回值 - Symbol.search當該對象被
String.prototype.search
方法調用時,會返回該方法的返回值。 - Symbol.split當該對象被
String.prototype.split
方法調用時,會返回該方法的返回值。 - Symbol.iterator指向該對象的默認遍歷器方法。
- Symbol.toPrimitive該對象被轉爲原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。
- Symbol.toStringTag在該對象上面調用
Object.prototype.toString
方法時,如果這個屬性存在,它的返回值會出現在toString
方法返回的字符串之中,表示對象的類型 - Symbol.unscopables該對象指定了使用
with
關鍵字時,哪些屬性會被with
環境排除。
Set
ES6 提供了新的數據結構 Set。它類似於數組,但是成員的值都是唯一的,沒有重複的值。
Set
本身是一個構造函數,用來生成 Set 數據結構。
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
Set
函數可以接受一個數組(或者具有 iterable 接口的其他數據結構)作爲參數,用來初始化。
// 例一
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
// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 類似於
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
一種去除數組重複成員的方法。
// 去除數組的重複成員
[...new Set(array)]
上面的方法也可以用於,去除字符串裏面的重複字符。
[...new Set('ababbc')].join('')
// "abc"
注意:Set 內部判斷兩個值是否不同,使用的算法叫做“Same-value-zero equality”,它類似於精確相等運算符(
===
),主要的區別是NaN
等於自身,而精確相等運算符認爲NaN
不等於自身。兩個對象總是不相等的
Set 結構的實例有以下屬性。
Set.prototype.constructor
:構造函數,默認就是Set
函數。Set.prototype.size
:返回Set
實例的成員總數。
Set 實例的方法分爲兩大類:操作方法(用於操作數據)和遍歷方法(用於遍歷成員)。下面先介紹四個操作方法。
add(value)
:添加某個值,返回 Set 結構本身。delete(value)
:刪除某個值,返回一個布爾值,表示刪除是否成功。has(value)
:返回一個布爾值,表示該值是否爲Set
的成員。clear()
:清除所有成員,沒有返回值。
Set 結構的實例有四個遍歷方法,可以用於遍歷成員。
keys()
:返回鍵名的遍歷器values()
:返回鍵值的遍歷器entries()
:返回鍵值對的遍歷器forEach()
:使用回調函數遍歷每個成員
需要特別指出的是,Set
的遍歷順序就是插入順序
由於 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以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"]
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
Map
ES6 提供了 Map 數據結構。它類似於對象,也是鍵值對的集合,但是“鍵”的範圍不限於字符串,各種類型的值(包括對象)都可以當作鍵
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
console.log(m) // Map { { p: 'Hello World' } => 'content' }
m.has(o) // true
m.delete(o) // true
m.has(o) // false
Map 也可以接受一個數組作爲參數。該數組的成員是一個個表示鍵值對的數組。
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"
console.log(map) // Map { 'name' => '張三', 'title' => 'Author' }
Set
和Map
都可以用來生成新的 Map。
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
Map 的鍵實際上是跟內存地址綁定的,只要內存地址不一樣,就視爲兩個鍵。這就解決了同名屬性碰撞(clash)的問題,我們擴展別人的庫的時候,如果使用對象作爲鍵名,就不用擔心自己的屬性與原作者的屬性同名。
如果 Map 的鍵是一個簡單類型的值(數字、字符串、布爾值),則只要兩個值嚴格相等,Map 將其視爲一個鍵,比如
0
和-0
就是一個鍵,布爾值true
和字符串true
則是兩個不同的鍵。另外,undefined
和null
也是兩個不同的鍵。雖然NaN
不嚴格相等於自身,但 Map 將其視爲同一個鍵。
Map的常用屬性方法
size
屬性返回 Map 結構的成員總數。new Map().size- set(key, value)設置鍵名
key
對應的鍵值爲value
,然後返回整個 Map 結構(因此可以鏈式寫法)。如果key
已經有值,則鍵值會被更新,否則就新生成該鍵 - get(key)方法讀取
key
對應的鍵值,如果找不到key
,返回undefined
- has(key)方法返回一個布爾值,表示某個鍵是否在當前 Map 對象之中
- delete(key)方法刪除某個鍵,返回
true
。如果刪除失敗,返回false
- clear()方法清除所有成員,沒有返回值
Map 結構原生提供三個遍歷器生成函數和一個遍歷方法。
keys()
:返回鍵名的遍歷器。values()
:返回鍵值的遍歷器。entries()
:返回所有成員的遍歷器。forEach()
:遍歷 Map 的所有成員。
需要特別注意的是,Map 的遍歷順序就是插入順序。用法和Set的基本上一樣
-
Map 轉爲數組
Map 轉爲數組最方便的方法,就是使用擴展運算符(
...
)。const myMap = new Map() .set(true, 7) .set({foo: 3}, ['abc']); [...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
-
數組 轉爲 Map
將數組傳入 Map 構造函數,就可以轉爲 Map。
new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { // true => 7, // Object {foo: 3} => ['abc'] // }
-
Map 轉爲對象
如果所有 Map 的鍵都是字符串,它可以無損地轉爲對象。
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 }
如果有非字符串的鍵名,那麼這個鍵名會被轉成字符串,再作爲對象的鍵名。
-
對象轉爲 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 轉爲 JSON
Map 轉爲 JSON 要區分兩種情況。一種情況是,Map 的鍵名都是字符串,這時可以選擇轉爲對象 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 的鍵名有非字符串,這時可以選擇轉爲數組 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 轉爲 Map
JSON 轉爲 Map,正常情況下,所有鍵名都是字符串。
function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false}
但是,有一種特殊情況,整個 JSON 就是一個數組,且每個數組成員本身,又是一個有兩個成員的數組。這時,它可以一一對應地轉爲 Map。這往往是 Map 轉爲數組 JSON 的逆操作。
function jsonToMap(jsonStr) { return new Map(JSON.parse(jsonStr)); } jsonToMap('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']}
模塊化語法
模塊功能主要由兩個命令構成:export和import。export
命令用於規定模塊的對外接口,import
命令用於輸入其他模塊提供的功能。
導出export
一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果你希望外部能夠讀取模塊內部的某個變量,就必須使用export
關鍵字輸出該變量。
// 可以直接把export放在聲明變量的前面
export let name = 'zs'
// 推薦使用下面的方式
let name = 'zs'
let fn = function(){}
let obj = {}
// 使用大括號指定所要輸出的一組變量
export {name, fn , obj}
// 可以在導出變量的時候給變量進行重命名
export {name as nm ,fn as f , obj as o}
export
命令規定的是對外的接口,必須與模塊內部的變量建立一一對應關係。
// 報錯
export 1;
// 報錯
var m = 1;
export m;
// 第一種寫法直接輸出 1,第二種寫法通過變量m,還是直接輸出 1。1只是一個值,不是接口
以下的寫法是正確的,規定了對外的接口m
其他腳本可以通過這個接口,取到值1
。它們的實質是,在接口名與模塊內部變量之間,建立了一一對應的關係
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var n = 1;
export {n as m};
// 報錯
function f() {}
export f;
// 正確
export function f() {};
// 正確
function f() {}
export {f};
導入import
使用export
命令定義了模塊的對外接口以後,其他 JS 文件就可以通過import
命令加載這個模塊。
import
後面的from
指定模塊文件的位置,可以是相對路徑,也可以是絕對路徑,.js
後綴可以省略。如果只是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置
注意,import
命令具有提升效果,會提升到整個模塊的頭部,首先執行。
import {name,fn,obj} from './filepath'
// 導出的變量重命名之後,導入這裏需要用重命名後的對應名來接受
import {nm, f, o} from './filepath'
// 還可以直接在接受的時候進行重命名
import {name as nm, fn as f, obj as o} from './filepath'
import
命令輸入的變量都是隻讀的,因爲它的本質是輸入接口。也就是說,不允許在加載模塊的腳本里面,改寫接口。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
// 但是可以修改對象或者數組中的中
a.name ='zs' // 這樣是可以的
模塊的整體加載
除了指定加載某個輸出值,還可以使用整體加載,即用星號(*
)指定一個對象,所有輸出值都加載在這個對象上面。
import * as data from './filepath'
// 打印的這個data是一個對象,包含了導出的全部數據
console.log(data.name)
console.log(data.fn)
console.log(data.obj)
export default 命令
使用import
命令的時候,用戶需要知道所要加載的變量名或函數名,否則無法加載。爲了給用戶提供方便,就要用到export default
命令,爲模塊指定默認輸出。
每一個模塊中只允許有一個默認的導出對象
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
// 可以用任意名稱指向export-default.js輸出的方法,這時就不需要知道原模塊輸出的函數名
// 這時import命令後面,不使用大括號。
本質上,export default
就是輸出一個叫做default
的變量或方法,然後系統允許你爲它取任意名字。因此還可以寫成以下的這個寫法
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同於
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同於
// import foo from 'modules';
async和await
在說async和await之前,來回顧一下promise,我們直接用一個例子來回顧吧
例子說明:我們要等5個異步操作都完成後執行某些事和還有一個就是當其中一個異步完成就執行某些事
function timeOut(time){
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, time);
});
}
// 我想在5個異步操作全部完成的時候,去做某件事情。
let t1 = timeOut(1000)
let t2 = timeOut(1000)
let t3 = timeOut(1000)
let t4 = timeOut(1000)
let t5 = timeOut(1000)
t1.then(function(){
console.log("我是t1")
})
t2.then(function(){
console.log("我是t2")
})
t3.then(function(){
console.log("我是t3")
})
t4.then(function(){
console.log("我是t4")
})
t5.then(function(){
console.log("我是t5")
})
Promise.all([t1, t2, t3, t4, t5]).then(function(){
console.log("所有異步操作完成了");
})
Promise.race([t1, t2, t3, t4, t5]).then(function(){
console.log("有一個異步率先完成了");
})
// Promise對象有個方法,all方法
// 當所有的被傳入的promise全部完成的時候,纔會執行這個all的回調
// Promise對象有個方法,race方法
// 當被傳入的promise有一個(第一個)完成的時候,就會執行這個race的回調
接下來咱們進入正題
ES2017 標準引入了 async 函數,使得異步操作變得更加方便。
async 函數是什麼?一句話,它就是 Generator 函數的語法糖。(治癒Generator是什麼後續再討論)
基本用法
async
函數返回一個 Promise 對象,可以使用then
方法添加回調函數。當函數執行的時候,一旦遇到await
就會先返回,等到異步操作完成,再接着執行函數體內後面的語句。
示例:
function timeOut(time){
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve(123);
}, time);
});
}
// async await 這個兩個關鍵字 是 es7 中提供的
// 可以再將 Promise的寫法 進行簡化
// async 和 await 必然是同時出現 (有await 必須有async)
async function test(){
let num = await timeOut(1000);
console.log("異步代碼完成" + num);
}
console.log("異步代碼前")
test();
console.log("異步代碼後")
儘管用我們使用了async和await之後,在書寫的形式上沒有了回調函數,並且看起來像是同步操作一樣,但是異步仍舊是異步,當同步的執行完之後纔會執行異步的輸出,因此上面的輸出是 “…前”,“…後”,“…完成”
再來實現以下當5個異步完成後執行某些事
async function test(){
let t1 = await timeOut(1000);
let t2 = await timeOut(1000);
let t3 = await timeOut(1000);
let t4 = await timeOut(1000);
let t5 = await timeOut(1000);
console.log("異步代碼完成" + num);
}
test()
我們就可以寫成這樣了,每次執行到await的位置處的時候,都要等待當前的異步完成了纔會執行下一個異步,所以最後的那個異步完成的操作一定是在所有的異步完成後執行的
async
函數返回一個 Promise 對象。
async
函數內部return
語句返回的值,會成爲then
方法回調函數的參數。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
async
函數內部拋出錯誤,會導致返回的 Promise 對象變爲reject
狀態。拋出的錯誤對象會被catch
方法回調函數接收到。
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
async
函數返回的 Promise 對象,必須等到內部所有await
命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return
語句或者拋出錯誤。也就是說,只有async
函數內部的異步操作執行完,纔會執行then
方法指定的回調函數。
async function test() {
// 直接可以通過變量接受的方式來接受異步獲取的結果
let t1 = await timeOut(1000);
console.log(t1)
let t2 = await timeOut(1000);
console.log(t2)
let t3 = await timeOut(1000);
console.log(t3)
let t4 = await timeOut(1000);
console.log(t4)
let t5 = await timeOut(1000);
console.log(t5)
return "finsih"
}
test().then(v => console.log(v))
await命令
正常情況下, await
命令後面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值。
async function f() {
// 等同於
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
任何一個await
語句後面的 Promise 對象變爲reject
狀態,那麼整個async
函數都會中斷執行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
上面代碼中,第二個await
語句是不會執行的,因爲第一個await
語句狀態變成了reject
。
await
語句前面沒有return
,但是reject
方法的參數依然傳入了catch
方法的回調函數。這裏如果在await
前面加上return
,效果是一樣的。
如果想要在報錯之後,繼續執行後續的代碼,我們可以用try…catch 來捕獲錯誤,或者用.catch()
async function f() {
// 方法一
try {
await Promise.reject('出錯了');
} catch (error) {
}
// 方法二
await Promise.reject('出錯了').catch(err=>console.log(err))
// 這樣這句代碼就可以執行了
await Promise.resolve('hello world'); // 不會執行
}