ECMA Script6基础

280 阅读52分钟

ECMAScript 6 简介

ECMAScript是JavaScript的规格,JavaScript是ECMAScript的一种实现。

ES6即是一个名词,也是一个泛指,含义是5.1版本以后的JavaScript的下一代标准,涵盖了ES2015,ES2016,ES2017等。

第一章 let和const命令

1. let命令

块级作用域:let命令用于声明变量,所声明的变量只在let命令所在的代码块内有效

for循环中的变量作用域包括:设置循环变量的父作用域和循环体内的子作用域

暂时性死区:let声明的变量不存在变量提升,必须在声明后使用变量,否则会报错

var声明的变量会发生变量提升,变量在声明前可用,值为undefined

只要在块级作用域内存在let命令,它所声明的变量就绑定了这个区域,不再受外界的影响。ES6规定,如果在区块中存在let和const命令,则这个区块对这些命令声明的变量从一开始就形成了封闭作用域。只要在声明前就使用这些变量,就会报错。

不允许重复声明:let不允许在相同的作用域内重复声明同一个变量

2. const命令

const声明一个只读的常量,一旦声明,常量的值就不能改变。

const实际上保证的并不是变量的值不得改动,而是变量指向的内存地址不得改动。

如果变量是基本类型不能改变值,如果是引用类型不能改变引用地址

块级作用域:const声明的常量不存在变量提升,只在声明所在的块级作用域内有效。

不允许重复声明:const声明常量时不允许重复声明。

暂时性死区:const声明的常量具有暂时性死区,只能在声明后使用。

3. 块级作用域

ES5只有全局作用域和函数作用域,没有块级作用域,导致了很多不合理的场景出现。

  1. 内层变量覆盖外层变量
  2. 用来计数的循环变量污染全局,导致内存泄漏

ES6新增块级作用域,解决了以上问题。

  1. 块级作用域允许任意嵌套
  2. 外层作用域不能读取内层作用域的变量
  3. 内层作用域可以定义外层作用域的同名变量

ES6允许在块级作用域内声明函数,函数声明语句的行为类似于let,在块级作用域之外不可引用。

4. 声明变量的6种方式

ES6一共有6种声明变量的方式:var命令、function命令、let命令、const命令、import命令、class命令

var和function:声明的变量是全局变量,会成为顶层对象的属性

let、class和const:声明的变量不是全局变量,不会成为顶层对象的属性

5. var、let和const对比

varletconst
声明变量声明变量声明常量
全局变量局部变量局部常量
window.变量名访问变量名访问变量名访问
存在变量提升不存在变量提升不存在变量提升
无暂时性死区有暂时性死区有暂时性死区
允许重复声明同一个变量不允许重复声明同一个变量不允许重复声明同一个常量
不具有块级作用域具有块级作用域具有块级作用域

第二章 变量的解构赋值

1. 数组的解构赋值

模式匹配:只要等号两边的模式相同,左边的变量就会被赋予对应的值

let [a, b, c] = ["a", "b", "c"]

let user = {}
[user.firstName, user.secondName] = 'Kobe Bryant'.split(' ') // Kobe Bryant

let [name, , title] = ['John', 'Jim', 'Sun', 'Moon'] // John Sun

let [name1, ...rest] = ["jack", "randy", "demi"]; // jack ["randy", "demi"]

解构不成功时,变量的值为undefined。

let [a,b] = [1] // a =1,b=undefined

只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。

let [one, two, three] = new Set([1, 2, 3]) // 1 2 3

数组解构赋值允许设置默认值。

ES6内部使用严格相等运算符(===)判断一个位置是否有值,如果一个数组成员不严格等于undefined,默认值是不会生效的。

let [name = "randy", surname = "demi"] = ["jack"]  // jack demi

let [x=1] = [null] // x=null

2. 对象解构赋值

对象的属性没有次序,变量必须与属性同名才能取到正确的值。

const { age, name } = { name: "randy", age: 24 }; // randy 24

对象解构赋值的内部机制是先找到同名属性,然后再赋值给对应的变量。

const { name: n, age: a } = { name: "randy", age: 24 }; // n:randy a:24

对象解构赋值允许设置默认值,默认值生效的条件是对象的属性值严格等于undefined。

const { name, age, sex = "male" } = user; // randy 24 male

解构失败,变量的值等于undefined。

let {foo} = {bar:123} // foo undefined

结构模式是嵌套对象,而且子对象所在的父属性不存在,那么将会报错。

// 嵌套对象,对于嵌套必须保证层级相同
const user2 = {
  name: "demi",
  age: 24,
  address: { province: "湖南", city: "岳阳", area: "汨罗" },
  likes: ["orange", "apple"],
};
const {
  name,
  age,
  address: { province, city, area },
  likes: [fruits1, fruits2],
} = user2;

3. 字符串的解构赋值

字符串是一个类似数组的对象,具有Iterator接口,同样可以进行解构赋值。

let str = 'randy'
let [a, b, c, d, e] = str // r a n d y

4. 变量解构赋值场景

  1. 变量值交换
let x = 1;
let y = 2;

[x,y] = [y,x]
  1. 从函数返回多个值
return {name, age,sex}
  1. 函数参数的定义
function fn([x,y,z]){}
  1. 提取JSON数据
let {id, data,src} = JSONData
  1. 函数参数默认值
function fn(x,y,z=15){}
  1. 遍历Map结构
for(let [key,value] of map){}
  1. 输入模块的指定方法
const {SourceMapConsumer,SourceNode} = require("source-map")

第三章 字符串的扩展

1. 字符的Unicode表示法

ES6加强了对Unicode的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点,这种表示法只限于码点在\u0000~\uFFFF之间的字符,超出这个范围的字符,必须用两个双字节的形式表示。

'\z' === 'z' // true 
'\172' === 'z' // true 
'\x7A' === 'z' // true 
'\u007A' === 'z' // true 
'\u{7A}' === 'z' // true

2. codePointAt()

javascript内部字符以UTF-16的格式存储,每个字符固定为2个字节。对于需要使用4个字节储存的字符,javascript会认为其是2个字符。

codePointAt方法的参数是字符在字符串中的位置(从0开始),返回值是十进制格式的Unicode码点。

可以利用codePointAt方法测试一个字符是2个字节还是4个字节

function is32Bit(c){
    return c.codePointAt(0) > 0xFFFF;
}

3. fromCodePoint()

将Unicode 码点翻译为对应字符,可以识别大于0xFFFF的字符。

fromCodePoint方法可以同时转换多个字符,并拼接返回结果。

String.fromCodePoint(value1, value2, ... value_n);

4. 字符的遍历器接口

ES6为字符串添加了遍历器接口Iterator,使其可以使用for...of循环遍历。

for...of循环遍历字符串时,可以正确识别大于0xFFFF码点的字符。

for(let i of 'str'){
   ... ...
}

5. includes、startsWith、endsWith方法

includes、startsWith、endsWith方法用来判断一个字符串是否包含在另一个字符串中。

includes():返回值为布尔值,表示是否找到了参数字符串

startsWith():返回值为布尔值,表示参数字符串是否在源字符串的头部

endsWith():返回值为布尔值,表示参数字符串是否在源字符串的尾部

以上3中方式均支持第二个可选参数,表示开始搜索的位置。

string.includes(searchElement[, fromIndex]);

string.startsWith(searchvalue, start);

string.endsWith(searchString[, length]);

startsWith方法对于字符串的大小写敏感。

endsWith方法的第二个参数与其他两个方法不同,其代表前n个字符,而其他两个方法表示从第n个位置开始到最后结束为止之间的字符。

6. repeat()

repeat方法返回一个新的字符串,表示将原字符串重复n次。

string.repeat(count)

如果参数是小数,会被Math.floor向下取整。

'na'.repeat(2.9) // 'nana'

参数为NaN则相当于0。

'na'.repeat(NaN) // ''

7. padStart与padEnd方法

padStart方法用于字符串的头部补全,padEnd方法用于字符串的尾部补全。

'x'.padStart(5,'ab) // 'ababx'
'x'.padEnd(5,'ab) // 'xabab'

第一个参数用来指定字符串的最小长度,第二个参数用来指定补全的字符串。

  • 如果原字符串长度等于或者大于指定的最小长度,则返回原字符串
  • 如果补全的字符串与原字符串的长度之和超过了最小长度,则会截取超出位数的补全字符串
  • 如果省略第二个参数,则会用空格补全

8. 模版字符串

模版字符串是增强版的字符串,用反引号标识。其可以用作普通字符串,也可以用作多行字符串,同时也支持在字符串中嵌入变量。

`hello:${name}`

`
${<div>111</div>}
${<div>222</div>}
`

第四章 数值的扩展

1. 二进制和八进制表示法

ES6 提供了二进制、八进制、十六进制数值的新的写法,分别用前缀0b(或0B)、0o(或0O)、0x(或0X) 表示

const a = 0b0101;
console.log(a); // 5

const b = 0o123;
console.log(b); // 83

const c = 0x123;
console.log(c); // 291

2. Number.isFinite()、Number.isNaN()

Number.isFinite方法用来检查一个数值是否为有限的,Number.isNaN方法用来检查一个值是否为NaN。

Number.isFinite(15) // true
Number.isFinite(0.8) // true
Number.isFinite(NaN) // false

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('a') // false

与传统的全局方法isFinite()和isNaN()相比,传统方法先调用Number()将非数值转为数值,再进行判断,而新方法只对数值有效,对于非数值一律返回false。Number.isNaN()只有对于NaN才会返回true,非NaN一律返回false。

isFinite('25') // true
Number.isFinite('25') // false

isNaN('NaN') // true
Number.isNaN('NaN') // false

3. Number.parseInt()、Number.parseFloat()

ES6将全局方法parseInt()和parseFloat()移植到了Number对象上,行为保持不变。

// 二进制 => 十进制
const b = 101
Number.parseInt(b, 2) // 5

// 八进制 => 十进制
const b = 101
Number.parseInt(b, 8) // 65

// 十六进制 => 十进制
const d = 101
Number.parseInt(d, 16) // 257

Number.parseFloat("10") + "<br>") // 10

4. Number.isInteger()

用来判断一个值是否为整数,需要注意的是,在javascript内部整数和浮点数都是同样的存储方法,所以3和3.0被视为同一个值。

Number.isInteger(25) // true
Number.isInteger(25.1) // false

5. Number.EPSILON

ES6在Number对象上新增了一个极小的常量,代表一个可以接受的误差范围。

function withinErrorMargin(left,right){
    return Math.abs(left - right) < Number.EPSILON
}

6. Number.isSafeInteger()

javascript能够精确表示的整数范围在-2^53到2^53之间,且不包含两个端点,超过这个范围就无法精确表示了。

ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER表示以上范围的两个端点值。

Number.isSafeInteger方法用来判断一个整数是否落在这个安全范围内。

Number.isSafeInteger(2) // true
Number.isSafeInteger(2.5) // false

第五章 Math对象的扩展

1. Math.trunc()

用于去除一个数的小数部分,返回整数部分

Math.trunc(8.76); // 8

对于非数值,Math.trunc内部使用Number方法将其转为数值后再处理。

Math.trunc('8.76'); // 8

对于空值和无法截取整数的值,返回NaN。

Math.trunc('foo'); // NaN

2. Math.sign()

用来判断一个数到底是正数、负数、还是零。

对于非数值,会先将其转换为数值再处理。

var a = Math.sign(3);    // 1
var b = Math.sign(-3);   // -1
var c = Math.sign(0);    // 0
  • 参数为正数,返回+1
  • 参数为负数,返回-1
  • 参数为 0,返回0
  • 参数为-0,返回-0
  • 其他值,返回NaN

3. Math.cbrt()

用于计算一个数的立方根,对于非数值,Math.cbrt方法内部先使用Number将其转化为数值后再处理。

Math.cbrt(-1) // -1
Math.cbrt(8) // 2

第六章 函数的扩展

1. 参数默认值

ES6允许函数的参数设置默认值,函数的参数是默认声明的,所以不能使用let或者const再次声明。

function multiply(a, b = 1) {
  return a * b;
}
console.log(multiply(5, 2)); // Expected output: 10
console.log(multiply(5)); // Expected output: 5

函数参数的默认值可以同解构赋值结合使用

function m1({x=0,y=0} = {}){
    return [x,y]
}

function m2({x,y} = {x:0,y:0}){
    return [x,y]
}

m1({x:3}) // [3,0]
m2({x:3}) // [3,undefined]

函数默认参数应该在函数的尾部。

function m1(x,y,z=1){}

函数的length属性将返回没有指定默认值的参数个数,如果指定了默认值,则length属性将不会包含此参数。

rest参数不会计入length属性。

设置默认值的参数后面的参数不会计入length属性。

function (a,b=0,c){} // length 1

如果设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域就会消失。这种语法行为在不设置参数默认值时不会出现。

参数默认值不是在定义时执行,而是在运行时执行。

let x = 1;

function(y = x){ // 变量y创建作用域,不能从内层作用域获取值,只能向外请求获取值
    let x = 2;
    console.log(y);
}

f() // 1

2. rest参数

...变量名,必须放到参数列表的末尾,用于获取函数的多余参数,返回值为一个数组。

// rest参数
function sortRestArgs(...theArgs) {
  var sortedArgs = theArgs.sort();
  return sortedArgs;
}

console.log(sortRestArgs(5, 3, 7, 1)); // 1, 3, 5, 7

// arguments参数
function sortArguments() {
  //arguments是参数对象不能直接使用sort()方法,因为不是数组实例,需要转换
  var sortedArgs = arguments.sort(); 
  return sortedArgs; // this will never happen
}
 
// 会抛出类型异常,arguments.sort不是函数
console.log(sortArguments(5, 3, 7, 1));

3. name属性

函数的name属性返回该函数的函数名。

var f = function foo(){};
f.name // 'foo';

(new Function).name // 'anonymous'

foo.bind({}).name // 'bound foo'

4. 箭头函数

() => {}

箭头函数允许传递多个参数,内部没有arguments,可以使用rest获取多余参数

let hello = () => {}
let hello2 = name => {}
let hello3 = name => name

箭头函数注意事项:

  • 函数体内的this对象就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数
  • 不可以使用call、apply、bind改变this指向
  • 不可以使用arguments对象
  • 不可以使用yield命令
var obj = {
    name: '小明',
    fn1: ()=>{
        setTimeOut(()=>{
            console.log(this.name)
        },100)
    },
    fn2: function(){
        console.log(this.name)
    },
}
obj.fn1(); // this指向window,undefined
obj.fn2(); // this指向obj, 小明

箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this,从而可以让this指向固定化。

5. 尾调用优化

尾调用是函数式编程的一个重要的概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步调用是调用另外一个函数。

函数调用会在内存中形成一个调用记录,又称调用帧,保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧。等到B运行结束,将结果返回给A,B的调用帧才会消失。如果函数B内部还调用函数C,那么就会生成一个C的调用帧,以此类推,所有的调用帧就形成一个调用栈。

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,所以直接用内层函数的调用帧取代外层函数的即可。

尾调用优化只保留内层函数的调用帧,节省了内存空间。

function f(){
    let m = 1;
    let n = 2;
    return g(m+n)
}
// 等同于
function f(){
    return g(3)
}
// 等同于
g(3)

6. 尾递归

函数调用自身成为递归,如果尾调用自身就称为尾递归。

尾递归可以实现同时只有一个函数调用帧,防止栈溢出问题。

function Fib(n,a=1,b=1){
    if(n<=1)return b;
    return Fib(n-1,b,a+b)
}

第七章 数组的扩展

1. 扩展运算符

扩展运算符是三个点(...),类似于rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。

console.log(...[1,2,3]) // 1 2 3

let foo = { ...['a', 'b', 'c'] };
foo // {0: "a", 1: "b", 2: "c"}

...扩展运算符可以代替数组的apply方法

Math.max.apply(null,[1,2,3]);
Math.max(...[1,2,3]);

合并数组

[1,2,3,...more]

字符串转数组,同时可以正确识别32位的Unicode字符

[...'hello'] // ['h','e','l','l','o']

任何Iterator接口的对象都可以用扩展运算符转换为数组。

但是类数组对象没有部署Iterator接口,只能使用Array.from方法转为数组。

2. Array.from()

将伪数组转成真数组,例如函数中的 arguments、DOM中的 NodeList等看上去都像数组却不能直接使用数组的 API,因为它们是伪数组(Array-Like),这时我们就需要将其转换成真正的数组。

伪数组包括类似数组的对象(array-like,object)和可遍历iterable对象(Map和Set)。

类数组对象的本质就是必须具备length属性。

let args = Array.from(arguments); 
let imgs = Array.from(document.querySelectorAll('img'));

Array.from(arrayLike[, mapFn[, thisArg]])

  • arrayLike:想要转换成数组的伪数组对象或可迭代对象
  • mapFn:如果指定了该参数,新数组中的每个元素会执行该回调函数,类似于map方法
  • thisArg:可选参数,执行回调函数mapFn时this对象
// 长度为5,值为1的数组
Array.from({
    length: 5
}, function() {
    return 1
}

3. Array.of()

用于将一组值转化为数组,如果没有参数,就返回空数组。

Array.of(7); // [ , , , , , , ] 
Array.of(1, 2, 3); // [1, 2, 3]

4. Array.copyWithin()

在当前数组内部将指定位置的元素复制到其他位置(会覆盖原有成员),然后返回当前数组

arr.copyWithin(target, start = 0, end = this.length)
let arr = [1, 2, 3, 4, 5] 

console.log(arr.copyWithin(1, 3))// [1, 4, 5, 4, 5] 
console.log(arr)// [1, 4, 5, 4, 5] 会改变原数组

接收3个参数:

  • target:从该位置开始替换
  • start:从该位置开始读取数据
  • end:到该位置前停止读取数据

5. Array.find()、Array.findIndex()

find方法用于找出第一个符合条件的数组成员,如果没有符合条件的数组成员,则返回undefined。

findIndex方法用于找出第一个符合条件的数组成员的下标,没有符合条件的数组成员,返回-1。

两个方法都接受第二个参数,用来绑定回调函数的this对象。

array.find(function(currentValue, index, arr),thisValue);
array.findIndex(function(currentValue, index, arr), thisValue)

find和findIndex方法均可以正确识别NaN,弥补了indexOf的不足。

let array = [5, 12, 8, 130, 44];  
let found = array.find(function(element) {     
    return element > 10; 
});

let index = array.findIndex(function(item) {
    return item >= 18;
})
console.log(found);// 12 返回的是值
console.log(found);// 3 返回的是下标

6. Array.fill()

用一个固定值填充一个数组,从起始索引到终止索引内的全部元素,不包括终止索引

fill(填充值, startIndex, endIndex);

let array = [1, 2, 3, 4] 
array.fill(0, 1, 3) 
console.log(array) // [1,0,0,4]

7. Array.keys()、values()、entries()

ES6提供了3个新的方法遍历数组,它们都会返回一个遍历器对象,可以用for...of循环遍历。

  • keys():对键名的遍历
  • values():对键值的遍历
  • entries():对键值对的遍历
const arr = ['a', 'b', 'c']

console.log(arr.keys()) // [0, 1, 2]
console.log(arr.values()) // a b c
console.log(arr.entries()) // [[0, 'a'], [1, 'b'], [2, 'c']]

8. Array.includes()

用于判断数组是否包含某个元素,返回一个布尔值。接收的第二个参数表示搜索的起始位置,默认为0。

includes方法可以正确识别NaN

[NaN].includes(NaN) // true

9. 数组的空位

数组的空位指数组的某一个位置没有任何值。

Array(3)// [,,,]

空位不是undefined,一个位置的值等于undefined,也代表有值,空位是没有任何值的,in运算符可以说明这一点。

0 in [,,,] // false
  • Array.from、keys、values、entries、find、findIndex方法会将数组的空位转为undefined
  • 扩展运算符会将数组的空位转为undefined
  • copyWithin方法会将空位一起复制
  • fill方法将空位视为正常的数组位置
  • for...of循环也会遍历空位

第八章 对象的扩展

1. Object.is()

判断两个值是否相等,对于基本数据类型比较的是值,引用数据类型判断的是内存地址是否一样。

一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的

// ===
console.log(0 === -0); // true
console.log(NaN === NaN); // false

// Object.is 
console.log(Object.is(0, -0)); // false
console.log(Object.is(NaN, NaN)); // true

2. Object.assign()

将原对象所有可枚举的属性复制到目标对象。

第一个参数是目标对象,后面参数都是原对象。如果目标对象与原对象有同名属性,后面的会覆盖前面的。

const obj = {
    name: '11',
    age:12
}
const newObj1 = Object.assign({},obj);
const newObj2 = Object.assign({name: '12'},obj);
newObj1 === newObj2 // false

如果只有一个参数,Object.assign会直接返回该参数,类似于浅拷贝。

Object.assign(obj) === obj // true

如果参数不是对象,会先将其转换为对象,然后返回。null和undefined不能作为参数,否则会报错。

Object.assign(2) // Number {2}

3. 属性的遍历

for...in循环:遍历对象自身的和继承的可枚举属性(不包含Symbol属性)

Object.keys:遍历对象自身的可枚举属性(不包含Symbol属性、继承的可枚举属性)

Object.getOwnPropertyName:遍历对象自身的所有属性(不包含Symbol属性,包括不可枚举属性)

Object.getOwnPropertySymbols:遍历对象自身的所有symbol属性

Reflect.ownKeys:遍历对象自身的所有属性,包括Symbol属性和不可枚举属性

4. Object.setPrototypeOf()、Object.getPrototypeOf()

Object.setPrototypeOf()用来设置对象的prototype对象。

Object.getPrototypeOf()用来获取对象的prototype对象。

5. 新增遍历方法

Object.keys():获取对象属性名,不能获取Symbol属性、不能获取不可枚举属性、不能获取原型链属性

Object.values(): 获取对象属性值

Object.entries():获取key和value的数组

6. 对象的扩展运算符

对象的扩展运算符用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性分配到指定的对象上面。

解构赋值相当于浅复制,不会复制继承自原型对象的属性。

let {x,y,z} = {x:1,y:2,z:3}

合并两个对象

let obj = {...obj1,...obj2}

第九章 Symbol类型

1. Symbol概述

ES6引入了一种新的数据类型Symbol,表示独一无二的值。

目前JavaScript的数据类型一共有7中,Null、Undefined、Number、String、Boolean、Object、Symbol

Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时容易区分。

let s1 = Symbol('s1')

Symbol值可以作为标识符用于对象的属性名,保证不会出现同名的属性。

Symbol类型还可以用于定义一组常量,保证这组常量的值都是不想等的。

let s1 = Symbol('s1')
let s2 = Symbol('s2')
console.log(s1) // Symbol()
console.log(s2) // Symbol()
console.log(s1 === s2) // false

2. Symbol.for()、Symbol.keyFor()

Symbol.for()接收一个字符串参数,搜索有没有以该字符串作为名称的 Symbol 值。如果有就返回这个 Symbol 值,没有就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。前者会被登记在全局环境中供搜索,后者不会

let s1 = Symbol.for('foo')
let s2 = Symbol.for('foo')
console.log(s1 === s2) // true

Symbol.for()为Symbol值登记的名字是全局环境的,可以在不同的iframe或service worker中取到同一个值。

Symbol.keyFor()返回一个已经登记的Symbol类型值的key。只有使用Symbol.for()创建的symbol才会有key。

const s1 = Symbol('foo')
console.log(Symbol.keyFor(s1)) // undefined

const s2 = Symbol.for('foo')
console.log(Symbol.keyFor(s2)) // foo

3. 内置的Symbol值

  1. Symbol.hasInstance

对象使用instance运算符时回调用这个方法,判断对象是否为某个构造函数的实例。

  1. Symbol.isConcatSpreadable

表示对象使用Array.prototype.concat()时是否可以展开。

数组的默认行为时可以展开的,取值为true或者undefined,都有这个效果。

  1. Symbol.species

指向当前对象的构造函数,创造实例时默认会调用这个方法,会使用这个属性返回的函数当作构造函数实例。

  1. Symbol.match

当执行str.match()时,如果该属性存在,回调用它返回该方法的返回值。

  1. Symbol.replace

当执行str.replace()时,如果该属性存在,回调用它返回该方法的返回值。

  1. Symbol.search

当执行str.search()时,如果该属性存在,回调用它返回该方法的返回值。

  1. Symbol.split

当执行str.split()时,如果该属性存在,回调用它返回该方法的返回值。

  1. Symbol.iterator

指向该对象的默认遍历器方法。

第十章 Set和Map数据结构

1. Set

类似于数组,值是唯一的,没有重复的值。

可以接受一个数组或者具有iterable的其他数据结构作为参数,用来初始

let s = new Set() 
let s = new Set([1, 2, 3, 4])

Set 数据结构不允许数据重复,所以添加重复的数据是无效的

s.add('hello') 
s.add('goodbye') 
s.add('hello').add('goodbye')

Set数据结构添加值时不会发生类型转换,能准确区分NaN

s.add(5);
s.add('5');
s.add(NaN);
s.add(NaN);
s // 5 '5' NaN

删除指定数据

s.delete('hello') 

删除全部数据

s.clear()

统计数据has

s.has('hello')

总长度

s.size

遍历操作包括keys()、values()、entries()、forEach(),由于Set数据结构的实例默认可遍历,其默认的遍历器生成函数就是它的values方法。

Set遍历顺序就是插入顺序

  • keys()返回键名
  • values()返回键值
  • entries()返回键值对
  • forEach()使用回调函数遍历每个成员
  • for…of可以直接遍历每个成员

数组去重

let arr = [1, 2, 3, 4, 2, 3] 
let s = new Set(arr) 
console.log(s) // Set(4) {1, 2, 3, 4}

合并去重

let s = new Set([...arr1, ...arr2])

交集

let s2 = new Set(arr2) 
let result = new Set(arr1.filter(item => s2.has(item)))

差集

let arr3 = new Set(arr1.filter(item => !s2.has(item))) 
let arr4 = new Set(arr2.filter(item => !s1.has(item))) 
console.log([...arr3, ...arr4])

2. WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合

const ws = new WeakSet();

WeakSet接收一个数组或者类似数组的对象作为参数,实际上,任何具有iterator接口的对象都可以作为WeakSet的参数。

添加成员

ws.add(value);

删除成员

ws.delete(value);

判断是否包含成员

ws.has(value);
  • WeakSet 的成员只能是对象
  • WeakSet 没有size属性
  • WeakSet 中的对象都是弱引用
  • WeakSet不可遍历

即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存空间,不考虑该对象还存在于 WeakSet 之中。

WeakSet适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在WeakSet里面的引用就会自动消失。

3. Map

类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。

let map = new Map([['name', 'randy'], ['age', 27]])

只有对同一个对象的引用,Map结构才将其视为同一个键。Map的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。

+0和-0是同一个键,NaN是同一个键

map.set(['a'],5);
map.get(['a']); // undefined

set:添加数据,如果key已经存在则键值会被更新,否则就生成该键

set方法返回值为当前Map结构,可以采用链式调用

let keyObj = {} 
let keyFunc = function() {} 
let keyString = 'a string'  

map.set(keyString, "和键'a string'关联的值") 
map.set(keyObj, '和键keyObj关联的值') 
map.set(keyFunc, '和键keyFunc关联的值')

get:获取数据,key不存在时返回undefined

map.get(keyObj) // 和键keyObj关联的值

delete:删除某个键,删除成功返回true,删除失败返回false

map.delete(keyObj)

clear:删除所有键值,没有返回值

map.clear()

size:统计数据

map.size

has:判断是否包含某个键,返回布尔值

map.has(keyObj)

遍历方法包括:keys()、values()、entries()、forEach()

Map遍历的顺序就是插入顺序

Map结构的默认遍历器接口就是entries()方法。

  • keys() 返回一个新的 Iterator 对象,包含按照顺序插入 Map 对象中每个元素的 key 值
  • values() 方法返回一个新的 Iterator 对象,包含按顺序插入Map对象中每个元素的 value 值
  • entries() 方法返回一个新的包含 [key, value] 对的 Iterator 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同
  • forEach() 方法将会以插入顺序对 Map 对象中的每一个键值对执行一次参数中提供的回调函数
  • for…of 可以直接遍历每个成员

4. Map与Object对比

  • 键的类型

一个Object的键只能是字符串或者 Symbol,但一个 Map的键可以是任意值,包括函数、对象、基本类型

  • 键的顺序

Map 中的键值是有序的,而添加到Object中的键则不是,进行遍历时,Map对象是按插入的顺序返回键值

  • 键值对的统计

通过 size 属性获取Map的键值对个数,Object的键值对个数只能手动计算

  • 键值对的遍历

Map可直接进行迭代,而Object的迭代需要先获取它的键数组,然后再进行迭代

  • 性能

Map 在涉及频繁增删键值对的场景下会有些性能优势

5. WeekMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合

  • WeakMap只接受对象作为键名(null除外)
  • WeakMap 没有size属性
  • WeakMap的键名指向的对象都是弱引用
  • WeakMap不可以遍历

如果要像对象中添加数据又不想干扰垃圾回收机制,便可以使用weakMap结构。例如在网页的DOM元素上添加数据时就可以使用WeakMap结构,当DOM元素被清除时,其所对应的WeakMap记录会自动移除。

WeakMap弱引用指的是键名而不是键值,键值是正常引用

WeakMap的方法

  • set:添加数据
  • delete:删除数据
  • get:获取数据
  • has:验证是否包含某个数据

第十一章 Proxy

1. Proxy概述

Proxy可以修改某些操作的默认行为,等同于在语言层面作出修改,所以处于一种元编程,即对编程语言进行编程。

Proxy提供了一种机制,可以对外界的访问进行过滤和改写。

const proxy = new Proxy(target,handler)
  • target:需要代理的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
  • handler:一个以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

Proxy 直接代理了target整个对象,并且返回了一个新的对象,能监听到对象属性的增加、删除、修改等操作

2. Proxy实例的方法

  1. get()

用于拦截某个属性的读取操作,该方法可以继承。

如果一个属性不可配置(configurable)或者不可写(writable),则该属性不能被代理,通过代理Proxy对象访问该属性会报错。

  1. set()

用于拦截某个属性的赋值操作。

如果目标对象自身的某个属性不可配置(configurable)也不可写(writable),则set不得改变这个属性的值,只能返回同样的值,否则会报错。

  1. apply()

拦截函数的调用、call和apply操作,接收3个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

  1. has()

用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型操作是in操作符。

如果原对象不可配置或者禁止扩展,has拦截会报错。

  1. construct()

拦截new命令,返回值必须是一个对象,否则会报错。

  1. deleteProperty()

拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete删除。

  1. revocable()

返回一个对象,其proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。

Proxy.revocable的一个使用场景是目标对象不允许直接访问,必须通过代码访问,一旦访问结束,就收回代理权,不允许再次访问。

3. Proxy拦截使用

let o = {
    name: 'randy',
    age: 20
}
let handler = {
    get(obj, key) {
        return Reflect.has(obj, key) ? obj[key] : ''
    }
}
let p = new Proxy(o, handler)
console.log(p.from)
  1. 接口数据拦截
let data = new Proxy(response.data, {
    set(obj, key, value) {
        return false
    }
})
  1. 数据校验拦截
export const Validator =  (obj, key, value) => {
    if (Reflect.has(key) && value > 20) {
        obj[key] = value
    }
}
let data = new Proxy(response.data, {
    set: Validator
})

get(target, prop):拦截对象属性的读取,比如proxy.foo和proxy['foo']

// target是被代理对象,prop是键
let dict = {
    'hello': '你好',
    'world': '世界'
}
dict = new Proxy(dict, {
    get(target, prop) {
        return prop in target ? target[prop] : prop
    }
})

set(target, prop, val):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值

// target是被代理对象,prop是键,val是值
let arr = []
arr = new Proxy(arr, {
    set(target, prop, val) {
        if (typeof val === 'number') {
            target[prop] = val
            return true
        } else {
            return false
        }
    }
})
arr.push(5)
arr.push(6)

has(target, prop):拦截对象是否包含某个属性的判断,返回一个布尔值

// target是被代理对象,prop是键
let user = {
    name: 'randy',
    age: 27
}

user = new Proxy(user, {
    has(target, prop) {
      if(prop === 'name') {
          return true
      }
      return false
    }
})
console.log('name' in user) // true
console.log('age' in user) // false

ownKeys:拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环等循环操作,返回一个数组

let userinfo = {
    username: 'randy',
    age: 27,
    _password: '***'
}
userinfo = new Proxy(userinfo, {
    ownKeys(target) {
        return Object.keys(target).filter(key => !key.startsWith('_'))
    }
})

for (let key in userinfo) {
   console.log(key) // username age
}
console.log(Object.keys(userinfo)) // ['username', 'age']

deleteProperty:删除拦截对象某个属性,返回一个布尔值

user = new Proxy(user, {
    deleteProperty(target, prop) { // 拦截删除
        if (prop.startsWith('_')) {
            throw new Error('不可删除')
        } else {
            delete target[prop]
            return true
        }
    }
})

apply:拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)

let sum = (...args) => {
    let num = 0
    args.forEach(item => {
        num += item
    })
    return num
}

sum = new Proxy(sum, {
    apply(target, ctx, args) {
        return target(...args) * 2
    }
})

construct:拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

let User = class {
    constructor(name) {
        this.name = name
    }
}
User = new Proxy(User, {
    construct(target, args, newTarget) {
        console.log(target, args, newTarget)
        return new target(...args)
    }
})

4. Proxy中的this问题

在Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理。

大多数情况需要将this绑定到原始对象上。

target.getData.bind(target)

5. Proxy实现观察者模式

const person = {
    name:'小明',
    age:15
}

function say(){
    console.log('say:恭喜你成年了');
}
        
function say1(){
    console.log('say1:恭喜你成年了');
}

function say2(){
    console.log('say2:恭喜你成年了');
}

const set = new Set([]);
set.add(say).add(say1).add(say2);

const pers = new Proxy(person,{
    set:function(target,key,value){
        if(key === 'age' && value >= 18){
            set.forEach(fn => fn());
        }
        return Reflect.set(target,key,value);
    }
});

第十二章 Reflect

1. Reflect概述

Reflect与Proxy类似,也是ES6为了操作对象而提供的新的API。

设计目标有以下几点:

  1. Reflect对象可以获取语言内部的方法
  2. Reflect可以修改某些Object方法的返回值,使其变得更合理
  3. 让Object操作编程函数行为
  4. Reflect对象的方法与Proxy对象的方法一一对应

2. 静态方法

  1. get(target,name,receiver)

查找并返回target对象的name属性,如果没有name属性,则返回undefined。

  1. set(target,name,value,receiver)

设置target对象的name属性等于value。该方法会触发Proxy.defineProperty拦截。

  1. has(target,name)

对应name in target中的in操作符。

  1. deletePrototype(target,name)

等同于delete target[name],用于删除对象属性。

该方法返回一个布尔值,如果删除成功或被删除的属性不存在,就返回true;否则返回false。

  1. construct(target,args)

等同于new target(...args)

第十三章 Promise

1. Promise概述

回调地狱即多层函数嵌套结构

function Fn(){
    function fn1(){
        function fn2(){
            function fn3(){
                ... ...
            }
        }
    }
}

promise是异步编程的一种解决方案,避免了层层嵌套的回调函数,比传统的解决方案更合理和强大。

Promise对象具有以下两个特点:

  1. 对象状态不受外界影响

Promise对象代表一个异步操作,有3中状态,Pending进行中、Fulfilled已成功、Rejected已失败。

  1. 状态改变后无法再次更改

一旦状态改变就不会再变,任何时候都可以拿到这个结果。状态的改变有两种可能,一种是从Pending到Fulfilled,一种是从Pending到Rejected。

Promise对象的缺点如下:

  1. 一旦创建就会立即执行,中途无法取消
  2. 如果不设置回调函数,Promise内部抛出的错误无法捕获
  3. 当处于Pending状态时,无法得知目前进展到哪一个阶段

2. 基本用法

  1. resolve()

在异步操作成功时调用,并将异步操作的结果,作为参数传递出去(Promise.resolve(num))。

resolve函数的参数如果是另外一个Promise实例,则后面的执行会等待两外一个Promise实例的状态改变后执行。

function Fn(){
    return new Promise((resolve, reject) => {
        // 异步操作,如请求后段接口等
        resolve(res)
    });
}

resolve函数还可以将对象转为Promise对象

Promise.resolve('foo');
  1. reject()

在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去(Promise.reject('操作失败'))

function Fn(){
    return new Promise((resolve, reject) => {
        // 异步操作,如请求后段接口等
        reject(res)
    });
}

reject函数还可以将对象转为Promise对象

Promise.reject('foo');

resolve和reject不会终结Promise的参数函数的执行

  1. then函数

then函数接收两个回调函数作为参数,第一个回调函数是Promise对象状态变为Resolved时调用,第二个回调函数是Promise对象状态变为Rejected时调用。

then方法会返回一个promise对象,可以继续调用.then()。

Fn().then(res=>{},err=>{}).then(res=>{},err=>{})....
  • then 方法必须返回一个 promise 对象
  • 如果then方法中没有return语句,就返回一个用undefined包装的Promise对象
  • 如果then方法中出现异常,则会流转到下一个then的onRejected或者最末尾的catch方法中
  • 如果then方法没有传入任何回调,则继续向下传递(即所谓的值穿透)
  1. catch()

Promise为链式调用,实际使用过程中可能会有很多的then方法,所以如果把错误处理都写在then方法中的话就需要在每个then中对错误进行处理了。而使用catch方法的话,只需要在链式调用的最末尾加上一个catch方法就可以了

  • 可以捕获reject传递的参数
  • 可以捕获resolve回调中发生的错误
Fn().then().catch(err=>{});
  1. all()

Promise.all(iterable) 参数为一组 Promise 实例组成的数组(或者是具有iterator接口的数据结构),用于将多个Promise实例包装成一个新的 Promise实例。

当数组中的Promise实例都为Resolved 的时候,Promise.all() 的状态才会返回Resolved,否则返回Rejected。并且Rejected是第一个被Rejected 的 Promise 的返回值。

Promise.all([
    postAction('url',data),
    getAction('url'),
    getAction('url'),
]).then(resolve => {
    console.log(resolve)
}).catch(err => {
    console.log(err)
}

手动封装Promise.all方法

Promise.myPromiseAll = function (pmarr) {
    return new Promise((resolve, reject) => {
        try {
            if (!pmarr.length) resolve([]);
            let length = 0; // 记录迭代器的个数
            let resultLength = 0; // 记录已执行的promise个数
            let result = []; // 用于存储返回值
            for (const key in pmarr) {
                length++;
                Promise.resolve(pmarr[key]).then((res) => {
                    result[key] = res;
                    resultLength++;
                    if (length === resultLength) resolve(result);
                },err=>{
                    reject(err)
                });
            }
        } catch (error) {
            reject(error);
        }
    });
};

Promise.myPromiseAll([1, 2, 3, Promise.reject(456)]).then(
    (res) => {console.log(res)},
    (err) => {console.log("error", err)}
);
// error 456
  1. race()

Promise.race方法与Promise.all方法不同,race方法中只要对象中有一个状态改变了,它的状态就跟着改变,并将那个改变状态实例的返回值传递给回调函数,也就是不管失败与成功,第一个Promise的返回值就是race方法的返回值,resolve就会进入then方法,否则进入catch方法

Promise.race([
    postAction('url',data),
    getAction('url'),
    getAction('url'),
]).then(resolve => {
    console.log(resolve)
}).catch(err => {
    console.log(err)
}
  1. finally()

不管Promise对象最后状态如何都会执行的操作,接收一个普通函数作为参数,该函数不管怎样都会执行。

Promise.race([
    postAction('url',data),
    getAction('url'),
    getAction('url'),
]).then(resolve => {
    console.log(resolve)
}).catch(err => {
    console.log(err)
}.finally(()=>{
})
  1. allSettled()

Promise.all() 具有并发执行异步任务的能力。但它的最大问题就是如果其中某个任务出现异常reject,所有任务都会挂掉,Promise直接进入 reject 状态。Promise.allSettled()方法就是不管是否成功失败,都会返回结果并进入then方法。

  1. any()

Promise.any()和Promise.race()类似都是返回第一个结果,但是Promise.any()只返回第一个成功的,尽管某个 promise 的 reject 早于另一个 promise 的 resolve,仍将返回那个首先 resolve 的 promise。

3. Promise应用

  1. axios

axios是基于promise封装的ajax请求库

  1. fetch

fetch是新浏览器自带的,基于promise封装的ajax请求库。异步回调中抛错catch捕捉不到,使用try catch进行处理,catch到错误后再手动reject

function fun17() {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      try {
        throw new Error("这是一个错误");
      } catch (err) {
        reject(err);
      }
    }, 1000);
  });
}

4. Promise链式调用

var p1 = new Promise((resolve,reject)=>{
    console.log('p1开始');
    resolve(p2);
    console.log('p1结束');
})

var p2 = new Promise((resolve,reject)=>{
    console.log('p2开始');
    resolve(p3);
    console.log('p2结束');
});

var p3 = new Promise((resolve,reject)=>{
    console.log('p3开始');
    resolve(p4);
    console.log('p3结束');
});

var p4 = new Promise((resolve,reject)=>{
    console.log('p4开始');
    resolve();
    console.log('p4结束');
});

p1.then().then().then();

image.png

var p1 = new Promise((resolve,reject)=>{
    console.log('p1开始');
    resolve();
    console.log('p1结束');
})

var p2 = new Promise((resolve,reject)=>{
    console.log('p2开始');
    resolve();
    console.log('p2结束');
});

var p3 = new Promise((resolve,reject)=>{
    console.log('p3开始');
    resolve();
    console.log('p3结束');
});

var p4 = new Promise((resolve,reject)=>{
    console.log('p4开始');
    resolve();
    console.log('p4结束');
});

p1.then(()=>{
    return p2;
}).then(()=>{
    return p3;
}).then(()=>{
    return p4;
});

image.png

5. Promise实现红路灯

const step = () => {
    return Promise.resolve()
        .then(() => {
             return new Promise((resolve, reject) => {
                setTimeout(() => {
                  console.log("red");
                  resolve();
                }, 3000);
             });
         })
         .then(() => {
              return new Promise((resolve, reject) => {
                setTimeout(() => {
                  console.log("yellow");
                  resolve();
                }, 2000);
              });
         })
         .then(() => {
              return new Promise((resolve, reject) => {
                setTimeout(() => {
                  console.log("green");
                  resolve();
                }, 1000);
              });
         })
         .then(() => {
              step();
         });
};

第十四章 Iterator和for...of

1. Iterator概念

遍历器是一个接口,为各种不同的数据结构提供统一的访问机制。任何数据结构,只要部署了Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator的3个作用:

  1. 为各种数据结构提供统一的访问接口
  2. 使得数据结构的成员能够按某种次序排列
  3. iterator主要供for...of消费

Iterator的遍历过程如下:

  1. 创建一个指针对象,指向当前数据结构的起始位置
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
  3. 不断调用指针对象的next方法,直到它指向数据结构的结束位置

Iterator的next方法返回值包含value和done两个属性,其中value属性是当前成员的值,done属性表示遍历是否结束。

2. 默认的Iterator接口

数据结构只要部署了Iterator接口,我们就成这种数据结构为可遍历的。

ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性。

原生具备Iterator接口的数据结构包括:Array、Map、Set、String、TypedArray、函数的arguments对象、NodeList对象。

对于原生部署Iterator接口的数据结构,我们不用自己编写遍历器生成函数,for...of循环会自动遍历它们。对于其他的数据结构,可以通过在Symbol.iterator部署Iterator接口实现for...of循环。

const arr = [1, 2, 3, 4, 5];
const iter = arr[Symbol.iterator]();
console.log(iter.next()); // {value:1,done:false}
console.log(iter.next()); // {value:2,done:false}
console.log(iter.next()); // {value:3,done:false}
console.log(iter.next()); // {value:4,done:false}
console.log(iter.next()); // {value:5,done:false}
console.log(iter.next()); // {value:undefined,done:true}

3. 调用Iterator接口的场合

  1. 解构赋值

对于数组和Set解构进行解构赋值时会默认调用Symbol.iterator方法

  1. 扩展运算符

只要数据结构部署了Iterator接口,就可以使用扩展运算符将其转换为数组

  1. yield*

yield*后面跟的是一个可遍历结构,它会调用该结构的遍历器接口

  1. 数组的遍历

for...of循环,Array.from(),Map()、Set()、WeakMap()、WeakSet(),Promise.all(),Promise.race()

4. 遍历器对象的return()

一般在for...of循环提前退出、一个对象在完成遍历前需要清理或释放资源等,都可以使用return方法。

for (let a of arr) {
    if (a >= 4) {
        break;
    }
    console.log(a);
}

5. for...of循环

for...of循环内部调用的是数据结构的Symbol.iterator方法,for...of循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象(arguments对象、DOM NodeList对象)、Generator对象、字符串。

  1. 数组

for...of循环调用遍历器接口时,只返回数组的数字索引的属性

const arr = [1, 2, 3, 4, 5];
arr[name] = '111';

for (let a of arr) {
  console.log(a);
} // 1 2 3 4 5
  1. 对象

对于普通的对象,可以采用for...in循环遍历键名,或者使用Object.keys方法将对象键名包装成一个数组,然后用for...of循环遍历数组

for (let key of Object.keys(obj)) {}
  1. forEach循环无法中途退出

forEach循环执行中,不能使用break或者return命令中途退出,for...of循环弥补了这一缺点,其可以结合break、continue和return使得循环提前结束。

arr.forEach((item) => {
    if (item === 4) {
        return;
    }
    console.log(item);
}); // 1 2 3 5

第十五章 async函数

1. async使用

ES2017引入了async函数,简单理解async/await是Generator函数的语法糖。

async函数对于Generator函数做了以下4个改进:

  1. 内置执行器
  2. 优化语义描述
  3. 扩展适应性
  4. 直接返回Promise对象
function timeOut(){
 return new Promise((resolve,reject)=>{
    setTimeout(()=>{
        console.log('12345');
        resolve('6798');
    },3000)
 })
}

async function seting(){
  console.log('0');
  const res = await timeOut();
  console.log(res); // 这一行代码类似于promise.then中的微任务
}
seting();
// 0
// 12345
// 6789

2. async语法

async函数会返回一个Promise对象,async函数内部return语句返回值,回成为then方法中回调函数的参数。

async函数返回的Promise对象必须等到内部所有await语句后面的Promise对象执行完才会发生状态改变,除非遇到return语句或者抛出错误。只有当async函数内部的异步任务执行完毕,才回到用then方法的回调函数。

正常情况下await后面是一个Promise对象,如果不是,会被转为一个立即resolve的Promise对象。

如果希望前一个异步任务操作失败时后一个异步任务正常进行,则可以将其包装在一个try...catch语句中,或者为Promise对象添加一个catch方法

async function() {
  try{
    const result = await getData()
  } catch(e) {
    console.log(e)
  }
}

多个异步任务并发执行

Promise.all([p1,p2,p3]);

const r1 = await p1();
const r2 = await p2();
const r3 = await p3();

多个异步任务继发执行

for(let p of [p1,p2,p3]){
  await p();
}

3. 异步遍历

for await ... of可以用来遍历异步的Iterator接口。

function Gen(time) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(time)
        }, time)
    })
}

async function test() {
    let arr = [Gen(2000), Gen(100), Gen(3000)]
    for await (let a of arr) {
        console.log(Date.now(),a);
    }
}

test()
// 1560092345730 2000
// 1560092345730 100
// 1560092346336 3000

4. async、Promise、Generator对比

  1. async和Promise

async相较于Promise的链式调用,代码易于读懂。

await 会阻塞后面代码正常运行,即使后续代码不依赖前者,导致代码失去了并发性。

  1. async和Generator

async内置了执行器,Generator 函数的执行必须靠执行器完成。

async中的async 和 await,比起星号和 yield,语义更清楚了。

第十六章 Class

1. class简介

ES6引入class关键字定义类,class可以看做是function的语法糖。

class Animal {
    constructor(type) {
        this.type = type
    }
    walk() {
        console.log( I am walking )
    }
}
let dog = new Animal('dog')


let Animal = function(type) {
    this.type = type
}
Animal.prototype.walk = function() {
    console.log( `I am walking` )
}
let dog = new Animal('dog')

2. constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时自动调用该方法。一个类必须有constructor方法,如果没有定义,则自动添加一个空的默认方法。

class Animal {
    constructor() {}
}

constructor方法默认返回实例对象(即this),也可以指定返回另外一个对象

class Animal {
    constructor() {
        return [1,2,3]
    }
}

new Animal instanceof Animal // false

3. 无变量提升

class不存在变量提升

new Foo() // 报错

class Foo{}

4. 类成员声明

在类的最外层作用域里面声明成员

class Car {
  color = 'blue';
  age = 2;
}

const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2

5. 私有属性和方法

ES13提案中,可以在方法名或者属性名前添加一个#前缀,使其私有化,私有方法外界无法访问

class Person {
  #say() {
    console.log('say hello')
  }
}

const person = new Person();
console.log(person.#say);

#属性名

class Person {
  #firstName = 'randy';
  #lastName = 'su';
}

const person = new Person();
console.log(person.#firstName);
console.log(person.#lastName);

6. this指向

class内部的方法中如果包含this,this默认指向类的实例。如果外部想单独使用方法,则需要在构造函数中使用bind绑定this或者使用箭头函数。

class Person {
  constructor(){
    this.say = this.sat.bind(this);
  }
  say(){
    return this.name;
  }
}

7. Setters & Getters

对于类中的属性,可以直接在 constructor 中通过 this 直接定义。也可以利用Setters & Getters直接在类的顶层来定义。

Setters & Getters就是在方法名前加上get/set。

class Animal {
    constructor(type, age) {
        this.type = type
        this._age = age
    }
    get age() {
        return this._age
    }
    // 只读属性不用设置set,set可以限制对属性的修改
    set age(val) {
        this._age = val
    }
}

8. 静态属性和方法

static关键字声明的属性和方法为静态属性和方法。

静态属性和方法不会被实例继承,只能使用类来调用,不能使用实例调用。

class Animal {
  // 静态属性
  static age = 15;
  // 静态方法
  static eat() {
    console.log(`I am eating`);
  }
}
// 静态属性
Animal.age = 15;

9. 实例属性和方法

class Animal {
  // 实例属性
  age = 15// 实例方法
  walk() {
    console.log(`I am walking`);
  }
}

10. Class的继承

Class可以通过extends关键字实现继承

class Dog extends Animal {}

子类必须在constructor函数中调用super方法,否则实例会报错。因为子类没有自己的this对象,而是继承父类的this对象,然后对其加工。如果不调用super方法,子类就得不到this对象。

ES6的继承机制原理是先创建父类的实例对象this(必须先调用super),然后再用子类的构造函数修改this。

super关键字既可以用作函数,也可以当作对象。

当super作为函数使用时,相当于类.prototype.constructor.call(this),返回子类的实例,由于ES6规定,通过super调用父类方法时,super内部的this会指向子类。

当super当作对象时,在普通函数中指向父类的原型对象,在静态方法中指向父类


class Animal {
  constructor(type) {
    this.type = type;
  }
  walk() {
    console.log(`I am walking`);
  }
  static eat() {
    console.log(`I am eating`);
  }
}

class Dog extends Animal {
  constructor(type,name) {
    super(type);
    this.name = name;
  }
  run() {
    console.log("I can run");
  }
}

let dog = new Dog("dog");
console.log(dog.type); // dog
dog.run(); // I can run
dog.walk(); // I am walking

第十七章 Generator

1. 简介

Generator是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。

执行Generator函数会返回一个遍历器对象,遍历器对象可以依次遍历Generator函数内部的每一个状态。

function * helloWorldGenerator(){
   yield 'hello';
   yield 'world';
   return 'ending';
}
const iter = helloWorldGenerator();
console.log(iter.next()); // {value: 'hello', done: false}

每次调用next方法,内部指针就从函数头部或者上一次停留下来的位置开始执行,直到遇到下一条yield语句为止。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。

Generator 函数会根据yield个数,将原来的函数切割成多个小且独立的函数,每次调用next只执行一个小函数。

2. yield表达式

遍历器对象的next方法执行逻辑如下:

  1. 遇到yield语句就暂停执行后面的操作,并将紧跟在yield后的表达式的值作为返回的对象的value属性值
  2. 下一次调用next方法时再继续往下执行,直到遇到下一条yield语句
  3. 如果没有再遇到yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值作为返回对象的value属性值
  4. 如果函数没有return语句,则返回对象的value属性值为unfined

yield语句只能在Generator函数中使用,否则会报错

3. next方法的参数

yield语句本身没有返回值,或者说其返回值为undefined,next方法可以带有一个参数,该参数会被当作上一条yield语句的返回值供下一条yield语句使用。

function * helloWorldGenerator(){
    const a = yield 'hello';
    yield a;
    return 'ending';
}
const iter = helloWorldGenerator();
console.log(iter.next()); // {value: 'hello', done: false}
console.log(iter.next('aaaa')); // {value: 'aaaa', done: false}

Generator函数从暂停状态到运行状态,其上下文状态是不变的

function * foo(x){
  const y = 2 * (yield (x + 1));
  const z = yield (y / 3);
  return (x + y + z);
}
const iter = foo(5);
console.log(iter.next()); // {value: 6, done: false}
console.log(iter.next()); // {value: NaN, done: false}
console.log(iter.next()); // {value: NaN, done: true}

第二次运行next方法时,没有传递参数,导致y的值等于2 * undefined(即NaN),后面的所有结果均为NaN

function * foo(x){
  const y = 2 * (yield (x + 1));
  const z = yield (y / 3);
  return (x + y + z);
}
const iter = foo(5);
console.log(iter.next()); // {value: 6, done: false}
console.log(iter.next(12)); // {value: 8, done: false}
console.log(iter.next(13)); // {value: 42, done: true}

第二次运行next方法时,传递参数12,则y的值等于2 * 12 = 24,24 / 3 = 8;第三次运行next方法时,传递参数13,则return的值等于5 + 24 + 13 = 42

4. for...of循环遍历

for...of循环可以自动遍历Generator函数生成的Iterator对象,且不需要调用next方法

function * foo(){
  yield 1;
  yield 2;
  yield 3;
  return 4;
}
for (let iter of foo()) {
  console.log(iter);
} // 1 2 3

一旦next方法返回的对象的done属性为true,for...of循环就会终止,且不会包含返回对象,所以return语句返回的值不在循环中。

5. Generator.throw()

throw方法可以再函数体外抛出错误,然后在Generator函数体内捕获。

const g = function *(){
  try {
    yield 1;
    yield 2;
  } catch (error) {
    console.log('内部错误',error);
  }
}

const i = g();
console.log(i.next()); 
console.log(i.throw());

// {value: 1, done: false}
// 内部错误 undefined
// {value: undefined, done: true}

6. Generator.return()

return方法可以返回给定的值,并终结Generator函数的遍历。

const g = function *(){
  yield 1;
  yield 2;
  yield 3;
}

const i = g();
console.log(i.next());
console.log(i.return(999));
console.log(i.next());

// {value: 1, done: false}
// {value: 999, done: true}
// {value: undefined, done: true}

7. yield* 表达式

如果在Generator函数内调用另外一个Generator函数,默认是不生效的,但是可以通过yield*表达式实现嵌套调用。

在yield命令后面加上星号,表明它返回的是一个遍历器对象,这被称为yield*语句。

function* gen1() {
 yield 1;
 yield 2;
 yield 3;
}

function* gen2() {
  yield * gen1();
  yield 4;
  yield 5;
}
for (let val of gen2()) {
  console.log(val);
}
// 1 2 3 4 5

上面的gen2是代理者,gen1是被代理者,yield* gen1会返回一个遍历器对象,实现用一个遍历器遍历多个Generator函数的效果。

如果被代理的Generator函数有return语句,则会像代理的Generator函数返回数据。

yield*后面的Generator函数等同于在Generator函数内部部署了一个for...of循环。

任何部署了Iterator接口的数据结构都可以被yield*遍历。

const g = function *(){
  yield* [1,2,3,4,5];
}
for (const iterator of g()) {
  console.log(iterator);
}
// 1 2 3 4 5

8. Generator函数this

Generator函数返回的遍历器对象可以看做是Generator函数的实例,它继承Generator函数的prototype对象上的方法。

function *g(name){
  this.name = name;
}
g.prototype.say=function(){
  console.log('hello');
}
const i = g('nancy');
i.say(); // hello
console.log(i.name); // unfined

Generator函数返回的遍历器对象不能获取this,也不能同new命令一起使用

可以通过以下方式使Generator函数返回一个正常的实例对象,既可以使用next方法,又可以获取正常的this

function *g(name){
  this.name = name;
  yield this.a = 1;
  yield this.b = 2;
}
const i = g.call(g.prototype,'nancy');
console.log(i.next()); // {value: 1, done: false}
console.log(i.name); // nancy

通过call方法替换内部this的指向,使其指向Generator函数的prototype,由于遍历器对象有继承属性,所以可以拿到其prototype上面的属性和方法。

9. Generator与状态机

状态机指每运行一次,状态就会改变。

var ticking = true;

function clock(){
  if(ticking){
     console.log('Tick');
  }else{
     console.log('Tock');
  }
  ticking = !ticking;
}
clock(); // Tick
clock(); // Tock
clock(); // Tick

Generator函数实现状态机可以减少外部变量,更简洁和安全

function* clock(){
  while (true) {
    console.log('Tick');
    yield;
    console.log('Tock');
    yield;
  }
}
const i = clock();
i.next(); // Tick
i.next(); // Tock
i.next(); // Tick
i.next(); // Tock

10. Generator函数异步应用

异步任务简单理解就是一个任务被拆分为多个子任务,各个子任务不是连续完成的。

Generator函数出现之前,解决异步任务的方式有回调函数、Promise等,回调函数由于嵌套层级太深会导致回调地狱问题,Promise由于需要使用API封装,会造成大量冗余代码,而且语义不是很清楚。

Generator函数是ES6基于协程思想的一个实现,其最大的特点是交出了函数的执行权。可以把Generator函数当作异步任务的容器,需要暂停操作的地方使用yield语句注明。

const foo = function * () {
    let result1  = yield ajax("/api/book.json");
    let result2  = yield ajax("/api/user.json");
};

const generator = foo();

generator.next().value.then(result =>{
    return generator.next(result).value
}).then(result =>{
    return generator.next(result).value
})

Generator函数虽然可以执行异步任务,但是不能很好的管理流程,可以搭配Thunk函数,实现流程管理

const foo = function * () {
    try {
        let result1  = yield ajax("/api/book.json");
        let result2  = yield ajax("/api/user.json");
    } catch (error) {
        console.log(error);
    }
};
// Thunk函数
function run(fn) {
  const g = fn();

  function next(result){
    if(result.done) return;
    result.value.then(data =>{
       next(g.next(data));
    }, err => {
        g.throw(new Error(err));
    })
  }
  next(g.next());
}
run(foo);

第十八章 Module

1. 概述

ES6模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD都只能在运行时确定依赖关系。

以下是一个CommonJS加载举例:

let {stat, exists, readFile} = require('fs');
// 等同于
let fs = require('fs');
let stat = fs.stat;
let exists = fs.exists;
let readFile = fs.readFile;

CommonJS加载的实质是整体加载,生成一个对象,然后判断对象是否包含所需要的属性或方法。这种加载模式被称为“运行时加载”,因为只有运行时才能得到这个对象,导致无法在编译时进行“静态优化”。

CommonJS加载特性包括2点:缓存、加载执行

缓存:CommonJS中一个模块就是一个脚本文件,requier命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象。以后需要用到这个模块时就会在对象上属性上取值,即使再次require也不会再次执行该模块,而是利用缓存。

加载执行:脚本在require的时候就全部执行。

ES6模块不是对象,而是通过export命令显示指定输出的代码,再通过import命令输入。

import { stat, exists, readFile } from 'fs';

ES6加载实质是从fs模块加载所需要的属性和方法,而不加载其他方法。这种加载被称为“编译时加载”或者静态加载,及ES6可以在编译时就完成模块加载,效率比CommonJS模块的加载方案高。

因为ES6的静态加载,所以可以在项目中使用类型检查(type system)等只能靠静态分析实现的功能。

ES6模块化的优点:

  1. 服务器和浏览器均支持ES6模块格式
  2. 浏览器的新增API可以通过模块格式提供
  3. 不再需要以对象做命名空间
  4. 自动开启严格模式

ES6和CommonJS对比:

  1. CommonJS模块输出的是一个值的复制,ES6模块输出的是值的引用
  2. CommonJS模块是运行时加载,ES6模块是编译时输出接口

2. export命令

export命令用于规定模块的对外接口。

一个模块可以看做一个独立的文件,文件内部的变量,外界无法获取。可通过export关键字输出变量,共外界访问。

export const name = 'hello'

const name = 'hello'
let addr = 'BeiJing City'
var list = [1, 2, 3]
export {
  name,
  addr,
  list
}

export function say() {
  console.log('say')
}

class Test {
  constructor() {
      this.id = 2
  }
}
export {
  Test
}

as可以为输出的变量重命名

export { name as Name, addr as Adder,list as _list };

export语句输出的变量与其对应的值是动态绑定关系,可以随时通过变量取得模块内部实时的值。export通过接口输出的是同一个值,不同的文件加载接口得到的都是同样的实例。

CommonJS模块输出的值是缓存,不存在动态更新

3. export default命令

export default可以为模块指定默认输出

export default function foo(){}
  • export default只能有一个,而export可以有多个
  • export能直接导出变量表达式,export default不行
  • export在导出变量的时候需要以{ }的形式,而 export default则不需要
  • 通过export方式导出,在导入时要加{ },export default则不需要

4. import命令

import命令用于加载指定模块。

import list, {cname,caddr} from A  
import list, {cname as name,caddr} from A  
import list, * as mod from A

由于import是静态执行,所以不能使用任何表达式、变量、if结构等

import { 'f' + 'oo' } from 'my_module';

let my_module = 'Module';
import { foo } from 'my_module';

if(x = 1){
  import { foo } from 'module1';
}else{
  import { foo } from 'module2';
}

import语句会执行加载的模块,如果多次重复加载,则只会执行一次

5. 模块整体加载

除了指定加载某个输出值,还可以通过整体加载(*)来指定一个对象,所有输出值都会加载到这个对象上。

import * as foo from 'module2';

6. import()

用来动态加载某个模块,会返回一个Promise对象。

import('./my_module').then(()=>{}).catch(()=>{})
  1. import()可以实现按需加载
  2. import()可以实现动态加载
  3. import()可以实现条件加载