由于受新型冠状病毒
的影响,假期又延长了,又不能出门,只好在家认真学习了,就借此机会阅读完阮一峰老师的ECMAScript 6入门教程,顺便将一些es6中常用的特性进行一个整理。相当于读书时代的划重点
1:let和const
用来声明变量,用法类似于var,但所声明的变量只能在变量所在的代码块内有效。
相同点:
都是用于块级作用域,不存在变量提升,一定要在声明后才可以使用,在声明前使用会报错。
不同点:
let定义的变量可以修改值。const定义的变量为常量,赋值后不能在修改。
2:变量解构赋值
解构是指按照一定模式,从数组和对象中提取值,对变量进行赋值。
1.数组解构
let [a, b, c] = [1, 2, 3];
相当于
let a = 1;
let b = 2;
let c = 3;
2.对象解构
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
3.解构用途
交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
从函数返回多个值
// 返回一个数组
function example() {
return [1, 2, 3];}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
提取JSON数据
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
3.字符串扩展
1.使用for...of遍历字符串。
var str = 'abcd';
for (let value of str){
console.log(value);
}
2.模板字符串
模板字符串是增强版的字符串,使用`来表示字符串的使用,也可以用来定义多行字符串,或者在字符串中使用变量。
//普通字符串
let a = `this is str`;
console.log(a);
//多行字符串
let glsl = `void main(){
var a;
}`
console.log(glsl);
//包含变量的字符串
let x = 1,y = 2;
let b = `x:${x}\n y:${y}`
console.log(b);
3.includes(), startsWith(), endsWith()
之前判断一个字符串中是否包含某个字符串时,使用的是indexOf
方法,在es6中新增了三个方法进行判断字符串中是否包含指定的字符。
includes()
:判断字符串中是否包含某个字符串。
startsWith()
:判断字符串是否以某个字符串开头。
endsWith()
:判断字符串是否以某个字符串结尾。
var string = `这是一个字符串。`
console.log(string.includes('字'));//true
console.log(string.startsWith('这'));//true
console.log(string.endsWith('。'));//true
4.repeat(n)
表示将原字符串复制n次,返回一个新的字符串。
console.log(string.repeat(3));
//这是一个字符串。这是一个字符串。这是一个字符串。
5.padStart(),padEnd()
根据指定的参数在开头或者结尾补全字符串。如:
console.log('x'.padStart(5, 'ab')) // 'ababx'
console.log('x'.padStart(4, 'ab')) // 'abax'
console.log('x'.padEnd(5, 'ab')) // 'xabab'
console.log('x'.padEnd(4, 'ab')) // 'xaba'
6.trimStart(),trimEnd()
trim()
用于消除前后的空格,trimStart()
用于消除字符串开头的空格。trimEnd()
用于消除字符串结尾的空格。
4.数值的扩展
1.Number.isFinite(), Number.isNaN()
isFinite
用于判断一个数值是否是有限的(即不是Infinity
)。如果参数类型不是数值,Number.isFinite
一律返回false
。
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
Number.isNaN()
用来检查一个值是否为NaN
。如果参数类型不是NaN
,Number.isNaN
一律返回false
。
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true
与全局的
isFinite()
和isNaN()
有什么区别?
全局的isFinite()
和isNaN()
会先调用Number()
将非数值转成数值。而这两个新方法只对数值有效,Number.isFinite()
对于非数值一律返回false
,Number.isNaN()
只有对于NaN
才返回true
,非NaN
一律返回false
。
1.Number.isInteger()
使用Number.isInteger()
可以判断变量是否为整数类型。
console.log(Number.isInteger(12.5));//false
console.log(Number.isInteger(12));//true
2.Math.trunc()
Math.trunc()
去除小数部分,返回整数部分。
console.log(Math.trunc(12.33));//12
5.函数的扩展
1.函数参数的默认值
在定义函数的时候,可以在函数中使用默认值,当调用函数时,传递的变量值为空时,如果有默认值,使用变量的时候将使用默认值,通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。如下所示:
function init(a,b,c = 10){
console.log(a);//12
console.log(b);//1
console.log(c);//10
}
init(12,1);
2.箭头函数
ES6 允许使用“箭头”(=>)定义函数。如下两个函数是一样的。
var fun = function(a,b){
console.log(a+b);
}
var fun1 = (a,b) => {
console.log(a+b);
}
fun(1,2);
fun1(1,2);
箭头函数注意点
- 函数体内的
this
对象,是定义时所在的对象,不是使用是所在的对象。- 不可当作构造函数来使用,也就是不能使用
new
来创建对象。- 不可使用
arguments
对象,如果要使用类似的功能,可以使用rest
- 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
关于第一点函数体内的this
对象,是定义时所在的对象,示例如下所示:
function foo() {
setTimeout(() => {
console.log('id:', this.id);//42
}, 100);
setTimeout(function(){
console.log('id:', this.id);//21
},200)
}
var id = 21;
foo.call({ id: 42 });
不适用场合
- 第一个场合是定义对象的方法,且该方法内部包括this。
- 第二个场合是需要动态this的时候,也不应使用箭头函数。
3.尾调用
尾调用就是指某个函数的最后一步是调用另一个函数。如下所示:
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
上面代码中,函数m
和n
都属于尾调用,因为它们都是函数f
的最后一步操作。
6.数组扩展
1.扩展运算符
扩展运算符是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
扩展运算符的作用
- 替代函数的 apply 方法
由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。
function fun(a,b,c){
console.log(a+b+c);
}
fun.apply(null,[1,2,3]);
fun(...[1,2,3])
- 复制数组
var a = [1,2,3];
var b = [...a];
b[1]= 0;
console.log(a);//1,2,3
console.log(b);//1,0,3
- 合并数组
var a = [1,2,3];
var b = [...a];
b[1]= 0;
console.log(a);//1,2,3
console.log(b);//1,0,3
var c = [...a,...b];
console.log(c);
2.Array.from()
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
3.Array.of()
Array.of
方法用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
7.对象的扩展
1.属性的可枚举性和遍历
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。
目前,有四个操作会忽略enumerable为false的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
2.属性遍历
对象的属性遍历一共有5种方法。
1.for...in
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
var obj = {
a:'a1',
b:'b1',
c:12
}
ergodic();
function ergodic(){
for(let key in obj){
console.log(key + '=' + obj[key]);
}
}
2.Object.keys(obj)
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
var obj = {
a:'a1',
b:'b1',
c:12
}
ergodic();
function ergodic(){
var array = Object.keys(obj);
array.forEach(key => {
console.log(key + '=' + obj[key]);
})
}
3.Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
var obj = {
a:'a1',
b:'b1',
c:12
}
ergodic();
function ergodic(){
var array = Object.getOwnPropertyNames(obj);
array.forEach(key => {
console.log(key + '=' + obj[key]);
})
}
4.Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有 Symbol 属性的键名。
5.Reflect.ownKeys(obj)
Reflect.ownKeys
返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
var obj = {
a:'a1',
b:'b1',
c:12
}
ergodic();
function ergodic(){
var array = Reflect.ownKeys(obj);
array.forEach(key => {
console.log(key + '=' + obj[key]);
})
}
3.扩展运算符
对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。对象的扩展运算符等同于使用Object.assign()
方法。
var obj = {
a:'a1',
b:'b1',
c:12,
}
var obj1 = {...obj};
console.log(obj1);
4.Null 判断运算符
ES2020
引入了一个新的Null
判断运算符??
。它的行为类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
5.Object.is()
用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。但与===
不同之处只有两个:一是+0
不等于-0
,二是NaN
等于自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
6.Object.assign()
Object.assign
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
注意点
- Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
-一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。
8.Symbol
Symbol
是一种新的数据类型,属性名使用Symbol
类型是独一无二的,不会跟其它的属性名发生冲突。Symbol
是一个原始类型的值,不是对象,不能添加给它添加属性。Symbol
可以接受一个字符串作为参数,如果参数是一个对象,会先调用它的toString
方法转成字符,再生成一个Symbol
值。Symbol
函数的参数只是当前Symbol
值的一个描述,因些两个参数相同的Symbol
是不相等的。Symbol
值不能与其它类型的值进行运算。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
Symbol
值可以转化为字符串和布尔类型的值。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
let sym = Symbol();
Boolean(sym) // true
1.Symbol.for()
Symbol.for()
可以接受一个字符串作为参数,然后生成一个Symbol
值,如果参数相同,生成的Symbol
值也相等。
console.log(Symbol.for('s') === Symbol.for('s'));//true
console.log(Symbol('s') === Symbol('s'));//false
通过Symbol.for()
创建的Symbol
值可以使用Symbol.keyFor()
获取参数名。
let s3 = Symbol.for('s3');
console.log(Symbol.keyFor(s3));//s3
let s4 = Symbol('s4');
console.log(Symbol.keyFor(s4));//undefined
2.Symbol.hasInstance
对象的Symbol.hasInstance
属性指向一个内部方法,当其它对象在使用instanceof
时,会调用这个方法。
const Even = {
[Symbol.hasInstance](obj){
return Number(obj) % 2 === 0;
}
}
console.log(1 instanceof Even) // false
console.log(2 instanceof Even) // true
3.Symbol.match
对象的Symbol.match
属性指向一个函数,当执行str.match(myObject)
时,如果该属性存在时,将返回该方法的返回值。
const MyMatcher = {
[Symbol.match](str){
return 'haha'
}
}
console.log('test'.match(MyMatcher));//haha
9.Set 和 Map 数据结构
1.Set
Set
类似于数组,但成员的值是唯一的,没有重复的值。
Set常用方法
Set.prototype.constructor
:造函数,默认就是Set函数。Set.prototype.size
:返回成员的数量。Set.prototype.add(value)
:往Set
中添加成员。Set.prototype.delete(value)
:删除指定的成员。Set.prototype.has(value)
:判断是否包含value
。Set.prototype.clear()
:清空所有成员。Set.prototype.keys()
:返回键名的遍历器Set.prototype.values()
:返回键值的遍历器Set.prototype.entries()
:返回键值对的遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员
2.WeakSet
WeakSet
和Set
类似,成员的值是唯一的,没有重复的值。但WeakSet
只能存储对象。并且WeakSet
中存储的是弱引用,当其它对象不再引用该对象时,垃圾回收器将自动回收该对象的内存。WeakSet
适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。WeakSet
不能被遍历。
常用方法
WeakSet.prototype.add(value)
:向WeakSet
实例添加一个新成员。WeakSet.prototype.delete(value)
:清除WeakSet
实例的指定成员。WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在WeakSet
实例之中。
3.Map
标准对象的键值是字符串或者Symbol
,不能使用对象作为键,如果使用对象作为键,将自动转成字符串。如果需要以对象作为键,将会用到Map
。如果对同一个键多次赋值,后面的值将覆盖前面的值。
var map = new Map();
var obj = {a:'a'};
map.set(obj,'这是对象作为key');
map.set('key','haha');
map.set('key','haha1');
console.log(map.get('key'));//haha1
console.log(map.get(obj));//这是对象作为key
常用方法
Map.prototype.size()
:返回Map
的长度。Map.prototype.set(key, value)
:set
方法设置键名key
对应的键值为value
,然后返回整个Map
结构。如果key
已经有值,则键值会被更新,否则就新生成该键。Map.prototype.get(key)
:get
方法读取key
对应的键值,如果找不到key
,返回undefined
。Map.prototype.has(key)
:has
方法返回一个布尔值,表示某个键是否在当前Map
对象之中。Map.prototype.delete(key)
:delete
方法删除某个键,返回true
。如果删除失败,返回false
。Map.prototype.clear()
:清空所有值。Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。
WeakMap
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。
WeakMap与Map的区别
- 首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
- 其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。一旦不再需要时,我们就必须手动删除引用。
10.Proxy
Proxy
是一种代理,在目标对象之前设置一层‘拦截’,在访问该对象时,都要先访问这个代理对象,通过这个代理对象可以进行过滤和修改等操作。
var proxy = new Proxy(target, handler);
new Proxy
生成一个Proxy
实例,target
表示需要拦截的目标对象,handler
用于定制拦截行为。如下所示为一个拦截get
方法的代码。
var o = new Proxy({},{
get:function(target,key){
return 'haha'
}
})
console.log(o.name);//haha
console.log(o['type']);//haha
注意,要使得Proxy
起作用,必须针对Proxy
实例(上例是proxy
对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
Proxy 可拦截的13个方法
get(target, propKey, receiver)
:拦截对象属性的读取,比如proxy.foo
和proxy['foo']
。set(target, propKey, value, receiver)
:拦截对象属性的设置,比如proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。has(target, propKey)
:拦截propKey in proxy
的操作,返回一个布尔值。deleteProperty(target, propKey)
:拦截delete proxy[propKey]
的操作,返回一个布尔值。ownKeys(target)
:拦截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。getOwnPropertyDescriptor(target, propKey)
:拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。defineProperty(target, propKey, propDesc)
:拦截Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。preventExtensions(target)
:拦截Object.preventExtensions(proxy)
,返回一个布尔值。getPrototypeOf(target)
:拦截Object.getPrototypeOf(proxy)
,返回一个对象。isExtensible(target)
:拦截Object.isExtensible(proxy)
,返回一个布尔值。setPrototypeOf(target, proto)
:拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。apply(target, object, args)
:拦截Proxy
实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。construct(target, args)
:拦截Proxy
实例作为构造函数调用的操作,比如new proxy(...args)
。
11.Reflect
Reflect
跟Proxy
一样都可以操作对象。使用Reflect
可以拿到对象原始的方法。
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
静态方法
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
12.Promise
Promise
是一种异步编程的解决方案,相对传统的回调函数和事件要强大。Promise
是一个对象,它可以获取异步操作的消息。
Promise
特点
Promise
的状态不受外界的影响。Promise
对象代表一个操作,它包含三种状态:pending
(进行中),fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果才能决定是哪种状态,其它操作无法改变Promise
的状态。- 一旦状态改变,就不会再变, 任何时候都可以获取到这个结果。
Promise
的状态变化只有两种,从
pending
(进行中)变成fulfilled
(已成功),或者从pending
(进行中)变成rejected
(已失败)。
Promise
缺点Promise
创建后会立即执行,无法中途取消。- 如果不设置回调函数,
Promise
内部发生错误时无法反应到外部。- 当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
1.基本用法
Promise
对象是一个构造函数,通过构造函数来创建Promise
实例。如下所示:
let promise = new Promise(function(resolve,reject){
setTimeout(resolve,500);
});
promise.then(function(value){
console.log('promise执行结束')
},function(error){
console.log('promise执行时发生了错误')
})
Promise
构造函数接受一个函数作为参数,函数包括resolve
和reject
两个函数,resolve
的作用是将Promise
对象的状态从pending
(进行中)变成fulfilled
(已成功)。在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending
变为 rejected
),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
注意
因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。,如下所示:
let p = new Promise(function(resolve,reject){
console.log(1);
resolve(2);
console.log(3);
})
p.then(function(value){
console.log(value);
})
//1
//3
//2
resolve
函数的参数除了正常的值以外,还可能是另一个 Promise
实例,比如像下面这样。
let p1 = new Promise(function(resolve,reject){
setTimeout(() => {
reject('执行错误')
}, 3000);
})
let p2 = new Promise(function(resolve,reject){
setTimeout(() => {
resolve(p1);
}, 1500);
})
p2.then(function(value){
console.log(value)
}).catch(error =>{
console.log(error)
})
2.Promise.prototype.then()
then
方法是Promise
原型对象上的一个方法,该方法的作用是Promise
在发生状态改变后的回调函数。then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。then
方法将返回一个Promise
对象,因此可以采用链式写法。如下所示:
let p3 = new Promise(function(resolve,reject){
resolve('p3');
})
p3.then(function(value){
console.log(value);
return new Promise(function(resolve,reject){
setTimeout(() => {
resolve('p3返回的Promise');
}, 1000);
})
}).then(function(value){
console.log(value)
})
//p3
//p3返回的Promise'
3.Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。如下所示:
let p3 = new Promise(function(resolve,reject){
resolve('p3');
})
p3.then(function(value){
console.log(value);
return new Promise(function(resolve,reject){
setTimeout(() => {
reject(new Error('p3的Promise返回错误信息'));
}, 1000);
})
}).then(function(value){
console.log('result'+value)
}).catch(function(error){
console.log(error);
})
//p3
//p3的Promise返回错误信息'
如果Promise
的状态已经变成了resolved
,再抛出异常是无效的。因为 Promise
的状态一旦改变,就永久保持该状态,不会再变了。如下所示:
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise.then(function(value) {
console.log(value)
})
.catch(function(error) {
console.log(error)
});
Promise
对象的错误具有‘冒泡’性质,它会一直向后传递,直到捕获为止。错误总是被下一个catch
捕获,一般来说,不要在then
方法里面定义 Reject
状态的回调函数(即then
的第二个参数),总是使用catch
方法。如下所示:
const promise = new Promise(function(resolve, reject) {
reject('发生了错误');
});
promise.then(function(value) {
console.log("success-"+value)
})
.catch(function(error) {
console.log("error="+error)
});
//error=发生了错误
4.Promise.prototype.finally()
finally
方法是不论Promise
的状态是什么样的,都会执行finally方法,如下所示:
const promise = new Promise(function(resolve, reject) {
reject('发生了错误');
});
promise.then(function(value) {
console.log("success-"+value)
})
.catch(function(error) {
console.log("error="+error)
}).finally(function(value){
console.log('finally');
});
//error=发生了错误
//finally
finally
方法不接受任何参数。finally
方法总是会返回原来的值。
5.Promise.all()
Promise.all()
将多个Promise
包装成一个Promise
实例。
let p1 = new Promise(function(resolve,reject){
resolve('p1');
})
let p2 = new Promise(function(resolve,reject){
resolve('p2');
})
let p3 = new Promise(function(resolve,reject){
resolve('p3');
})
let p= Promise.all([p1,p2,p3]).then(function(value){
console.log('success:' + value)
}).catch(function(error){
console.log('fail='+error);
})
p
的状态由p1
、p2
、p3
决定,分成两种情况。
- 只有
p1
、p2
、p3
的状态全为fulfilled
时,p
的状态才为fulfilled
。此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。- 只要
p1
、p2
、p3
中有一个状态为rejected
时,p
的状态就为rejected
。此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
注意,如果作为参数的 Promise
实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。如下所示:
let p1 = new Promise(function(resolve,reject){
resolve('p1');
})
let p2 = new Promise(function(resolve,reject){
reject('p2');
}).catch(function(error){
})
let p3 = new Promise(function(resolve,reject){
resolve('p3');
})
let promise = Promise.all([p1,p2,p3]).then(function(value){
console.log('success:' + value)
}).catch(function(error){
console.log('fail='+error);
})
6.Promise.race()
Promise.race()
和Promise.all()
类似,都可以将多个Promise
包装成一个Promise
实例。 不同之处是只要有一个实例率先改变状态,Promise.race()
实例的结果也将改变状态,如下所示:
let p1 = new Promise(function(resolve,reject){
resolve('p1');
})
let p2 = new Promise(function(resolve,reject){
reject('p2');
}).catch(function(error){
})
let p3 = new Promise(function(resolve,reject){
resolve('p3');
})
let promise = Promise.race([p1,p2,p3]).then(function(value){
console.log('success:' + value)
}).catch(function(error){
console.log('fail='+error);
})
//success:p1
7.Promise.allSettled()
Promise.allSettled()
也是接受一组 Promise
实例作为参数,包装成一个新的 Promise
实例。只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束。该方法由 ES2020
。如下所示:
let p1 = new Promise(function(resolve,reject){
resolve('p1');
})
let p2 = new Promise(function(resolve,reject){
reject('p2');
})
let p3 = new Promise(function(resolve,reject){
resolve('p3');
})
let promise = Promise.allSettled([p1,p2,p3]).then(function(value){
debugger
console.log('success:' + value)
}).catch(function(error){
console.log('fail='+error);
})
该方法返回的新的 Promise
实例,一旦结束,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled后,Promise
的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()
的 Promise
实例。
8.Promise.resolve()
Promise.resolve()
将现有对象转化为Promise
对象。如下所示:
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
Promise.resolve方法的参数分成四种情况。
- 参数是一个
Promise
实例,如果参数是Promise
实例,那么Promise.resolve
将不做任何修改、原封不动地返回这个实例。- 参数是一个
thenable
对象。thenable
对象指的是具有then
方法的对象,Promise.resolve
方法会将这个对象转为Promise
对象,然后就立即执行thenable
对象的then
方法。- 参数不是具有then方法的对象,或根本就不是对象。如果参数是一个原始值,或者是一个不具有
then
方法的对象,则Promise.resolve
方法返回一个新的Promise
对象,状态为resolved
。- 不带有任何参数。
Promise.resolve()
方法允许调用时不带参数,直接返回一个resolved
状态的Promise
对象。
9.Promise.reject()
Promise.reject()
返回一个新的Promise
对象,该对象的状态为rejected
。
13.Iterator 和 for...of 循环
Iterator
是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据类型只要部署了Iterator
接口,就能完成遍历操作。
Iterator
作用
- 为各种数据类型提供一个统一的,简便的访问接口。
- 使用数据结构的成员按照某种次序进行排列。
- 使用
for...of
进行遍历。
1.默认的Iterator 接口
凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
如下所示:
var array = [1,2,3,4];
var iterator = array[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
14.Generator
Generator
是一种异步编程的解决方案,语法行为与传统的函数不同。Generator
是一种状态机函数,封装了多个内部状态。执行Generator
函数会返回一个遍历器对象,可依次遍历Generator
函数内部的每一个状态。
Generator 特征
function
与函数名之间有一个*
。如function* test(){}
。- 函数内部可以使用
yield
表达式,定义不同的内部状态。
function* helloGenerator(){
yield 'hello';
yield 'Generator';
return 'end'
}
var it = helloGenerator();
for(var item of it){
console.log(item);
}
Generator
函数的调用跟调用普通函数一样,直接在函数名后面加一个括号,但与普通函数不一样的是,Generator
函数执行后返回的是一个Iterator
对象,可以调用next
方法,或者使用for...of
进行遍历。
1.yield 表达式
由于Generator
函数返回的是一个Iterator
对象,只有在执行next
方法时才会遍历下一个状态,通过使用yield
表达式可以设置一种暂停标注。当执行完next
方法后,将yield
后面的值作为value
返回,执行完一条后并暂停,只有当调用下一个next
方法才会往下执行并继续查找yield
表达式,当查找不到yield
时,将返回并结束遍历。yield
只能在Generator
函数中使用,在其它地方使用时将报错。
2.Generator.prototype.throw()
Generator
函数返回的对象,都有一个throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
3.yield* 表达式
使用yield*
表达式,用来在一个 Generator
函数里面执行另一个 Generator
函数。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
4.Generator用途
-
异步操作的同步化表达
通过使用Generator
可以处理异步函数,用来代替回调函数。将异步操作写在yield
表达式上,等调用next
方法后再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield
表达式下面,反正要等到调用next
方法时再执行。如下所示:
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 卸载UI
loader.next()
-
控制流程管理
利用for...of循环会自动依次执行yield命令的特性,提供一种更一般的控制流管理的方法。如下所示:
let steps = [step1Func, step2Func, step3Func];
function* iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
-
部署 Iterator 接口
利用Generator
函数,可以在任意对象上部署Iterator
接口。如下所示:
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
-
作为数据结构
Generator
可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为Generator
函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。如下所示:
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
for (task of doStuff()) {
// task是一个函数,可以像回调函数那样使用它
}
5.Generator 函数的异步应用
异步是指一个任务不是连续完成的,可以理解成该任务被分成为两段,先执行第一段,然后再执行其它的任务,等做好准备后再执行第二段的任务。
比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。
6.async 函数
async
函数是Generator
的方法糖。async
就是将Generator
函数的*
换成了async
,将yield
换成了await
。async
函数返回一个Promise
对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
let readFile = function(){
return new Promise(function(resolve,reject){
setTimeout(() => {
resolve('文件读取成功')
}, 2000);
})
}
let asyncReadFile = async function(){
let rf1 = await readFile()
let rf2 = await readFile();
console.log(rf1.toString());
console.log(rf2.toString());
}
asyncReadFile();
7.await命令
await
命令后面是一个Promise
对象,如果是其它值,将直接返回对应的值。如下所示:
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
await
命令后面的Promise
对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到。如下所示:
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
任何一个await
语句后面的Promise
对象变为reject
状态,那么整个async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
使用try...catch结构,实现多次重复尝试。
const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
break;
} catch(err) {}
}
console.log(i); // 3
}
test();
上面代码中,如果await
操作成功,就会使用break
语句退出循环;如果失败,会被catch
语句捕捉,然后进入下一轮循环。
使用注意点
await
命令后面的Promise
对象,运行结果可能是rejected
,最好将await
命令放在try...catch
中。- 多个
await
命令如果不存在先后顺序,最好同时时行。await
命令只能在async
中使用,在其它地方使用将会报错。
15.
async
函数可以保留运行堆栈。
8.async函数实现原理
async
的实现原理就是将Genernator
函数与自动执行器包装在一个函数里。如下所示:
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。
16.Class
ES6
可以使用class
关键字创建类,相比使用构造函数创建类更具有面向对象的思想,如下所示:
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
console.log("x="+this.x + " y="+this.y);
}
}
let point = new Point(12,13);
point.toString();
类的内部所有定义的方法,都是不可枚举的(non-enumerable)。如下所示:
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
console.log("x="+this.x + " y="+this.y);
}
}
let point = new Point(12,13);
point.toString();
console.log(Object.keys(Point.prototype));//[]
console.log(Object.keys(point));// ["x", "y"]
1.取值函数(getter)和存值函数(setter)
与 ES5
一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
2.Class表达式
函数一样,类也可以使用表达式的形式定义。如下所示:
const Circle = class C{
getRaidus(){
console.log('圆的半径为10')
}
}
const circle = new Circle();
circle.getRaidus();
3.静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
4.实例属性
Class
定义的类中,添加属性可以在构造函数中添加,也可以在类的顶层定义属性,如下所示:
const Circle = class C{
raidus = 20;
constructor(){
this.x = 10;
this.y = 10;
}
getRaidus(){
console.log(`x=${this.x};x=${this.y};radius=${this.raidus};`)
}
}
const circle = new Circle();
circle.getRaidus();
5.静态属性
静态属性指的是 Class
本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性。
// 老写法
class Foo {
// ...
}
Foo.prop = 1;
// 新写法
class Foo {
static prop = 1;
}
6.私有属性
为class
加了私有属性。方法是在属性名之前,使用#表示。如下所示:
class Foo {
#privateValue = 42;
getPrivateValue() {
return this.#privateValue;
}
}
var foo = new Foo();
console.log(foo.getPrivateValue())
7.继承
Class
可以通过extends关键字实现继承,这比 ES5
的通过修改原型链实现继承,要清晰和方便很多。如下所示:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
8.super
super
既可以当作函数来使用,也可以使用对象来使用。
super
作为函数调用时,代表父类的构造函数。ES6
要求,子类的构造函数必须执行一次super
函数。
class A {}
class B extends A {
constructor() {
super();
}
}
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
super
指向父类的原型对象,所以定义在父类实例上的属性和方法,无法通过super
来调用的。
9.Object.getPrototypeOf()
Object.getPrototypeOf
可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
可以使用这个方法判断,一个类是否继承了另一个类。
10.类的 prototype 属性和proto属性
Class
创建的类,同样有prototype
和__proto__
属性,同样存在两条继承链。
- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类。。- 子类
prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {
}
class B extends A {
}
console.log(B.__proto__ === A);//true
console.log(B.prototype.__proto__ === A.prototype);//true
11.实例的 proto 属性
子类实例的__proto__
属性的__proto__
属性,指向父类实例的__proto__
属性,也就是说子类原型的原型是父类的原型。
class A {
}
class B extends A {
}
let a = new A();
let b = new B();
console.log(b.__proto__.__proto__ === a.__proto__);//true
17.Module
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。
严格模式
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句- 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象- 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈- 增加了保留字(比如
protected
、static
和interface
)
1.export 命令
模块主要由两个命令组成,export
和import
命令。export
用于向外部导出接口。import
用于引入外部导出的接口。一个单独的文件就是一个模块,模块类的变量和函数在外部无法获取,如果希望外部能够访问,需要使用export
输入变量。export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。
export const url = 'http://www.gzcopright.cn';
function test (){
};
function util (){
}
export {test,util as Util};
2.import命令
使用export
导出的变量,在其它JS
文件中使用import
可以加载模块。import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。import
命令具有提升效果,会提升到整个模块的头部,首先执行,这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前。
import{url,Util,log} from './module.js'
function test(){
console.log(url)
}
test();
如果多次执行import
语句,只会执行一次,如下所示:
import 'lodash';
import 'lodash';
3.模块的整体加载
除了指定加载某个输出值,可以使用*
来加载所有输出的变量,如下所示:
import * as obj from './module.js'
function test(){
console.log(obj.url)
}
test();
4.export default 命令
export default
用于指定模块的默认输出,一个模块只能有一个默认输出。export default
输出的命令在使用import
时,不需要使用{}
,并且名称可以随便取。如下所示:
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
5.export 与 import 的复合写法
如果在同一模块中,先输入后输出同一个模块,import
和export
可以写在一起。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
6.模块的继承
模块之间是可以继承的,如下所示,circleplus
模块,继承了circle
模块。
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
上面代码中的export *
,表示再输出circle
模块的所有属性和方法。
7.import()
使用import()
可以动态加载模块,使用方法跟import
一样,唯一的区别就是import
需要放在代码的顶层,不支持动态导入。
适用场合
- 按需加载,
import()
可以在需要的时候,再加载某个模块。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
- 条件加载。
import()
可以放在if代码块,根据不同的情况,加载不同的模块。
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
- 动态的模块路径。
import()
允许模块路径动态生成。
import(f())
.then(...);
上面代码中,根据函数f的返回结果,加载不同的模块。
18.Module 的加载实现
加载js
文件,使用的是<script>
,默认是同步加载,既浏览器引擎解析dom
时,如果碰到了script
标签时会先加载js
脚本。当js
文件太大时,会阻塞线程出现卡死的情况,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
defer
和async
都支持异步加载脚本。但两者有一定的区别
defer
加载的脚本需要等面布渲染完成后再运行,如果有多个script
使用defer
时,每个文件是按顺序执行了。async
是加载完后就执行,如果有多个script
使用async
时,顺序是不固定的。
浏览器加载ES6
模块,也使用<script>
标签,但是要加入type="module"
属性。浏览器对于带有type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
1.ES6 模块与 CommonJS 模块的差异
ES6
模块输出的是值的引用,而CommonJS
模块输出的是值拷贝。ES6
模块是编译时加载接口,而CommonJS
模块是运行时加载。
19.编程风格
let
取代var
,常量使用const
- 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
- 使用数组成员对变量赋值时,函数的参数如果是对象的成员,如果函数返回多个值,优先使用解构赋值。
- 单行定义的对象,最后一个成员不要以逗号结尾,多行定义的对象,最后一个成员以逗号结尾。对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用
Object.assign
方法。- 使用扩展运算符
(...)
拷贝数组。使用Array.from
方法,将类似数组的对象转为数组。- 立即执行函数可以写成箭头函数的形式。那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了
this
。- 注意区分
Object
和Map
,只有模拟现实世界的实体对象时,才使用Object
。如果只是需要key: value
的数据结构,使用Map
结构。因为Map
有内建的遍历机制。- 总是用
Class
,取代需要prototype
的操作。因为Class
的写法更简洁,更易于理解。- 使用
import
取代require
。使用export
取代module.exports
。