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只有全局作用域和函数作用域,没有块级作用域,导致了很多不合理的场景出现。
- 内层变量覆盖外层变量
- 用来计数的循环变量污染全局,导致内存泄漏
ES6新增块级作用域,解决了以上问题。
- 块级作用域允许任意嵌套
- 外层作用域不能读取内层作用域的变量
- 内层作用域可以定义外层作用域的同名变量
ES6允许在块级作用域内声明函数,函数声明语句的行为类似于let,在块级作用域之外不可引用。
4. 声明变量的6种方式
ES6一共有6种声明变量的方式:var命令、function命令、let命令、const命令、import命令、class命令
var和function:声明的变量是全局变量,会成为顶层对象的属性
let、class和const:声明的变量不是全局变量,不会成为顶层对象的属性
5. var、let和const对比
| var | let | const |
|---|---|---|
| 声明变量 | 声明变量 | 声明常量 |
| 全局变量 | 局部变量 | 局部常量 |
| 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. 变量解构赋值场景
- 变量值交换
let x = 1;
let y = 2;
[x,y] = [y,x]
- 从函数返回多个值
return {name, age,sex}
- 函数参数的定义
function fn([x,y,z]){}
- 提取JSON数据
let {id, data,src} = JSONData
- 函数参数默认值
function fn(x,y,z=15){}
- 遍历Map结构
for(let [key,value] of map){}
- 输入模块的指定方法
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值
- Symbol.hasInstance
对象使用instance运算符时回调用这个方法,判断对象是否为某个构造函数的实例。
- Symbol.isConcatSpreadable
表示对象使用Array.prototype.concat()时是否可以展开。
数组的默认行为时可以展开的,取值为true或者undefined,都有这个效果。
- Symbol.species
指向当前对象的构造函数,创造实例时默认会调用这个方法,会使用这个属性返回的函数当作构造函数实例。
- Symbol.match
当执行str.match()时,如果该属性存在,回调用它返回该方法的返回值。
- Symbol.replace
当执行str.replace()时,如果该属性存在,回调用它返回该方法的返回值。
- Symbol.search
当执行str.search()时,如果该属性存在,回调用它返回该方法的返回值。
- Symbol.split
当执行str.split()时,如果该属性存在,回调用它返回该方法的返回值。
- 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实例的方法
- get()
用于拦截某个属性的读取操作,该方法可以继承。
如果一个属性不可配置(configurable)或者不可写(writable),则该属性不能被代理,通过代理Proxy对象访问该属性会报错。
- set()
用于拦截某个属性的赋值操作。
如果目标对象自身的某个属性不可配置(configurable)也不可写(writable),则set不得改变这个属性的值,只能返回同样的值,否则会报错。
- apply()
拦截函数的调用、call和apply操作,接收3个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
- has()
用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型操作是in操作符。
如果原对象不可配置或者禁止扩展,has拦截会报错。
- construct()
拦截new命令,返回值必须是一个对象,否则会报错。
- deleteProperty()
拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete删除。
- 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)
- 接口数据拦截
let data = new Proxy(response.data, {
set(obj, key, value) {
return false
}
})
- 数据校验拦截
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。
设计目标有以下几点:
- Reflect对象可以获取语言内部的方法
- Reflect可以修改某些Object方法的返回值,使其变得更合理
- 让Object操作编程函数行为
- Reflect对象的方法与Proxy对象的方法一一对应
2. 静态方法
- get(target,name,receiver)
查找并返回target对象的name属性,如果没有name属性,则返回undefined。
- set(target,name,value,receiver)
设置target对象的name属性等于value。该方法会触发Proxy.defineProperty拦截。
- has(target,name)
对应name in target中的in操作符。
- deletePrototype(target,name)
等同于delete target[name],用于删除对象属性。
该方法返回一个布尔值,如果删除成功或被删除的属性不存在,就返回true;否则返回false。
- construct(target,args)
等同于new target(...args)
第十三章 Promise
1. Promise概述
回调地狱即多层函数嵌套结构
function Fn(){
function fn1(){
function fn2(){
function fn3(){
... ...
}
}
}
}
promise是异步编程的一种解决方案,避免了层层嵌套的回调函数,比传统的解决方案更合理和强大。
Promise对象具有以下两个特点:
- 对象状态不受外界影响
Promise对象代表一个异步操作,有3中状态,Pending进行中、Fulfilled已成功、Rejected已失败。
- 状态改变后无法再次更改
一旦状态改变就不会再变,任何时候都可以拿到这个结果。状态的改变有两种可能,一种是从Pending到Fulfilled,一种是从Pending到Rejected。
Promise对象的缺点如下:
- 一旦创建就会立即执行,中途无法取消
- 如果不设置回调函数,Promise内部抛出的错误无法捕获
- 当处于Pending状态时,无法得知目前进展到哪一个阶段
2. 基本用法
- resolve()
在异步操作成功时调用,并将异步操作的结果,作为参数传递出去(Promise.resolve(num))。
resolve函数的参数如果是另外一个Promise实例,则后面的执行会等待两外一个Promise实例的状态改变后执行。
function Fn(){
return new Promise((resolve, reject) => {
// 异步操作,如请求后段接口等
resolve(res)
});
}
resolve函数还可以将对象转为Promise对象
Promise.resolve('foo');
- reject()
在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去(Promise.reject('操作失败'))
function Fn(){
return new Promise((resolve, reject) => {
// 异步操作,如请求后段接口等
reject(res)
});
}
reject函数还可以将对象转为Promise对象
Promise.reject('foo');
resolve和reject不会终结Promise的参数函数的执行
- 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方法没有传入任何回调,则继续向下传递(即所谓的值穿透)
- catch()
Promise为链式调用,实际使用过程中可能会有很多的then方法,所以如果把错误处理都写在then方法中的话就需要在每个then中对错误进行处理了。而使用catch方法的话,只需要在链式调用的最末尾加上一个catch方法就可以了
- 可以捕获reject传递的参数
- 可以捕获resolve回调中发生的错误
Fn().then().catch(err=>{});
- 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
- 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)
}
- finally()
不管Promise对象最后状态如何都会执行的操作,接收一个普通函数作为参数,该函数不管怎样都会执行。
Promise.race([
postAction('url',data),
getAction('url'),
getAction('url'),
]).then(resolve => {
console.log(resolve)
}).catch(err => {
console.log(err)
}.finally(()=>{
})
- allSettled()
Promise.all() 具有并发执行异步任务的能力。但它的最大问题就是如果其中某个任务出现异常reject,所有任务都会挂掉,Promise直接进入 reject 状态。Promise.allSettled()方法就是不管是否成功失败,都会返回结果并进入then方法。
- any()
Promise.any()和Promise.race()类似都是返回第一个结果,但是Promise.any()只返回第一个成功的,尽管某个 promise 的 reject 早于另一个 promise 的 resolve,仍将返回那个首先 resolve 的 promise。
3. Promise应用
- axios
axios是基于promise封装的ajax请求库
- 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();
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;
});
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个作用:
- 为各种数据结构提供统一的访问接口
- 使得数据结构的成员能够按某种次序排列
- iterator主要供for...of消费
Iterator的遍历过程如下:
- 创建一个指针对象,指向当前数据结构的起始位置
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
- 不断调用指针对象的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接口的场合
- 解构赋值
对于数组和Set解构进行解构赋值时会默认调用Symbol.iterator方法
- 扩展运算符
只要数据结构部署了Iterator接口,就可以使用扩展运算符将其转换为数组
- yield*
yield*后面跟的是一个可遍历结构,它会调用该结构的遍历器接口
- 数组的遍历
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对象、字符串。
- 数组
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
- 对象
对于普通的对象,可以采用for...in循环遍历键名,或者使用Object.keys方法将对象键名包装成一个数组,然后用for...of循环遍历数组
for (let key of Object.keys(obj)) {}
- 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个改进:
- 内置执行器
- 优化语义描述
- 扩展适应性
- 直接返回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对比
- async和Promise
async相较于Promise的链式调用,代码易于读懂。
await 会阻塞后面代码正常运行,即使后续代码不依赖前者,导致代码失去了并发性。
- 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方法执行逻辑如下:
- 遇到yield语句就暂停执行后面的操作,并将紧跟在yield后的表达式的值作为返回的对象的value属性值
- 下一次调用next方法时再继续往下执行,直到遇到下一条yield语句
- 如果没有再遇到yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值作为返回对象的value属性值
- 如果函数没有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模块化的优点:
- 服务器和浏览器均支持ES6模块格式
- 浏览器的新增API可以通过模块格式提供
- 不再需要以对象做命名空间
- 自动开启严格模式
ES6和CommonJS对比:
- CommonJS模块输出的是一个值的复制,ES6模块输出的是值的引用
- 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(()=>{})
- import()可以实现按需加载
- import()可以实现动态加载
- import()可以实现条件加载