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'); // 不会执行
}