My Javascript ES6-11 notes
# ECMASript 6 新特性
# 1. let 关键字
let
关键字用来声明变量,使用 let
声明的变量有几个特点:
- 不允许重复声明 (var 可以)
- 块级作用域 (ES6 才有的,只要是 let 声明的变量出了任何代码块 {} 就无效了,要是是 var 出了代码块 {} 有时还能用)
ES5-> 全局作用域,函数作用域,eval 里面作用域 (这个也是!)
ES6-> 全局作用域,函数作用域,eval 里面作用域,还有__块级作用域__
{
var a = 3
}
console.log(a); // 3
用 var 在全局作用域中,相当于没有这个包裹他的块级作用域,就相当于给 window 加了个属性,所以照样可以访问到
{
let a = 3
}
console.log(a); // 报错
let 块级作用域,只有代码块中做效
当然像 if, for, while 等等等那些代码块都是有自己的块级作用域
- 不存在变量提升 (也是跟 var 不一样,var 允许变量提升)
- 不影响作用域链
块级作用域是不能从外向内找,作用域链是向外找,所以没关系
# 应用场景:以后声明变量使用 let 就对了
案例 1:给多个 div
循环注册点击事件
-
错误示例:
// 错误示例,divs.length = = = 3 document.addEventListener('DOMContentLoaded', function () { let divs = document.querySelectorAll('.box div'); for (var i = 0; i < divs.length; i++) { divs[i].addEventListener('click', function () { divs[i].style.backgroundColor = 'pink'; //这个我们之前说过的 }); } console.log(i); // 3 }); /* i 为当前作用域下的共享变量。 当每次点击 div 的时候,各个点击事件共享 i 的值。 此时 i = 3,这将报错。 */
在全局作用域中的 for 循环里 var 声明 i, 然后循环很多次其实就是相当于是:
因为 var 没有块级作用域,所以这么做其实也就是让全局的 i 变了 (window.i), 最后循环结束,window.i 也变成最后的值
所以之后要是触发事件开启异步任务再按照作用域去找 i 这个变量就会找到这个全局变量 i 一看就是最后的值
-
正确实例:将以上代码中的
var
改为let
。不用像 ES5 那样使用闭包等等方式也能解决
在全局作用域中的 for 循环里 let 声明 i, 然后循环很多次其实就是相当于是:
因为 let 有块级作用域,他们之间是互不影响到,自己在当前的 iteration 里面使用,跟其他 iteration 的 i 不相关
因为他们是有块级作用域,所以并不会成为全局 (window) 的属性 (这样就不是 window.i 了)
案例 2:1s 后循环输出所有数字
- 错误示例:
for (var i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
/*
输出:6 6 6 6 6
循环从1-5的时间很短暂,远不及 1s。
此时五个异步事件瞬间加入到异步事件队列中,等待 1s后依次执行。
而此时i为6,故瞬间输出 5 个 6。
异步事件队头
(1) console.log(i);
(2) console.log(i);
(3) console.log(i);
(4) console.log(i);
(5) console.log(i);
*/
- 正确示例:
for (let j = 1; j <= 5; j++) {
setTimeout(() => {
console.log(j);
}, 1000);
}
// 输出:1 2 3 4 5
// let 有块级作用域,每个 j 都会形成一个自己的块级作用域,与相应的异步事件共享:
// {j = 1;} {j = 2;} {j = 3;} {j = 4;} {j = 5;}
- 解决方法 2:
// 给每一个 i 设置一个立即执行函数,会形成自己的块级作用域,不影响外部变量。
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 1000);
})(i);
}
这里的 let 跟 java 里的变量感觉很像
# 2. const 关键字
const
关键字用来声明常量, const
声明有以下特点:
- 声明必须赋初始值
- 标识符一般为大写
- 不允许重复声明
- 值不允许修改 (java 里的 static final? 像是)
- 块级作用域
注意:对象属性修改和数组元素变化不会出发 const
错误
这点像是 java 里面用 final 标识一个变量,这个变量如果是基础性就不能变值,如果是 object, 那么这个变量存的这个 object 的地址不能变,但是这个 object 的属性可以随便变
# 应用场景:声明对象类型使用 const,非对象类型声明选择 let
案例:
const arr = [1, 2, 3, 4];
arr.push(5, 6);
console.log(arr);
// 不报错
const obj = {
uname: 'rick',
age: 30
}
obj.age = 40;
// 只要不改变地址,就不报错
# 3. 变量的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为 解构赋值。
数组的解构赋值:
const arr = ['red', 'green', 'blue'];
let [r, g, b] = arr;
数组用 []
变量名随便,反正都是按照顺序挨个取出来给你每个变量赋值
对象的解构赋值:
const obj = {
uname: 'rick',
age: 30,
sayHi: function () {
console.log('hello');
},
sayBye() {
console.log('Bye~');
}
}
let {name, age, sayHi} = obj; //这里就是给name,age,sayHi变量从obj里面找对应的键取值赋值
let {sayBye} = obj; // 本来在对象中是方法的也可以直接赋值然后调用
//调用
sayHi(); // 就可以了, 这样不需要obj.sayHi()了
sayBye(); //这个可以这样
对象用 {}
跟数组方式不同,注意用的变量名和对象里面想取值对应的属性名 (键名) 必须一样!
# 应用场景:频繁使用对象方法、数组元素,就可以使用解构赋值形式。
注意! 数组是按顺序赋值的,对象是按变量名赋值的
# 4. 模板字符串
模板字符串(template string)是增强版的字符串,用反引号 ` 标识,特点:
- 字符串中可以出现__换行符__(就是你在 ide 之中直接换行可以的,如果是引号则不行需要用 + 连上)
- 可以使用
${xxx}
形式输出变量
let name = 'jack';
console.log(`hello, ${name}`); //用``就可以用${变量名}来获取变量值,而不是使用加号连接
let ul = `<ul>
<li>apple</li>
<li>banana</li>
<li>peach</li>
</ul>` //上方都是直接换行,都可以的
# 应用场景:当遇到字符串与变量拼接的情况使用模板字符串。
# 5. 简化对象写法
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
let uname = 'rick';
let age = 30;
let sayHi = function () {
console.log('Hi');
}
// 组建对象
const obj = {
uname, // 直接放进来就行 (完整的方式是uname:uname)但是后面省略掉了
age,
sayHi() { //方法也不用冒号了 (sayHi: function(){...}) 直接这样就行
console.log('Hi');
}
}
# 应用场景:对象简写形式简化了代码,所以以后用简写就对了。
# 6. 箭头函数
ES6 允许使用「箭头」 =>
定义函数。
function
写法:
function fn(param1, param2, …, paramN) {
// 函数体
return expression;
}
=>
写法:
let fn = (param1, param2, …, paramN) => {
// 函数体
return expression;
}
一般都是省略掉参数括号前面的 "function" 然后再参数括号后面加上 =>
箭头函数的 注意点:
- 如果形参只有一个,则小括号可以省略
- 函数体如果只有一条语句,则花括号可以省略 (此时 return 也必须省略),函数的返回值为该条语句的执行结果
- 箭头函数
this
是静态的,始终指向__声明时所在作用域__下this
的值
所以不论是用 this 改变方式比如说 call,apply,bind 都是不会变的
- 箭头函数不能作为构造函数实例化
//构造函数还是像之前一样的写法
function Ababa (name, age){
this.name = name;
this.age = age;
}
//或者是类里面的构造函数
constructor(name, age) {
this.uname = uname;
this.age = age;
}
- 不能使用
arguments
// 省略小括号
let fn1 = n => {
return n * n;
}
// 省略花括号
let fn2 = (a + b) => a + b;
// 箭头函数 this 始终指向声明时所在作用域下 this 的值
const obj = {
a: 10,
getA () {
let fn3 = () => {
console.log(this); // obj {...}
console.log(this.a); // 10
}
fn3();
}
}
案例 1:箭头函数 this
始终指向声明时所在作用域下 this
的值, call
等方法无法改变其指向。
let obj = {
uname: 'rick',
age: 30
};
let foo = () => {
console.log(this);
}
let bar = function () {
console.log(this);
}
// call 对箭头函数无效
foo.call(obj); // window
bar.call(obj); // obj {...}
案例 2:筛选偶数
let arr = [2, 4, 5, 10, 12, 13, 20];
let res = arr.filter(v => v % 2 = = = 0);
console.log(res); // [2 ,4 10, 12, 20]
案例 3:点击 div 两秒后变成粉色
- 方案 1:使用
_this
保存div
下的this
,从而设置div
的style
属性。
div.addEventListener('click', function () {
let _this = this;
setTimeout(function () {
console.log(_this); // <div>...<div>
_this.style.backgroundColor = 'pink';
}, 2000)
});
- 方案 2:使用
=>
箭头函数__(重要!!!)__
div.addEventListener('click', function () {
setTimeout(() => {
console.log(thid); // <div>...</div>
this.style.backgroundColor = 'pink';
}, 2000);
});
注意这里是将里面的 function 替换为箭头函数,这么做可以让里面那个函数在声明的时候就确定当前函数里面的 this 指向的会是什么
如果是正常 function 的话,不能用 this 因为 setTimeout 函数里面的回调函数里面的 this 指向的是 window 而不是触发事件的 div
箭头函数里面的 this 是静态的, 箭头函数里面的 this__指向的是__声明那个函数所在的作用域下的 this 值,而在上面这个例子里,箭头函数是在这个外层的 click 事件触发而调用的回调函数里面所声明的,而这个外面这个回调函数是由 div 来调用的所以里面的 this 指向的是 div.
箭头函数在那个作用域下声明也会让自己的 this 指向 div. 所以就可以使用 this 了,而不是像之前一样把 div 地址存到变量然后通过作用域访或者使用 bind 等等
总结一句话,箭头函数所在的作用域里面的 this 指向谁那箭头函数里面的 this 就指向谁
-
不适合事件回调函数因为回调函数一般都是由 DOM 元素触发事件调用的,所以一般里面的 this 是我们想要操作的.
但要是我们把回调函数设为箭头函数,那么里面的 this 就会指向那个回调函数声明的作用域之中 (比如说在全局作用域下), 那么里面的 this 就指向的是 window 了,并不是我们想要的
-
不适合对象方法也是差不多原因
# 7. 函数参数默认值设定
ES6 允许给函数参数设置默认值,当调用函数时不给实参,则使用参数默认值。
具有默认值的形参,一般要靠后。
如果那个参数在调用函数没有给它传参
要是你没给默认值就会是 undefined
要是给了默认值就会用那个默认值
let add = (x, y, z=3) => x + y + z;
console.log(add(1, 2)); // 6
可与解构赋值结合:
function connect({ host = '127.0.0.1', uesername, password, port }) {
console.log(host); // 127.0.0.1
console.log(uesername);
console.log(password);
console.log(port);
}
connect({
// host: 'docs.mphy.top',
uesername: 'root',
password: 'root',
port: 3306
})
可以不是参数,可以直接
来代表传进来的对象,然后就可以直接用那个 键名 来代表他存的值而不是 对象。键名 (之前的方式)
当然是可以给摸个属性传默认值这样就算你传的对象里没有 host 属性也会取默认的
(注意!这跟上面传好几个参数不一样,那种方式是按熟顺序的)
对象则是按照键名匹配
# 8. rest 参数
ES6 引入 rest 参数,用于获取函数的实参,用来代替 arguments
,作用与 arguments
类似。将接收的参数序列转换为一个数组对象。
用在函数形参中,语法格式: fn(a, b, ...args)
,写在参数列表最后面。
函数参数声明必须是有
...args
来代表所有其他的 (所以当然可以跟其他参数一起用)然后如果
fn(a, b, ...args)
传进来了五个参数,那么前面两个分别给了a
和b
, 然后args
则会代表剩下那三个,并且会把那三个存到一个数组里面,所以可以使用数组的各种方法
let fn = (a, b, ...args) => {
console.log(a);
console.log(b);
console.log(args);
};
fn(1,2,3,4,5);
/*
1
2
[3, 4, 5]
*/
案例 1:求不定个数数字的和
let add = (...args) => {
let sum = args.reduce((pre, cur) => pre + cur, 0);
return sum;
}
console.log(add(1, 2, 3, 4, 5)); // 15
# 应用场景:rest 参数非常适合不定个数参数函数的场景
# 9. spread 扩展运算符
扩展运算符( spread
)也是三个点( ...
)。它好比 rest 参数的__逆__运算,将一个数组转为用逗号分隔的参数序列,对数组进行解包。可用在调用函数时,传递的实参,将一个数组转换为参数序列。(主要用法)
跟 rest 反着来
- rest 放函数形参
- … 放调用函数传参一个数组 (或对象) 的前面
扩展运算符也可以将对象解包。
展开数组:
function fn(a, b, c) {
console.log(arguments);
console.log(a + b + c);
}
let arr = ['red', 'green', 'blue'];
fn(...arr)
// [Arguments] { '0': 'red', '1': 'green', '2': 'blue' }
// redgreenblue
案例 1:数组合并
let A = [1, 2, 3];
let B = [4, 5, 6];
let C = [...A, ...B];
console.log(C); // [1, 2, 3, 4, 5, 6]
直接这样可合并
let C = [...A, ...B];
案例 2:数组克隆
这种数组克隆属于浅拷贝。
浅拷贝只是拷贝对象__第一层__的普通数据类型, 不是直接拷贝那个对象的地址!
浅拷贝被拷贝对象要是也有属性时复杂数据类型拷贝过去的就是地址了,而不是真正拷贝了一个一样的对象
所以浅拷贝真正只是创建了一个新对象然后拷贝了被拷贝对象的第一层属性为普通数据类型的值以及第一层属性为复杂数据类型的地址,所以这些被存地址的对象里面的普通数据类型和复杂数据类型属性都没被拷贝过去
let arr1 = ['a', 'b', 'c'];
let arr2 = [...arr1];
console.log(arr2); // ['a', 'b', 'c']
案例 3:将伪数组转换为真实数组
const divs = document.querySelectorAll('div'); //这里其实获得的是一个对象(节点对象)存着所有div,每个div都是属性的值(可以说是伪数组,不能用数组的方法),(其实对象里面的键就是0,1,2,3,...(看你有几个对象)),而那些div对象其实是值,这样就很伪数组因为都是0,1,2,..像是数组的下标
let divArr = [...divs]; //这样就让每一个div对象存到一个数组里面,现在就可以用数组方法了
console.log(divArr);
案例 4:对象合并
// 合并对象
let obj1 = {
a: 123
};
let obj2 = {
b: 456
};
let obj3 = {
c: 789
};
let obj = { ...obj1, ...obj2, ...obj3 };
console.log(obj);
// { a: 123, b: 456, c: 789 }
可以将 ES5 中的 arguments 给转成数组,但是现在 ES6 有了 rest 就没有必要
# 10. Symbol
# 10.1 Symbol 基本介绍与使用
ES6 引入了一种新的原始数据类型 Symbol
,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,是一种类似于字符串的数据类型。
JavaScript 的七种基本数据类型:
- 值类型(基本类型):string、number、boolean、undefined、null、symbol
- 引用数据类型:object(包括了 array、function)
Symbol 的特点:
- Symbol 的值是唯一的,用来解决命名冲突的问题
- Symbol 值不能与其他数据进行运算 (比如说 ±*/, 比较><, 等等等都不可以)
- Symbol 定义的对象属性不能使用
for...in
循环遍历,但是可以使用Reflect.ownKeys
来获取对象的所有键名
Symbol 的创建:
- 创建一个
Symbol
:
let s1 = Symbol();
console.log(s1, typeof s1);
// Symbol() symbol
-
添加具有标识的
Symbol
:let s2 = Symbol('1'); let s2_1 = Symbol('1'); console.log(s2 = = = s2_1); // false // Symbol 都是独一无二的, 那个传的String类型参数只是用来形容给这个Symbol的,给我们看的,没有其他的
上面代码中,
s1
和s2
是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol()
,不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。如果 Symbol 的参数是一个对象,就会调用该对象的
toString
方法,将其转为字符串,然后才生成一个 Symbol 值。 -
使用
Symbol.for()
方法创建,名字相同的Symbol
具有相同的实体。
let s3 = Symbol.for('apple');
let s3_1 = Symbol.for('apple');
console.log(s3 === s3_1); // true
- 输出
Symbol
变量的描述,使用description
属性
let s4 = Symbol('测试');
console.log(s4.description); // 测试
# 10.2 对象添加 Symbol 类型的属性
案例:安全的向对象中添加属性和方法。
分析:如果直接向对象中添加属性或方法,则原来对象中可能已经存在了同名属性或方法,会覆盖掉原来的。所以使用 Symbol
生成唯一的属性或方法名,可以更加安全的添加。
代码实现:
// 这是一个 game 对象,假设我们不知道里面有什么属性和方法
const game = {
uname: '俄罗斯方块',
up: function () { },
down: function () { }
}
// 通过 Symbol 生成唯一的属性名,然后给 game 添加方法
let [up, down] = [Symbol('up'), Symbol('down')];
game[up] = function () {
console.log('up');
}
game[down] = function () {
console.log('down');
}
// 调用刚刚创建的方法
game[up](); //而不是用game里面的up方法(which is called by game.up())
game[down]();
可以把 Symbol 理解成一个唯一的字符串,显示的是 Symbol (),但实际上是一个字符串,添加到 game 方法,理论上不会和现有的属性名冲突,所以可以很安全的添加方法,调用使用 game [methods.up]()
总结:为了防止你创建和方法和对象本身的方法冲突,选择在另一个对象中用 symbol 创建独一无二的方法,用 [] 调用,不会污染本身对象中的方法
# 10.3 Symbol 内置值
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。可以称这些方法为魔术方法,因为它们会在特定的场景下自动执行。
方法 | 描述 |
---|---|
Symbol.hasInstance |
当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法 |
Symbol.isConcatSpreadable |
对象的 Symbol.isConcatSpreadable 属性等于的是一个布尔值,表示该对象用于 Array.prototype.concat() 时,是否可以展开 |
Symbol.species |
创建衍生对象时,会使用该属性 |
Symbol.match |
当执行 str.match(myObject) 时,如果该属性存在,会调用它,返回该方法的返回值。 |
Symbol.replace |
当该对象被 str.replace(myObject) 方法调用时,会返回该方法的返回值。 |
Symbol.search |
当该对象被 str.search(myObject) 方法调用时,会返回该方法的返回值。 |
Symbol.split |
当该对象被 str.split(myObject) 方法调用时,会返回该方法的返回值。 |
Symbol.iterator |
对象进行 for...of 循环时,会调用 Symbol.iterator 方法,返回该对象的默认遍历器 |
Symbol.toPrimitive |
该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。 |
Symbol. toStringTag |
在该对象上面调用 toString() 方法时,返回该方法的返回值 |
Symbol. unscopables |
该对象指定了使用 with 关键字时,哪些属性会被 with 环境排除。 |
上面的都是 Symbol 对象的属性,而我们可以用这些 Symbol 的属性又被我们当做对我们一些对象的属性来改变我们对象在特定场景下的表现, 说白了就是扩展对象功能
案例 1: Symbol.hasInstance
方法判断是否属于这个对象时被调用。
class A {
static [Symbol.hasInstance]() {
console.log('判断是否属于这个对象时被调用');
}
}
let obj = {};
console.log(obj instanceof A
// 判断是否属于这个对象时被调用
// false
案例 2:数组使用 concat
方法时,是否可以展开。
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [4, 5, 6];
arr2[Symbol.isConcatSpreadable] = false;
console.log(arr1.concat(arr2));
// [ 1, 2, 3, [ 4, 5, 6, [Symbol(Symbol.isConcatSpreadable)]: false ] ]
console.log(arr1.concat(arr3));
// [ 1, 2, 3, 4, 5, 6 ]
# 11. 迭代器
# 11.1 定义
遍历器( Iterator
)就是一种机制。它是一种接口,为各种不同的数据结构提 供统一的访问机制。任何数据结构只要部署 Iterator
接口,就可以完成遍历操作。
- ES6 创造了一种新的遍历命令
for...of
循环,Iterator
接口主要供for...of
消费。 - 原生具备
iterator
接口的数据 (可用 for of 遍历)Array
Arguments
Set
Map
String
TypedArray
NodeList
在 js 里面,接口可以看成是我们对象的一个属性 (属性名为 Symbol.iterator)
案例:使用 next()
方法遍历原生自带 iterator
接口的数据:
// 遍历 Map
const mp = new Map();
mp.set('a', 1);
mp.set('b', 2);
mp.set('c', 3);
let iter1 = mp[Symbol.iterator]();
// next() 方法每执行一次,指针自增
console.log(iter1.next()); // { value: [ 'a', 1 ], done: false }
console.log(iter1.next()); // { value: [ 'b', 2 ], done: false }
console.log(iter1.next()); // { value: [ 'c', 3 ], done: false }
console.log(iter1.next()); // { value: undefined, done: true }
// 遍历数组
let xiyou = ['唐僧','孙悟空','猪八戒','沙僧'];
let iter2 = xiyou[Symbol.iterator]();
console.log(iter2.next()); // { value: '唐僧', done: false }
console.log(iter2.next()); // { value: '孙悟空', done: false }
console.log(iter2.next()); // { value: '猪八戒', done: false }
console.log(iter2.next()); // { value: '沙僧', done: false }
上面的案例只是为了证明他们自带 iterator
接口,实际上直接使用 for...of
方法遍历即可( iterator
接口为 for...of
)服务。例如,可以使用 for [k, v] of map
来遍历 Map 数据结构中的键和值。
const mp = new Map();
mp.set('a', 1);
mp.set('b', 2);
mp.set('c', 3);
for (let [k, v] of mp) {
console.log(k, v);
}
/*
a 1
b 2
c 3
*/
for( let k in ...)
,k 保存的是键名
for( let v of ...)
,v 保存的是值
# 11.2 工作原理
- 这个你创建的对象的__proto__属性存的对应的原型对象里有 Symbol.iterator 属性,这个属性里面存了函数
- 这个函数里面,显示创建一个指针对象,指向当前数据结构 (就是那些原生具备
iterator
接口的数据结构) 的起始位置 - 第一次调用对象的
next
方法,指针自动指向数据结构的第一个成员 - 接下来不断调用
next
方法,指针一直往后移动,直到指向最后一个成员 - 每调用
next
方法返回一个包含value
和done
属性 (done 属性主要是看有没有完成遍历当前对象) 的对象
# 应用场景:需要自定义遍历数据的时候,要想到迭代器。
# 11.3 自定义遍历数据 (给自己建的对象添加 Iterator 接口 (属性))
我们可以通过给数据结构添加自定义 [Symbol.iterator]()
方法来使该数据结构能够直接被遍历,从而使 for...of
能够直接遍历指定数据,达到为 for...of
服务的功能。
// 需求:遍历对象中的数组
const xiaomi = {
uname: '小明',
course: [ '高数', '大物', '英语', '数据库' ],
// 通过自定义 [Symbol.iterator]() 方法 , 记得外面这个[]括号
[Symbol.iterator]() {
// 初始指针对象指向数组第一个
let index = 0;
// 保存 xiaomi 的 this 值, 可以用这种方式或者箭头函数也可以
let _this = this;
return {
next: function () {
// 不断调用 next 方法,直到指向最后一个成员
if (index < _this.course.length) {
return { value: _this.course[index++], done: false };
} else {
// 每调用next 方法返回一个包含value 和done 属性的对象
return { value: undefined, done: true };
}
}
}
}
}
// for...of直接遍历达到目的
for (let v of xiaomi) {
console.log(v);
}
上面这种方式可以按照我们想要遍历的数据而进行操作,而不是遍历所有的数据
这个 [Symbol.iterator] 方法其实就是返回了一个对象,这个对象 (可以看做就是指针) 里面有一个方法叫做 next
这个 next 方法就是遍历我们这个想要进行遍历的对象,这个 next 方法调用之后会按照我们给的条件来决定停不停止遍历
然后上面我们定的情况就是如果
index < _this.course.length
也就是说如果 index 这个下标变量小于我们一开始定的想要被遍历的对象的course
属性 (存的数组) 的长度就会返回一个对象,这个返回的对象有两个属性 (value 和 done), 这两个属性的值交给我们自己决定
- 这个 value 属性就是我们用
for (let v of 我们自己定的,想遍历的对象)
时 v 的值- 这个 done 属性就是说有没有遍历完,false 就是没有遍历完,没有遍历完就会默认一直调用 next 方法,直到返回的对象的 done 属性为 true.(这个也是我们按照外面 if statement 的 condition 来决定什么情况下才可以终止 next (返回对象的 done 属性为 true))
在这里我们就可以看到我们可以决定到底遍历我们那个对象的哪些内容,是对象所有的属性?还是对象的某个数组 / 对象属性?
我们自己写这个遍历器属性可以按照自己需求来决定遍历什么,遍历多少,等等等 (通过改比如说什么条件结束遍历,返回的对象的 value 值到底是返回的是什么等)
# 12. Generator 生成器函数
# 12.1 生成器函数的声明和调用
生成器函数是 ES6 提供的一种 异步编程解决方案,语法行为与传统函数完全不同。
*
的位置没有限制 (放到function
和函数名之间就行)- 使用
function * gen()
和yield
可以声明一个生成器函数。生成器函数返回的结果是迭代器对象,调用迭代器对象的next
方法可以得到yield
语句后的值。 - 每一个
yield
相当于函数的暂停标记,也可以认为是一个__分隔符__,每调用一次next()
,生成器函数就往下执行一段。 next
方法可以传递实参,作为yield
语句的返回值
例如以下生成器函数中,3 个 yield
语句将函数内部分成了 4 段。
function* generator() {
console.log('before 111'); // 下面yield分割了,这一块是生成器第 1 段
yield 111;
console.log('before 222'); // 下面yield分割了,这一块是生成器第 2 段
yield 222;
console.log('before 333'); // 下面yield分割了,这一块是生成器第 3 段
yield 333;
console.log('after 333'); // 这一块是生成器第 4 段
}
let iter = generator(); // 执行函数返回一个迭代器对象
console.log(iter.next()); //使用和这个迭代器对象的next方法就可以执行第一段(开始到遇到yield)代码并且返回一个对象,这个对象具有value和done属性
console.log(iter.next()); //从第一个yield之后到第二个yield之间的,并且返回一个对象,这个对象具有value和done属性
console.log(iter.next()); //so on...
console.log(iter.next()); //最后一个yield到结束之间的最后的代码,并且返回一个对象,这个对象具有value和done属性
/*
before 111
{ value: 111, done: false }
before 222
{ value: 222, done: false }
before 333
{ value: 333, done: false }
after 333
{ value: undefined, done: true }
*/
以上每次 next 方法执行返回的结果都是一个对象由 value 和 done 属性,value 存的就是对应的每一段
yield
语句之后的值
遇到 yield 语句,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性的值。
再次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 语句。
同样道理像我们这以前说的 for…of 那样,其实就是靠迭代器实现的,所以要是用 for…of
结果:
也就是每个 next 方法返回的对象的 value (之前说过) 属性.
# 12.2 生成器函数的参数传递
function* generator(arg) {
console.log(arg); // 生成器第 1 段
let one = yield 111;
console.log(one); // 生成器第 2 段
let two = yield 222;
console.log(two); // 生成器第 3 段
let three = yield 333;
console.log(three); // 生成器第 4 段
}
let iter = generator('aaa'); // 传给生成器第 1 段
console.log(iter.next());
console.log(iter.next('bbb')); // 传给生成器第 2 段,作为这一段开始的 yield 语句返回值
console.log(iter.next('ccc')); // 传给生成器第 3 段,作为这一段开始的 yield 语句返回值
console.log(iter.next('ddd')); // 传给生成器第 4 段,作为这一段开始的 yield 语句返回值
/*
aaa
{ value: 111, done: false }
bbb
{ value: 222, done: false }
ccc
{ value: 333, done: false }
ddd
{ value: undefined, done: true }
*/
每次给 next 方法传参给的就是给上一个结尾的 yield 语句当做返回值的,而不是当前的.
所以要传要从第二个开始传 (第一个也没法传,(他之前也没 yield))
# 12.3 生成器函数案例
案例 1:1s 后输出 111,然后 2s 后输出 222,然后 3s 后输出 333
- 传统方式:嵌套太多,代码复杂,产生 回调地狱。
setTimeout(() => {
console.log(111);
setTimeout(() => {
console.log(222);
setTimeout(() => {
console.log(333);
}, 3000);
}, 2000);
}, 1000);
- 生成器实现:结构简洁明了
function one() { // 第一个函数用作做第一个异步任务的
setTimeout(() => {
console.log(111);
iter.next();
}, 1000);
}
function two() { // 第二个函数用作做第二个异步任务的
setTimeout(() => {
console.log(222);
iter.next();
}, 2000);
}
function three() { // 第三个函数用作做第三个异步任务的
setTimeout(() => {
console.log(333);
}, 3000);
}
function* generator() {
yield one();
yield two();
yield three();
}
let iter = generator();
iter.next();
以上代码太妙了!!!
首先三个函数分别代表三个计时器每个即使器都有要执行的异步任务 (也就是先不执行,只有需要执行的时候才会去安排排队然后等主线程执行栈执行).
然后创建了一个生成器函数,有三个 yield 每个 yield 后面都对应一个异步任务 (计时器,不过计时器里设的异步任务), 然后调用了 next 方法会执行生成器第一段也就是开始到第一个 yield, 也会执行 (因为是加了括号,代表调用函数,函数的返回值作为 next 方法的返回对象的 value 值) yield 后面的那个 one 函数,然后执行 one 给加上了定时器,然后过了 1 秒 (roughly,more depend on how much tasks the js main thread has to do), 执行回调函数,这个执行函数里面有
iter.next();
所以就是让生成器执行现在开始 (也就是第一个 yield 之后) 一直到下一个 yield 然后因为那个 yield 后面也是个函数就执行了 (需要的是一个表达式作为 next 方法的返回对象的 value 值,而且这个后面跟的是一个调用而不是函数名,所以就会执行), 这个执行的函数是 two 函数,然后 so on… 就可以不用像之前一样嵌套这些定时器 (以及计时器里面的挨个的异步任务) 产生回调地狱.这么做也可以而且很整洁
案例 2:生成器函数模拟每隔 1s 获取商品数据
function getUsers() {
setTimeout(() => {
let data = '用户数据';
iter.next(data); // 传参给生成器函数的第 2 段,后面类似
}, 1000);
}
function getOrders() {
setTimeout(() => {
let data = '订单数据';
iter.next(data);
}, 1000);
}
function getGoods() {
setTimeout(() => {
let data = '商品数据';
iter.next(data);
}, 1000);
}
function* generator() {
let users = yield getUsers();
console.log(users);
let orders = yield getOrders();
console.log(orders);
let goods = yield getGoods();
console.log(goods);
}
let iter = generator();
iter.next();
这里跟上面很像,只不过在执行
getUsers()
函数的时候,这个函数里面有iter.next(data);
把自己的 data 作为参数传给 next 方法,这个是运行第二次执行 next 方法 (第一次就是在外面没有参数的,只是启动然后可以调用到第一个 yield 后面的getUsers()
函数), 所以这个第二次执行 next 方法传进的参数会成为第一个 yield (也就是上一次 next (也就是最一开始那个 next) 结尾到达的 yield) 语句的返回值的 (注意不是 next 函数执行完返回的那个对象!那个对象的 value 属性就是 yield 后面跟着的东西 (因为这里是函数所以会先调用取到这个函数的返回值 (这里是 undefined, 因为这三个函数都没有返回什么)), 而 done 值就是看有没有结束当前的生成器的所有部分 (三个 yield 给生成器分为了四段,所以除了一开始的 next 还需要执行三次 next)).所以给 next 方法传参数是给 yield 语句传返回值的,所以上面用了
iter.next(data);
把自己的 data 传进了第一个 yield 语句的返回值,所以第一个 yield 语句 ->yield getUsers();
不管 yield 后面是什么返回值就是那个 data, 然后我们用users
变量接收了一下,然后就是老样子执行下一个,下一个照样传,so on…这样就可以获取到每个异步任务执行完毕所获得的值等.
# 13. Promise
# 13.1 Promise 的定义和使用
Promise
是 ES6 引入的__异步编程的新解决方案__。语法上 Promise 是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果。
一个 Promise
必然处于以下几种状态之一:
- 待定(
pending
):初始状态,既没有被兑现,也没有被拒绝。 - 已兑现(
fulfilled
):意味着操作成功完成。 - 已拒绝(
rejected
):意味着操作失败。
Promise
的使用:
- Promise 构造函数:
new Promise((resolve, reject)=>{})
Promise.prototype.then
方法Promise.prototype.catch
方法
一个简单案例:
let p = new Promise(function (resolve, reject) { //实例化Promise对象
// 使用 setTimeout 模拟请求数据库数据操作
setTimeout(function () {
// 这个异步请求数据库数据操作是否正确返回数据
let isRight = true;
if (isRight) {
let data = '数据库中的数据';
// 设置 Promise 对象的状态为操作成功
resolve(data);
} else {
let err = '数据读取失败!'
// 设置 Promise 对象的状态为操作失败
reject(err);
}
}, 1000);
});
p.then(function (value) {
console.log(value);
}, function (reason) {
console.error(reason);
})
- 实例化 Promise 对象需要传进一个函数,这个函数有两个参数 (就是内部定的,跟之前 foreach 里的 value 和 index 那些很像), 当然这俩参数名随便写,一般我们是 resolve,reject
- 这两个参数其实是两个函数,由 js 引擎提供不用自己部署
- 在这个函数里面我们可以写个异步操作 (可以是整个定时器啥的), 反正只要里面会有属于异步任务就都可以放,毕竟这个 Promise 做的就是异步编程解决方案
- 之后在里面我们就可以调用那两个参数函数来改变我们这个 Promise 对象的状态
- 如果我们异步任务获取了想要的数据 data, 然后我们用成功 (参数) 函数
resolve(data)
- 我们可以在这个实例化 Promise 对象的外面使用这个实例化对象来调用
then
方法- 这个 then 方法需要两个参数,这两个参数也是两个函数,这两个参数函数每一个都有一个参数,一般第一个参数函数里面的参数我们称之为 value, 第二参数函数里面的参数我们称之为 reason
- 如果是
resolve(data)
那么就会执行 then 方法里面第一个参数 (函数) 里面的代码,其中这个函数的参数 (一般使用 value 来代表), 这个 value 就是这里我们传给 resolve 的那个 data 变量传的值- 同理如果是
reject(data2)
就用会调用 then 方法的第二个参数函数,然后这个第二个参数函数里面的函数 (一般使用 reason 来代表), 这个 reason 就是这里我们传给 reject 的那个 data2 变量传的值
- 注意使用了 resolve 或者 reject 都会退出当前函数
# 13.2 Promise 封装读取文件
// 使用 nodejs 的 fs 读取文件模块
const fs = require('fs');
const p = new Promise(function (resolve, reject) {
fs.readFile('./resources/为学.txt', (err, data) => {
// err 是一个异常对象
if (err) reject(err);
resolve(data);
})
})
p.then(function (value) {
// 转为字符串输出
console.log(value.toString());
}, function (reason) {
console.log('读取失败!!');
})
# 13.3 Promise 封装 Ajax 请求
const p = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('get', 'https://api.apiopen.top/getJoke');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
// 成功
resolve(xhr.response);
} else {
// 失败
reject(xhr.status);
}
}
}
});
// 指定回调
p.then(function (value) {
console.log(value);
}, function (reason) {
console.error(reason);
})
如果不用 Promise 封装,只是最原始的方法,就是只是放到 onreadystatechange 事件触发的回调函数里面操作得到的数据 / 错误.
而用了 Promise 封装,就是在这个异步任务之后,使用 then 的方法处理返回的数据 / 错误
注意这个 promise. then 中的那些函数是异步执行,那些函数就是回调函数,只有在 Promise 对象为 resovled/rejected/… 的时候调用
使用了 Promise 封装,既让整个看起来非常整洁
# 13.4 Promise.prototype.then 方法
先复习一下一个 Promise
的三种状态:
- 待定(
pending
):初始状态,既没有被兑现,也没有被拒绝。 - 已兑现(
fulfilled
):意味着操作成功完成。 - 已拒绝(
rejected
):意味着操作失败。
Promise.prototype.then
方法返回的结果依然是Promise
对象 (一个新的实例化的 Promise 对象),对象状态由回调函数的执行结果决定。
具体情况如下:
- 若 then 方法没有返回值,则
then
方法返回的对象的状态值为成功fulfilled
,返回结果值为undefined
const p = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve('用户数据')
reject('出错了');
}, 1000);
})
// 未设定返回值
const res = p.then((value) => {
console.log(value);
}, (reason) => {
console.warn(reason);
})
// 打印 then 方法的返回值
console.log(res);
打印的结果:
- 如果回调函数中返回的结果是非
Promise
类型的属性,则 then 方法返回的对象,其状态为成功(fulfilled
),返回结果值取决于then
方法所执行的是哪个函数(resolve
或reject
)。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve('用户数据')
reject('出错了');
}, 1000);
})
// 返回的非 Promise 对象
const res = p.then((value) => {
console.log(value);
return '成功了!!';
}, (reason) => {
console.warn(reason);
return '出错啦!!'
})
// 打印 then 方法的返回值
console.log(res);
打印结果:
看调用的哪个回调函数然后那个回调函数 return 后面就是 then 方法返回的 Promise 对象的 PromiseResult 属性的值
- 如果回调函数中返回的结果是
Promise
类型(return new Promise()
),则then
方法返回的Promise
对象状态与该返回结果的状态相同,返回值也相同。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('用户数据')
// reject('出错了');
}, 1000);
})
const res = p.then((value) => {
console.log(value);
// 返回 Promise 对象
return new Promise((resolve, reject) => {
resolve('(1)成功了!!!');
// reject('(1)出错了!!!')
})
}, (reason) => {
console.warn(reason);
return new Promise((resolve, reject) => {
// resolve('(2)成功了!!!');
reject('(2)出错了!!!')
})
})
// 打印 then 方法的返回值
console.log(res);
打印结果:
这里上面先是 resolve 所以 then 会调用第一个参数 (回调函数), 然后在那里面又返回了一个 Promise 对象也是 resolve 然后值为 “(1)成功了!!!” 所以最后 then 方法返回的是这个 Promise 对象,状态为 fulfilled, 值为 “(1)成功了!!!”
如果回调函数里面返回的 Promise 实例化对象里面是失败那当然就是失败状态
- 如果回调函数中返回的结果是
throw
语句抛出异常 (不管是不是在返回的新的 Promise 对象里面抛出 (像下面那一样) 还是直接就是抛,都一样),则then
方法返回的实例化 Promise 的对象的状态值为rejected
,返回结果值为throw
抛出的字面量或者Error
对象。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('用户数据');
}, 1000);
});
const res = p.then((value) => {
console.log(value);
return new Promise((resolve, reject) => {
throw new Error('错误了!!');
})
});
// 打印结果
console.log(res);
打印结果如下:
如果调用 then 的 Promise 那个实例化对象里面 throw new … 那么也会调用 then 里面的第二个参数 (回调函数), 跟
reject
的一样
# 13.4 链式调用
Promise.prototype.then
方法返回的结果还是 Promise
对象,这意味着我们可以继续在该结果上使用 then
方法,也就是链式调用。
const p = new Promise((resolve,reject)=>{});
p.then(value=>{}, reason=>{}).then(value=>{}, reason=>{}).then(value=>{}, reason=>{})
...
因为 then 方法也是返回的一个 Promise 对象,我们可以继续嵌套放 then 方法对于之前那个 then 方法返回的 promise 对象的状态进行对应的操作.
每个 then 方法两个参数 (分别是两个回调函数), 所以都是异步任务.
注意 then 方法可以只放一个参数 (回调函数), 这意味着只有调用当前这个 then 方法的那个 Promise 实例化对象的状态是 fulfilled (还是 resolved 来着) 才会调用这个回调函数 (让这个异步任务去排队等着执行)
这么做可以解决回调地狱问题
# 13.5 链式调用练习 - 多个文件读取
如果是不用 Promise, 回调地狱: (而且可以看到取名也很麻烦,都要不同的变量名,这里 err 不用就懒得改了)
如果使用 Promise:
const fs = require('fs');
let p = new Promise((resolve, reject) => {
fs.readFile('./resources/users.md', (err, data) => {
// 传给下一轮文件读取操作
resolve(data);
})
});
p.then(value => {
return new Promise((resolve, reject) => {
// value 为第一次读取的文件数据,data 为第二次(当前)读取的数据
fs.readFile('./resources/orders.md', (err, data) => {
// 将上轮读取结果和本轮合并传到下一轮轮读取操作
resolve([value, data]);
});
});
}).then(value => {
return new Promise((resolve, reject) => {
fs.readFile('./resources/goods.md', (err, data) => {
// value 为上一轮传递过来的文件数据数组
value.push(data);
// 传给下一轮操作
resolve(value);
});
});
}).then(value => {
// 合并数组元素,输出
console.log(value.join('\n'));
});
这里就是先建个 Promise 实例化对象,然后读取第一个文件 (读取也有回调函数作为参数,这个回调函数也是异步), 回调函数里面就是 resolve 获得的 data
接着就可以对这个 Promise 实例化对象使用 then 方法然后因为这个 Promsie 实例化对象是 resolve 状态我们就在 then 方法第一个参数 (回调函数) 去将这个接收到的数据和要读取的第二个文件放一起,怎么读取第二个文件?
跟读取第一个文件一样新建一个 Promise 实例化对象然后 return 作为当前 then 方法的返回值
注意这里不能不建新的 Promise 对象然后直接读取文件,因为这样是可以读取到第二个文件,但是获取到的第二个文件中的 data 无法给别人,只是自己有了,所以我们需要这个 Promise 对象包括住读取这个文件,然后再讲读取到的 data 与读取第一个文件中的 data 拼接一下然后 resolve 这个样子我们这个现在当前 then 方法返回的就是这个新建的 Promise 实例化对象然后这个对象的 PromiseResult 属性存的值 (也就是通过这个 Promise 实例对象调用 then 方法中第一个参数 (回调函数) 里面的那个 (系统默认给我们的) 那个参数) 存的就是上面我们传的拼接好的从两个文件读取出来的 data, 我们对这个实例化对象继续调用 then 方法来按照那个对象的状态调用函数然后用调用的函数里面 (系统默认给我们的) 的参数来处理所有接收到的 data.
# 13.6 Promise.prototype.catch
catch()
方法返回一个 Promise
,并且处理拒绝的情况。它的行为与调用 Promise.prototype.then(undefined, onRejected)
相同。
Internally calls
Promise.prototype.then
on the object upon which is called, passing the parametersundefined
and theonRejected
handler received; then returns the value of that call (which is aPromise
).
即:
obj.catch(onRejected);
等同于:
obj.then(undefined, onRejected);
语法:
p.catch(onRejected);
p.catch(function(reason) {
// 拒绝
});
举例:
var p1 = new Promise(function (resolve, reject) {
resolve('Success');
});
p1.then(function (value) {
console.log(value); // "Success!"
throw 'oh, no!';
}).catch(function (e) {
console.log(e); // "oh, no!"
}).then(function () {
console.log('有 catch 捕获异常,所以这句输出');
}, function () { // then的
console.log('没有 catch 捕获异常,这句将不会输出');
});
输出结果:
Success
oh, no!
有 catch 捕获异常,所以这句输出
catch
不管被连接到哪里,都能捕获上层__未捕捉过__的错误。所以一般是放到最后,前面可能会有多个 then (链式调用) 且都没第二个回调函数 (要是有了可能就出问题了因为如果前面的 promise 被 reject 了会被这个 then 的第二个参数回调函数捕获然后调用然后返回一个 promise, 这个 promise 可能会是 resolve, 那么之后 catch 就接收不到了), 所以会有多个 Promise 对象,然后这些对象任何错误,只要没被以及捕获 (then 的第二函数), 那么最后 catch 都会接收到
# 一、reject 后的东西,一定会进入 then 中的第二个回调,如果 then 中没有写第二个回调,则进入 catch
var p1=new Promise((resolve,rej) => {
console.log('没有resolve')
//throw new Error('手动返回错误')
rej('失败了')
})
p1.then(data =>{
console.log('data::',data);
},err=> {
console.log('err::',err)
}).catch(
res => {
console.log('catch data::', res)
})
VM367054:2 没有resolve
VM367054:11 err:: 失败了
- then 中没有第二个回调的情况
var p1=new Promise((resolve,rej) => {
console.log('没有resolve')
//throw new Error('手动返回错误')
rej('失败了')
})
p1.then(data =>{
console.log('data::',data);
}).catch(
res => {
console.log('catch data::', res)
})
VM367054:2 没有resolve
VM367054:11 catch data:: 失败了
- 如果没有 then, 也可以直接进入 catch
var p1=new Promise((resolve,rej) => {
console.log('没有 resolve')
//throw new Error('手动返回错误')
rej('失败了')
})
p1.catch(
res => {
console.log('catch data::', res)
})
VM367087:2 没有resolve
VM367087:9 catch data:: 失败了
# 二、resolve 的东西,一定会进入 then 的第一个回调,肯定不会进入 catch
var p1=new Promise((resolve,rej) => {
console.log('resolve')
//throw new Error('手动返回错误')
resolve('成功了')
})
p1.then(data =>{
console.log('data::',data);
}).catch(
res => {
console.log('catch data::', res)
})
VM367087:2 resolve
VM367087:9 data:: 成功了
- 不会进入 catch 的情况
var p1=new Promise((resolve,rej) => {
console.log('resolve')
//throw new Error('手动返回错误')
resolve('成功了')
})
p1.catch(
res => {
console.log('catch data::', res)
})
VM367087:2 resolve
throw new Error 的情况和 rej 一样,但是他俩只会有一个发生
另外,网络异常(比如断网),会直接进入 catch 而不会进入 then 的第二个回调
# 14. Set
# 14.1 Set 的定义和使用
ES6 提供了新的数据结构 Set
(集合)。它类似于数组,但 成员的值都是唯一的,集合实现了 iterator
接口,所以可以使用『扩展运算符』(也就是 "…") 和『 for...of
』进行遍历。
定义一个 Set 集合:
let st1 = new Set();
let st2 = new Set([可迭代对象]); //传的是数组,然后数组里面存的就是任何,然后我们之后就可以迭代里面的
集合(这里假设有一个集合 st
)的属性和方法:
st.size
:返回集合个数st.add(item)
:往集合中添加一个新元素item
,返回当前集合st.delete(item)
:删除集合中的元素,返回boolean
值st.has(item)
:检测集合中是否包含某个元素,返回boolean
值st.clear()
:清空集合- 集合转为数组:
[...st]
- 合并两个集合:
[...st1, ...st2]
# 14.2 集合案例
案例 1: 数组去重
let arr1 = [1, 2, 2, 3, 3, 3, 4, 1, 2];
let res1 = [...new Set(arr1)];
console.log(res1); // [ 1, 2, 3, 4 ]
自动给我们传进去的数组去重
案例 2:数组求交集
let arr2_1 = [1, 2, 2, 3, 4, 5];
let arr2_2 = [3, 6, 6, 7, 1, 4];
let res2 = arr2_1.filter(v => new Set(arr2_2).has(v))
console.log(res2); // [ 1, 3, 4 ]
案例 3:数组求并集
let arr3_1 = [1, 2, 2, 3, 4, 5];
let arr3_2 = [3, 6, 6, 7, 1, 4];
let res3 = [...new Set([...arr3_1, ...arr3_2])];
console.log(res3); // [ 1, 2, 3, 4, 5, 6, 7 ]
案例 4:数组求差集
let arr4_1 = [1, 2, 2, 3, 4, 5];
let arr4_2 = [3, 6, 6, 7, 1, 4];
let res4 = [...new Set(arr4_1)].filter(v => !(new Set(arr4_2).has(v)))
console.log(res4); // [ 2, 5 ]
# 15. Map
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合。但是 “键” 的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map 也实现了 iterator 接口,所以可以使用『扩展运算符』和『for…of』进行遍历。
定义一个 Map:
let mp1 = new Map();
mp1.set('aaa', 111);
mp1.set('bbb', 222);
mp1.set('ccc', 333);
let mp2 = new Map([
['aaa', 111],
['bbb', 222],
['ccc', 333]
]);
console.log(mp1['aaa']); // 111
console.log(mp2.get('bbb')); // 222
Map 的属性和方法:( k
为键, v
为值)
size
:返回 Map 的元素(键值对)个数set(k, v)
:增加一个键值对,返回当前 Mapget(k)
:返回键值对的键值has()
:检测 Map 中是否包含某个元素clear()
:清空集合,返回undefined
使用 for…of 遍历一个 map 会获取到多个数组,第一个数组的第一个元素是这个 map 存的第一个键对象 (或者普通数据类型) 第二元素是这个 map 存的第一个值对象 (或者普通数据类型)
# 16. class 类
这部分在 JS 高级也涉及,故可以前往 JS 高阶 class 学习。那部分的笔记更加详细,有原理。所以这一节后面部分只给出例子。
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。基本上,ES6 的 class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
# 16.1 复习 ES5 function 构造函数 的继承
具体知识点可以前往:JS 高级
//手机
function Phone(brand, price){
this.brand = brand;
this.price = price;
}
Phone.prototype.call = function(){
console.log("我可以打电话");
}
//智能手机
function SmartPhone(brand, price, color, size){
Phone.call(this, brand, price);
this.color = color;
this.size = size;
}
//设置子级构造函数的原型
SmartPhone.prototype = new Phone;
// 矫正 constructor 指向
SmartPhone.prototype.constructor = SmartPhone;
//声明子类的方法
SmartPhone.prototype.photo = function(){
console.log("我可以拍照")
}
SmartPhone.prototype.playGame = function(){
console.log("我可以玩游戏");
}
const chuizi = new SmartPhone('锤子',2499,'黑色','5.5inch');
console.log(chuizi);
# 16.15 ES5 和 ES6 静态成员,静态方法等
# ES5
记得再这样 ES5 里,构造函数本身也是个对象,所以可以直接给这个对象加属性,但是之后要是创建了这个构造函数的实例化对象,这个实例化对象是无法 access 之前给构造函数对象添加的属性的,这些属性时__静态成员__.
(直接给构造函数对象添加的函数属性也是无法通过构造函数的实例化对象调用的,一般方法是需要放到构造函数对象的原型对象上)
同理,给实例化对象加的属性 (属性,方法等) 是无法用原来的构造函数对象获取的,这些是__实例成员.__
是无法让构造函数对象和构造函数实例化对象相同的,要想相同就要用构造函数的原型对象 \
# ES6
一样,只要在类里面,在属性或者方法前面加上
static
, 这些属于静态成员 / 方法实例化对象是无法调用或使用的,只能通过这个类的对象本身 (貌似在 ES6 之中,这个类本身也是个对象,跟 ES5 中构造函数本身就是一个对象一样)
这点跟 java 不一样,java 实例化对象还是可以 access 到静态成员 / 方法,这里不允许
# 16.2 extends 类继承和方法的重写
ES6 中直接使用 extends
语法糖(更简洁高级的实现方式)来实现继承,同时可以重写父类的方法,直接在子类中重新写一次要重写的方法即可覆盖父类方法。
class Phone{
//构造方法
constructor(brand, price){
this.brand = brand;
this.price = price;
}
//父类的成员属性
call(){
console.log("我可以打电话!!");
}
}
class SmartPhone extends Phone {
//构造方法
constructor(brand, price, color, size){
super(brand, price);// Phone.call(this, brand, price)
this.color = color;
this.size = size;
}
photo(){
console.log("拍照");
}
// 方法的重写
call(){
console.log('我可以进行视频通话');
}
}
const xiaomi = new SmartPhone('小米',799,'黑色','4.7inch');
xiaomi.call();
xiaomi.photo();
- 类里面的构造函数必须是 constructor
- 类里面的方法必须是
方法名(...){...}
, 不可以像我们在之前对象里面的写法 ->方法名:function(...){...}
- 注意类里面直接写的方法都会默认加到该类的 prototype 存的原型属性的身上,而不是像我们 ES5 那样特意要在外面给那个原型对象加上个属性存我们这个共享的方法
- 用类实现继承就很简单,很像 java, 不用 es5 那么麻烦
- 其实底层实现原理应该就是 es5 那么做,都是把子类对象的 prototype 设为 new 出来的父类的实例化对象,只是这样我们不用搞那么麻烦的,直接这样写就很方便
- 可以不写 constructor, 也是可以的
- 在子类声明跟父类重名的方法就会覆盖,要用父类就是 super. 方法名 ();
# 16.3 getter 和 setter
实际上, getter
和 setter
是 ES5(ES2009)提出的特性,这里不做详细说明,只是配合 class
使用举个例子。
当属性拥有 get
/ set
特性时,属性就是访问器属性。代表着在访问属性或者写入属性值时,对返回值做附加的操作。而这个操作就是 getter
/ setter
函数。
使用场景: getter
是一种语法,这种 get
将对象属性绑定到 查询该属性时将被调用的函数。适用于某个需要动态计算的成员属性值的获取。 setter
则是在修改某一属性时所给出的相关提示。
class Test {
constructor(log) {
this.log = log;
}
get latest() {
console.log('latest 被调用了');
return this.log;
}
set latest(e) {
console.log('latest 被修改了');
this.log.push(e); //这里才是真正修改了
}
}
let test = new Test(['a', 'b', 'c']);
// 每次 log 被修改都会给出提示,(并不会真正让test.latest变成'd')
test.latest = 'd';
// 每次获取 log 的最后一个元素 latest,都能得到最新数据。
console.log(test.latest);
以上输出:
latest 被修改了
latest 被调用了
[ 'a', 'b', 'c', 'd' ]
get 成员AAA () {...; return 返回的值}
, 然后实例化对象.成员AAA
就是那个返回的值
- 每次调用
成员AAA
, 因为有这个get 成员AAA
, 就会运行这个get 成员AAA
里面的代码
set 成员AAA (XXX) {...;}
, 然后实例化对象.成员AAA = BBB
就会把这个 BBB 传给那个 XXX 然后执行里面的代码
- set 必须需要一个参数
- 每次修改
成员AAA
的值,因为有这个set 成员AAA
, 就会运行这个set 成员AAA
里面的代码,这里必须需要一个参数,那这个参数就是我们修改成员AAA
给的新值- 一般都是在这里 set 那里面的代码测试修改的新值符不符合我们要求,符合我们就在里面修改那个值,记住我们在外面修改这个值只是尝试并执行 set 里面的代码,真正修改不修改要看我们 set 里面的代码有没有写让他修改
# 17. 数值扩展
Number.EPSILON
是 JavaScript 表示的最小精度,一般用来处理浮点数运算。例如可以用于两个浮点数的比较。
let equal = (x, y) => Math.abs(x - y) < Number.EPSILON;
console.log(0.1 + 0.2 === 0.3); // false
console.log(equal(0.1 + 0.2, 0.3)); // true
- 二进制和八进制:二进制以
0b
开头,八进制以0o
开头。
let b = 0b1010;
let o = 0o777;
let d = 100;
let x = 0xff;
console.log(x);
-
Number.isFinite
检测一个数值是否为有限数。console.log(Number.isFinite(100)); // false
console.log(Number.isFinite(100 / 0)); // true
console.log(Number.isFinite(Infinity)); // false -
Number.parseInt 和 Number.parseFloat
ES6 给 Number
添加了 parseInt
方法, Number.parseInt
完全等同于 parseInt
。将字符串转为整数,或者进行进制转换。 Number.parseFloat
则等同于 parseFloat()
Number.parseInt === parseInt; // true
Number.parseFloat === parseFloat; // true
Number.parseInt(s, base);
s
:待转换的字符串base
:进位制的基数
console.log(Number.parseInt('5211314love')); // 5211314
console.log(Number.parseFloat('3.1415926神奇')); // 3.1415926
Number.isInteger()
判断一个数是否为整数。
console.log(Number.isInteger(5)); // true
console.log(Number.isInteger(2.5)); // false
Math.trunc()
将数字的小数部分抹掉。
console.log(Math.trunc(3.5)); // 3
Math.sign
判断一个数到底为正数 负数 还是零
上面有些方法本来就有,只不过是全局的函数,现在我们是将他们用 Number 也可以调用,成为 Number 对象的属性.
这么干就可以让别人更好知道在文档里去哪找那个函数
# 18. 对象方法扩展
ES6 新增了一些 Object
对象的方法。
- Object.is 比较两个值是否严格相等,与『===』行为 基本一致
Object.assign
对象的合并,将源对象的所有可枚举属性,复制到目标对象__proto__
、setPrototypeOf
、setPrototypeOf
可以直接设置对象的原型
# 18.1 Object.is()
Object.is()
方法判断两个值是否完全相同。 Object.is
比较两个值是否严格相等,与 ===
行为 基本一致。返回一个 Boolean
类型。
Object.is(value1, value2);
Object.is()
方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:
- 都是
undefined
- 都是
null
- 都是
true
或false
- 都是相同长度的字符串且相同字符按相同顺序排列
- 都是相同对象(意味着每个对象有同一个引用)
- 都是数字且
- 都是
+0
- 都是
-0
- 都是
NaN
- 或都是非零而且非
NaN
且为同一个值
- 都是
与 ==
运算不同。 ==
运算符在判断相等前对两边的变量(如果它们不是同一类型)进行强制转换 (这种行为的结果会将 "" == false
判断为 true
),而 Object.is
不会强制转换两边的值。
与 ===
算也不相同。 === 运算符 (也包括 ==
运算符) 将数字 -0
和 +0
视为相等,而将 Number.NaN
与 NaN
视为不相等。
# 18.2 Object.assign
Object.assign
对象的合并,相当于浅拷贝。
注意跟完全浅拷贝不一样的是如果接收合并的对象有被合并的对象没有的属性,那么合并后接收合并的对象依然会有那个属性
const config1 = {
host: 'localhost',
port: 3306,
name: 'root',
pass: 'root',
test: 'test'
};
const config2 = {
host: 'http://atguigu.com',
port: 33060,
name: 'atguigu.com',
pass: 'iloveyou',
test2: 'test2'
}
console.log(Object.assign(config1, config2));
# 18.3 Object.setPrototypeOf 和 Object.getPrototypeof
Object.setPrototypeOf
用于设置对象的原型对象, Object.getPrototypeof
用于获取对象的原型对象,相当于 __proto__
。
const school = {
name: '尚硅谷'
}
const cities = {
xiaoqu: ['北京','上海','深圳']
}
Object.setPrototypeOf(school, cities);
console.log(Object.getPrototypeOf(school));
console.log(school);
# 19. ES6 模块化
模块化是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来。
# 19.1 模块化的好处
模块化的优势有以下几点:
- 防止命名冲突
- 代码复用
- 高维护性
# 19.2 模块化规范产品
ES6 之前的模块化规范有:
- CommonJS => NodeJS、Browserify
- AMD => requireJS
- CMD => seaJS
# 19.3 ES6 模块化语法
模块功能主要由两个命令构成: export
和 import
。
export
命令用于规定模块的对外接口import
命令用于输入其他模块提供的功能
# 19.3.1 模块导出数据语法
- 单个导出
// 单个导出
export let uname = 'Rick';
export let sayHello = function () {
console.log('Hi, bro!');
}
- 合并导出
let uname = 'Rick';
let sayHello = function () {
console.log('Hi, bro!');
}
// 合并导出
export { uname, sayHello };
- 默认导出
一个 js 文件中只能有一个默认暴露
这是因为默认暴露比如说下面的这个对象,然后我们导入
import XXX from 那个js文件
,然后就这个 XXX 就相当于是这里导出的对象,可以 XXX.uname 啊 XXX.sayHello () 等要是这个 js 文件还有另外一个默认暴露一个对象,或者其他的,那到时候我们导入,XXX 到底指的是导出的哪一个?
所以一个 js 文件只能有一个默认导出,其他的要么合并导出,要么单个导出,然后接收的时候对于合并导出以及单个导出一般都是__解构赋值导入__, 这样就更方便,直接用 (看下面), 注意__解构赋值导入__其实跟我们之前学的解构对象一样,这里导出的不管是合并导出还是单个导出其实就是一整个就是个对象,然后我们导入的时候解构我们就需要用到那个值 (比如说导出的是 uname,sayHello 等,那么在解构赋值导入时那个 {} 里面也需要用 uname,sayHello 等), 不过我们可以用
as
取别名
// 默认导出
export default {
uname: 'Rick',
sayHello: function () {
console.log('Hi, bro!');
}
}
# 19.3.2 模块导入数据语法
- 通用导入方式
import * as m1 from './js/m1.js';
import * as m2 from './js/m2.js';
import * as m3 from './js/m3.js';
// m1,m2,m3都是变量可以在当前文件里可以使用,注意返回的是对象,对象属性存的则是我们设的各种数据或者方法
注意默认导出 (default), 这个 default 本身会是一个返回的对象的属性,然后这个 default 属性存的对象里面的属性才是我们设的各种数据或者方法
相当于是多了一层,调用的话就比如说是 m3.default.(我们设的方法或者数据)…;
- 解构赋值导入
import { uname, sayHello } from './js/m1.js';
// 有重复名可以设置别名
import { uname as uname2, sayHello as sayHello2 } from './js/m2.js';
console.log(uname);
// 配合默认导出
import {default as m3} from "./src/js/m3.js"; //default必须取别名不能直接default
上面做法就__不用__通过 m1.XX,m2.XX 来调用我们之前 export 的方法或者数据,直接用结构用的变量就行 (或者取得别名)
注意 default 方式还是需要 (比方说在这里) m3.XX (中间不需要 default 了)
- 简便方式导入,针对默认暴露
import m3 from "./src/js/m3.js";
重要!这种只能对我们 export 时使用 default 的方式时使用
这个 import 其实跟 ``import {default as m3} from “./src/js/m3.js”;` 一样, 我们这种方式直接用 m3.XXX 就行了
这种引入方式其实就是把 {default as m3} 省略成了 m3 (直接用别名)
所以需要是 default 的引出才可以这样引入,其他引出方式不行的
# 19.3.3 ES6 使用模块化方式二
注意在 html 文件里面的 script 标签之中要 import 或者 script 标签的 src 属性的 js 文件里有 import 都需要给这个 script 标签一个 type 属性,值为 module
将文件导入都写进一个 app.js 文件中,然后在里面写入要导入的模块。app.js 中的内容如下:
import * as m1 from './js/m1.js';
import * as m2 from './js/m2.js';
import * as m3 from './js/m3.js';
console.log(m1);
console.log(m2);
console.log(m3);
在 index.html 中引入 app.js 文件内容:
<script src="./app.js" type="module"></script>
# 19.4 使用 babel 对模块化代码转换
有的浏览器不支持 ES6 语法,这时候就需要使用 babel 来将其转换成 ES5 等价语法。
- 安装工具
npm i babel-cli babel-preset-env browserify(webpack) -D
- 编译 (看 es6 js 文件然后创建出来对应的 es5 文件)
npx babel src/js -d dist/js --presets=babel-preset-env
-
打包 (将创建出来的 es5 文件预编译打包 nodejs 模块,使之能够在浏览器端使用)
这个只需要对我们 App.js (主入口) 文件产生的对应的 es5 js 文件使用,只用打包这一个,因为放到这个 html script 标签里的就是打完包的它
npx browserify dist/js/app.js -o dist/bundle.js
我们之后会学到 webpack 代替这些
为什么需要这个打包?
我们要是之前在原来 es6 js 文件中导入了其他 js 文件 (模块), 那么按照 es6 生成的 es5 js 文件里面会把那些 import 语句变换成 require, 也就是我们 common.js 里面那种方式 (导出方式也一样)
这个 require (等) 语句是属于 node 里面的,要是想让这个 js 文件用到浏览器端就需要打包了不然无法识别
最后就可以将打包之后生成的文件 (一般是 App.js 那个文件最后编译然后打包后的文件,因为这文件里面就导入了其他的各种 js 模块等进行操作) 放到我们 index.html 的 script 标签里,这样子就很整洁,就一个 script 标签 (或许还有但不会太多), 解决了我们之前没有 js 模块化时候的各种处理方式 (参考 “js 模块化.md”) 的麻烦
哦,对了,记得那个 script 标签里面加上
type="module"
# 19.5 ES6 模块化引入 npm 安装的包
先下载再导入
npm install jquery
再通过 import
导入即可。
上面说的应该是 require (node 里面的,属于 common.js) 方法,不确定 import (es6 模块化方式) 会不会是一样的
# ECMASript 7 新特性
# 1. Array.prototype.includes
includes
方法用来检测数组中是否包含某个元素,返回布尔类型值。
# 2. 指数运算符
在 ES7 中引入指数运算符 **
,用来实现幂运算,功能与 Math.pow(a, b)
结果相同。
2 ** 3 // 8
Math.pow(2, 3) // 8
# ECMAScript 8 新特性
# 1. async 和 await
async
和 await
两种语法结合可以让异步代码像同步代码一样。(即:看起来是同步的,实质上是异步的。)
先从字面意思理解, async
意为异步,可以用于声明一个函数前,该函数是异步的。 await
意为等待,即等待一个异步方法完成。
# 1.1 async
async
声明( function
)的函数成为 async 函数,语法:
async function funcName() {
//statements
}
async
内部可以使用 await
,也可以不使用。 async
函数的返回值是一个 Promise
对象,因此执行这个函数时,可以使用 then
和 catch
方法。 根据 函数体内部 的返回值, async
函数返回值具体情况如下:
- 函数体内不返回任何值,则
async
函数返回值为一个成功(fulfilled
)的Promise
对象,状态值为undefined
let a = async function() {}
let res = a()
console.log(res)
// Promise{<fullfilled>: undefined}
- 返回结果__不是__一个
Promise
,则async
函数返回值为一个成功(fulfilled
)的Promise
对象,状态值为这个内部返回值。
let a = async function () {
return 'hello'
}
let res = a()
console.log(res)
// Promise{<fullfilled>: 'hello'}
- 内部抛出错误,则
async
函数返回值为一个失败的Promise
对象。
let a = async function foo() {
throw new Error('出错了')
}
a().catch(reason => {
console.log(reason)
})
- 若函数内部返回值是一个
Promise
对象,则async
函数返回值的状态取决于这个Promise
对象 (resolve 为 fulfilled,reject 为 rejected 状态)。
let a = async function () {
return new Promise((resolve, reject) => {
resolve("成功")
})
}
a().then(value => {
console.log(value)
})
就跟我们之前 Promise 的 then 方法很像
# 1.2 await
await
相当于一个运算符,右边接一个值。一般为一个 Promise
对象,也可以是一个非 Promise
类型。当右接一个非 Promise
类型, await
表达式返回的值就是这个值;当右接一个 Promise
对象,则 await
表达式会阻塞后面的代码,等待当前 Promise
对象 resolve
的值。
-
综合
async
和await
而言。await
必须结合async
使用,而async
则不一定需要await
。 -
async
会将其后的函数的返回值封装成一个Promise
对象,而await
会等待这个Promise
完成,然后返回resolve
的结果。 -
当这个
Promise
失败或者__抛出异常__时,需要时使用try-catch
捕获处理。
Promise
使用链式调用解决了传统方式回调地狱的问题,而async-await
又进一步优化了代码的可读性。
const p = new Promise((resolve, reject)=>{
resolve('成功')
})
async function main() {
let res = await p
console.log(res)
}
main()
// '成功'
const p = new Promise((resolve, reject)=>{
reject('失败')
})
async function main() {
try {
let res = await p
console.log(res)
} catch(e) {
console.log(e)
}
}
main()
// '失败'
await 后面跟着非 Promise 对象那么这个 await 语句返回的结果就是这个后面的非 Promise 对象
await 后面跟着 Promise 对象那么这个 await 语句返回的结果就是这个后面的 Promise 对象__resolve__的值 (不是状态), 如果是 reject 则是会被 try catch 接收到,然后 e 就代表了那个 reject 的值
# 1.3 综合应用 - 读取文件
需求:先读取用户数据 user,然后读取订单数据 order,最后读取商品数据 goods。
对于这种异步操作很容易想到使用 Promise
,代码如下:
const fs = require('fs')
let p = new Promise((resolve, reject) => {
fs.readFile('./files/user.md', (err, data) => {
if (err) reject(err)
resolve(data)
})
})
p.then(value => {
return new Promise((resolve, rejecet) => {
fs.readFile('./files/order.md', (err, data) => {
if (err) rejecet(err)
resolve([value, data])
})
})
}, reason => {
console.log(reason)
}).then(value => {
return new Promise((resolve, reject) => {
fs.readFile('./files/goods.md', (err, data) => {
if (err) reject(err)
value.push(data)
resolve(value)
})
})
}, reason => {
console.log(reason)
}).then(value => {
console.log(value.join('\n'))
}, reason => {
console.log(reason)
})
但是,使用 Promise
链式调用虽然避免了回调地狱,但这种链式调用过多难免引起代码复杂,看起来不直观。可以使用 async
和 await
方法优化,代码如下:
const fs = require('fs')
function readUser() {
return new Promise((resolve, reject) => {
fs.readFile('./files/user.md', (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
function readOrder() {
return new Promise((resolve, reject) => {
fs.readFile('./files/order.md', (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
function readGoods() {
return new Promise((resolve, reject) => {
fs.readFile('./files/goods.md', (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
async function read() {
try{
let user = await readUser()
let order = await readOrder()
let goods = await readGoods()
console.log([user, order, goods].join('\n'))
}catch(e){
console.err(e);
}
}
read()
这样,代码看起来很直观,就好像是同步代码一样,实际上是异步操作。
async,await 想法就是将异步操作封装到一个 Promise 对象里面,每个异步操作对应一个 Promise 对象。这个 Promise 对象的状态和值都会按照我们具体异步操作的来决定
再将每一个 Promise 封装到一个函数里面,这个函数的返回值就是这个 Promise 对象
这样我们就可以再创建一个函数用 async 修饰,这么做这个函数默认__返回的就是个 Promise 对象 (很重要)__, 而这个对象具体状态以及值由我们写的而决定,async 函数会是异步
之后我们就可以再这个 async 函数里面执行那些有着异步操作的函数 (which returns a Promise 对象), 在每一个函数之前需要加上 await, 加上 await 就会__等 (异步,不是同步)__让他后面那个函数执行完 (因为返回出来的是个 Promise 对象)
如果这个函数的返回的 Promise 对象是 resolve, 那么这整个 await 语句就会返回这个 resolve 的值 (我们可以拿个变量接收)
如果这个函数的返回的 Promise 对象是 reject, 则会 try catch 到,执行 catch 语句中的.
这么干最后可以将所有用变量接收的数据聚齐再在这个 async 函数里面 return new Promise…, 在把这个数据用 resolve 传出去
如果是之前的触发了 try catch 那么就在 catch 语句里面也是 return new Promise…, 在把这个错误用 reject 传出去
这样再拿一个变量接收这个 async 函数的返回值,返回的就是个 Promise 对象,然后我们可以用 then 方法等看该对象的状态进行操作
记得那个 async 函数需要我们调用,不然只是声明
async,await 结合就很像我们正常用函数接收返回值等等等,只不过在前面加上个 await 罢了
# 1.4 综合应用 - 封装 ajax
function sendAjax(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.send()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response))
}
reject(xhr.status)
}
}
})
}
async function main() {
let res = await sendAjax('http://poetry.apiopen.top/sentences')
let poem = res.result.name + '——' + res.result.from
document.body.innerText = poem
}
main()
这里封装的 ajax 还不能体现 async-await
的作用所在,因为没有出现多个 ajax 请求。在又多个 ajax 请求并且后续的请求依赖于前一个请求的结果的时候, async-await
的优点就体现出来了。
总结:
一个函数如果加上 async ,那么该函数就会返回一个 Promise。
await 只能在 async 函数中使用,可以把 async 看成将函数返回值使用 Promise.resolve () 包裹了下。
async 和 await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。
缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
任何一个
await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
The
await
expression causesasync
function execution to pause until aPromise
is settled (that is, fulfilled or rejected), and to resume execution of theasync
function after fulfillment. When resumed, the value of theawait
expression is that of the fulfilledPromise
.If the
Promise
is rejected, theawait
expression throws the rejected value.or throw new Error:
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。If the value of the expression following the
await
operator is not aPromise
, it’s converted to a resolved Promise.
如果多个 await 然后每个后面都是一个函数 (有着异步操作的), 那么还是会排队,后面异步等前面异步结束 (因为 await 的存在)
要是这些异步任务没有依赖 (后面的不需要前面的发生了才能发生), 就可以:
多个 await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中, getFoo
和 getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有 getFoo
完成以后,才会执行 getBar
,完全可以让它们同时触发。
这是因为 await 必须要等到当前那个异步操作结束才可以继续下一个
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
// 这第二个写法是先让那些异步函数排队(等着执行),也就相当于触发了异步操作
// 然后之后的await就会等那个对应的异步操作返回的结果,因为这种方式先让他们都触发了,就比之前块了
上面两种写法,
getFoo
和getBar
都是同时触发,这样就会缩短程序的执行时间。
# 2. Object.values 和 Object.entries
Object.values()
方法返回一个给定对象的所有可枚举属性值的数组,类似于Object.keys()
,只是前者返回属性值,后者返回键值组合的数组。
let obj = {
a: 1,
b: {1:2},
c: [1,2,3]
}
console.log(Object.values(obj))
// [1, {1: 2}, [1,2,3]]
console.log(Object.keys(obj))
// ['a', 'b', 'c']
Object.entries()
方法返回一个给定对象自身可遍历属性[key,value]
的数组(数组元素也是一个个的数组的数组)
const obj = {a: 1, b: 2, c: 3};
console.log(Object.entries(obj))
// [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]
返回的是一个数组,这样就可以使用`for...of`遍历了。
const obj = { a: 1, b: 2, c: 3 };
for (let [k, v] of Object.entries(obj)) {
console.log(k, v)
}
# ECMAScript 9 新特性
可以对对象使用 rest 参数和…(扩展元素符)
rest 参数:
function f1({host,port,...user}){}
f1({
host:'',
port:'',
a:'hi',
b:'bye'
});
上面 user 就代表了一个对象这个对象有个 a 属性 (值为 hi) 和 b 属性 (值为 bye)
这里跟数组不同的是,数组里 (一般用 arg 代表剩下所有的参数) 那个都是用数组存,对象里面剩下所有用 rest 代表会被以对象的形式代表
…(扩展元素符):
const obj1 = {...};
const obj2 = {...};
const obj3 = {...};
const all = {...obj1,...obj2,...obj2}
用… 把每一个对象里面的键和值都分成多个参数,所以最后 all 就是有这上对象的所有属性与值
就跟数组用的方式是一样的
# ECMAScript 10 新特性
# 1. Object.fromEntries
Object.fromEntries () 方法把可迭代对象的键值对列表转换为一个对象。
语法:
Object.fromEntries(iterable)
iterable
:类似 Array 、 Map 或者其它实现了可迭代协议的可迭代对象。- 返回值:一个由该迭代对象条目提供对应属性的新对象。
- 相当于
Object.entries
(ES8)的逆运算。
const mp = new Map([
[1, 2],
[3, 4]
])
const obj = Object.fromEntries(mp)
console.log(obj)
// { '1': 2, '3': 4 }
const arr = [[1, 2]]
console.log(Object.fromEntries(arr))
// {'1': 2}
# 2. trimStart () 和 trimEnd ()
trimStart()
去除字符串开头连续的空格(trimLeft
是此方法的别名)trimEnd()
去除字符串末尾连续的空格(trimRight
是此方法的别名)
# 3. Array.prototype.flat 和 Array.prototype.flatMap
Array.prototype.flat(i)
:展平一个多维数,i 为要展开的层数,默认为 1,即展开一层。
let arr1 = [1, [2, 3], [4, 5]]
console.log(arr1.flat(1))
// [1,2,3,4,5]
let arr2 = [1, [2, 3, [4, 5]]]
console.log(arr2.flat(2))
// [1,2,3,4,5]
使用 Infinity
作为深度,展开任意深度的嵌套数组
[1, [2, 3, [4, 5]]].flat(Infinity)
// [1, 2, 3, 4, 5, 6]
也可以使用 flat
来去除数组空项
let arr = [1,2,3,,4]
arr.flat() // [1,2,3,4]
Array.prototype.flatMap
:相当于map
和flat
的结合,方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
let arr = [1,2,3,4]
let res1 = arr.map(x => [x ** 2])
console.log(res1)
// [[1],[4],[9],[16]]
let res2 = arr.flatMap(x => [x ** 2])
console.log(res2)
// [1,4,9,16]
# 4. Symbol.prototype.description
使用 Symbol()
创建的 Symbol
字面量,可以直接使用 description
获取该字面量的描述。
let sym = Symbol('hello')
console.log(sym.description)
// hello
# ECMAScript 11 新特性
# 1. 类的私有属性
ES11 提供了类的私有属性,在类的外部无法访问该属性。只有再类的内部能访问。
class Person{
//公有属性
name;
//私有属性
#age;
#weight;
//构造方法
constructor(name, age, weight){
this.name = name;
this.#age = age;
this.#weight = weight;
}
intro(){
console.log(this.name);
console.log(this.#age);
console.log(this.#weight);
}
}
//实例化
const girl = new Person('晓红', 18, '45kg');
// 外部无法直接访问
// console.log(girl.name);
// console.log(girl.#age);
// console.log(girl.#weight);
girl.intro();
# 2. allSettled
# Promise.allSettled()
该 Promise.allSettled()
方法返回一个在所有给定的 promise
都已经 fulfilled
或 rejected
后的 promise
,并带有一个对象数组,每个对象表示对应的 promise
结果。 allSettled
方法返回的 Promise
对象始终是成功( fulfilled
)的。
使用场景:
- 有多个彼此不依赖的异步任务成功完成时使用。
- 想得到每个
promise
的结果时使用。
也就是传进去一个 Promise 数组,然后返回的是个 Promise 对象,这个对象的状态属性肯定为 fulfilled,value 属性为一个对象数组,数组里面每个对象对应了每个传进去的 Promise 对象,有着同样的属性和值 (状态 and value)
# Promise.all()
_对比于 Promise.all()
, all()
也接受一个 Promise
对象数组_参数,只要有一个失败( rejected
),那么返回的 Promise
对象就是失败( rejected
)的。
使用场景:
- 传__进去的
Promise
对象彼此依赖,且需要在其中任何一个失败的时候停止。__
也就是传进去一个 Promise 数组,然后返回的是个 Promise 对象,如果传进去的 Promise 数组里面有任何一个 Promise 最后状态是 rejected 那么这个 Promise.all () 方法返回的 Promise 对象的状态属性肯定为 rejected (只有全是 resolve 这个才是 resolve, 有点事务的意思),value 属性为一个数组,数组里面每个元素对应了每个传进去的 Promise 对象的 value 属性
两个 Promise
都是成功的情况:
let p1 = new Promise((resolve, reject) => {
resolve('用户数据-1')
})
let p2 = new Promise((resolve, reject) => {
resolve('订单数据-2')
})
let res1 = Promise.allSettled([p1, p2])
let res2 = Promise.all([p1, p2])
console.log(res1)
console.log(res2)
输出结果:
一个成功,一个失败:
let p1 = new Promise((resolve, reject) => {
resolve('用户数据-1')
})
let p2 = new Promise((resolve, reject) => {
reject('失败了')
})
let res1 = Promise.allSettled([p1, p2])
let res2 = Promise.all([p1, p2])
console.log(res1)
console.log(res2)
打印结果:
# 3. matchAll
matchAll()
方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。
const regexp = /t(e)(st(\d?))/g;
const str = 'test1test2';
const array = [...str.matchAll(regexp)];
console.log(array[0]);
// expected output: Array ["test1", "e", "st1", "1"]
console.log(array[1]);
// expected output: Array ["test2", "e", "st2", "2"]
# 4. 可选链
# 4.1 定义
可选链 ?.
是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
原则:如果可选链 ?.
前面的部分是 undefined
或者 null
,它会停止运算并返回该部分。
let user = {
address: {
}
}
console.log( user?.address?.street ); // undefined(不报错)
# 4.2 短路效应
短路效应:正如前面所说的,如果 ?.
左边部分不存在,就会立即停止运算(“短路效应”)。所以,如果后面有任何函数调用或者副作用,它们均不会执行。
这有和 &&
的作用类似,但上述改用 &&
会显得代码冗余度高:
console.log(user && user.address && user.address.street)
# [4.3 其它变体:?.(),?.]
可选链 ?.
不是一个运算符,而是一个特殊的语法结构。它还 可以与函数和方括号一起使用。
例如,将 ?.()
用于调用一个可能不存在的函数(即使不存在也不报错)。
function foo() {
console.log('hello')
}
foo?.()
// hello
?.[]
允许从一个可能不存在的对象上安全地读取属性。(即使不存在也不报错)。
let obj = {
key: 123
}
console.log(obj?.['key'])
// 123
# 5. 动态 import 导入
const btn = document.getElementById('btn');
btn.onclick = function(){
import('./hello.js').then(module => {
module.hello();
}
# 6. BigInt
BigInt
是一种特殊的数字类型,它提供了对任意长度整数的支持。
创建 bigint
的方式有两种:在一个整数字面量后面加 n
或者调用 BigInt
函数,该函数从字符串、数字等中生成 bigint
。
let n1 = 123n
let n2 = 456n
let n3 = BigInt(789)
console.log(typeof n1) // bigint
console.log(n1+n2) // 579n
console.log(n2+n3) // 1245n
比较运算符:
- 例如
<
和>
,使用它们来对bigint
和number
类型的数字进行比较没有问题:
alert( 2n > 1n ); // true
alert( 2n > 1 ); // true
- 但是请注意,由于
number
和bigint
属于不同类型,它们可能在进行==
比较时相等,但在进行===
(严格相等)比较时不相等:
alert( 1 == 1n ); // true
alert( 1 === 1n ); // false
# 7. globalThis
全局对象提供可在任何地方使用的变量和函数。默认情况下,这些全局变量内置于语言或环境中。
在浏览器中,它的名字是 window
,对 Node.js 而言,它的名字是 global
,其它环境可能用的是别的名字。
ES11 中 globalThis
被作为全局对象的标准名称加入到了 JavaScript 中,所有环境都应该支持该名称。所有主流浏览器都支持它。
使用场景: 假设我们的环境是浏览器,我们将使用 window
。如果你的脚本可能会用来在其他环境中运行,则最好使用 globalThis
。
# 说下我的理解
# 事件循环,Promise,async-await, 异步 (微任务宏任务) 同步执行顺序
我的理解:要是代码遇到可能会耗时比较久的任务,比如说计时器 (就算是 0 秒也会在看到是个定时器时会认为是耗时的), 还有 ajax 请求,反正就是看起来会耗时的任务,这些一般都会是被异步操作.
- js 引擎 (执行栈) 一遇到一个看起来会耗时的任务就会把他放进宿主环境里面去执行,比如说是个定时器,在宿主环境中会先等我们执行栈的代码执行完之后再去执行,不过一般放在宿主环境 (web api (浏览器或者 node)) 是宏任务,而像 Promise.then,Promise.catch 这种微任务是在 js 引擎中的一块地方
- 现在我们同步任务都完成了 (记住包括 new Promise 对象里面那个函数的代码), 就回先去把微任务都挨个执行,这里的执行只是单纯将运行一下,但是不会运行里面的回调函数!!!里面的回调函数 (callback) 一般异步任务都会有的,这个回调函数 (不管宏任务还是微任务) 其实才是我们真正想执行的,这个函数就是我们真正想要执行的,只不过要么需要先等一个事件触发,要么需要计时到了,要么需要 ajax 请求返回了,要么需要等 Promise 对象返回了确定了状态属性,等等等
- 这样微任务里面的回调函数会给放入任务队列 (queue) 中排队等着执行
- 然后接着是宏任务,只是执行一下比如说计时器会开始倒计时,倒计时结束会把回调函数放到任务队列 (queue) 中排队等着执行,如果是事件触发,那么就会把那个事件触发留在那个宿主环境中 (web api), 这样之后一当我们触发了就会把那里面的函数放入任务队列 (queue) 中排队等着执行
- 当然每一次执行完每一个宏任务会先看看有没有微任务,一旦有就执行所有微任务把他们的回调函数放入任务队列排队
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
要是没有,就回去执行剩下的宏任务 (要是有的话), 再把下个宏任务的回调函数放入任务队列排队
注意在这一阶段 (确认了没有微任务), 在宏任务中,哪一个执行先满足了条件触发了回调函数,就把他的哪个回调函数放入任务队列排队 -> 比如说一个 ajax 请求 (ajax 请求本身如果设置为异步, 他请求本身就是异步的,会在同步代码执行完毕后请求 (一般都是通过我们某个事件触发而发送的请求 (请求通过事件触发 -> 服务器处理(这时浏览器仍然可以作其他事情)-> 处理完毕同步需要等待返回结果才能继续,异步不必等待,一般需要监听异步的结果)))(请求中,需要 3ms), 还有个计时器 (2ms 触发), 会首先按照顺序先进来的执行,但这个执行只是相当于启动了一下,非常的快,这个时候因为是计时器先到了触发了回调函数,所以是计时器的回调函数先放到了任务队列排队 (所以这里并不是按照前后顺序什么的)
这个回调函数执行完之后当然就会看有没有微任务什么的
# 非常重要!创建 (new) Promise 实例化对象时里面那个函数参数的里面的代码是同步操作的,Promise 只有 then,catch, 等等 methods 是异步操作 (微任务)
# 同样,async 函数里面在到达 await 之前的代码都是同步执行的
await 做了什么
从字面意思上看 await 就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个 promise 对象也可以是其他值。
很多人以为 await 会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上 await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面 (下面) 的代码 (尽管这些代码都不是异步操作,都会被) 加入到 microtask (微任务! 不是宏任务!) 中,_然后就会跳出整个 async 函数_来执行 async 函数后面 (下面) 的代码。
注意!
-
await 对于__后面跟着的是 promise 对象__, 则会堵塞后面的,必须要等获得到了这个 promise 对象,所以要是一个函数的调用,await 必须等到那个函数执行完毕然后把那个 Promise 对象结果返回过来然后那个 Promise 对象有 resolve 的值了 (rejected 或者 throw… 会被 catch 接住) 才会把下面的代码放到微服务之中
-
await 对于__后面跟着的是非 promise 对象__,比如箭头函数,同步表达式等等,await 等待函数或者直接量的返回,而不是等待其执行结果
比如说:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
上面的代码其实可以写为:
async function async1() {
console.log('async1 start')
Promise.resolve(async2()).then(() => {
console.log('async1 end')
})
}
所以 async2 函数因为用了小括号所以会被调用,这里还是属于同步操作 (因为看上方第二种写法,这个函数调用还是在 new Promise 阶段相当于 (我们上面讲的)), 调用 async2 函数里面的代码也是像我们上面说的在到达 await 之前的代码都是同步的除非是异步的操作 (任务) 比如说计时器等
执行完就会回来,这个 async1 函数里面的在 await 下面的代码就相当于是放进了 promise 对象.then 方法里面,也就是属于微任务了,所以会在微任务里面等我们同步任务都完成之后先执行微任务,比如说这里的 then 方法 (虽然第一个写法中看不到), 然后会把 then 方法里面的回调函数放到任务队列排队,这个回调函数里面包括的就是 async1 函数里面 await 下面的那些代码,要是这里面有其他微任务或者宏任务就是会正常操作 (该放微任务的放微任务,该放宏任务的放宏任务)
再比如说:
async function async1() {
let one = await f1();
let two = await f2();
let three = await f3();
}
上面的代码其实可以写为 (我的理解):
async function async1() {
Promise.resolve(await f1()).then((data) => {
let one = data;
Promise.resolve(await f2()).then((data) => {
let two = data;
Promise.resolve(await f3()).then((data) => {
let three = data;
})
})
})
}
以下都是我个人理解,可能是对可能是错
这里主要就是相当于之前我们看到的多个 await 一个接一个其实是会堵塞的
这是因为 await 需要后面的值 (一般都是函数调用,所以需要函数调用完了) 然后再把 await 语句返回值给那个变量,其实在第二种写法中更直观 (按照我的理解写的第二种写法, 希望是对的).
- 也就是在第一个 await 语句其实是在那个 async 函数里面是同步执行的 (因为属于 new Promise 的代码 (上面讲过))
- 然后结果赋值给 one 接着第二个 await, 也两个都是同步的,只不过是属于第一个 new Promise 出的 Promise 对象的 then 方法里面,这个 then 方法是微任务,会放到微任务那一块执行 (也或许是等待执行), 只有在那个 Promise 对象的状态和值确认了才会真正把 then 方法 (执行) 然后里面的回调函数放到任务队列里面排队,这个回调函数里面的代码就是我们给 one 赋值然后第二个 await
- 注意这里因为是 then 里面,所以是只有第一个 await 后面的函数 (fn1) 执行完了,然后 await 获取到了那个函数的返回值,第一个 Promise 对象才会被返回才可以调用 then
- 所以这就形成了继发关系
总结: 当 async 函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。那其实就是说在 async 函数内,每当遇到 await 关键字的时候,函数就是阻塞住,必须等到异步操作有结果时才会往下继续执行
说明之前说到的继发关系解决方法:
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 这个方法直接用就行不说明
// 写法二
let value1 = fn1();
let value2 = fn2();
let value3 = fn3();
let one = await value1; //这里等待value1的返回 后面可以do something
let two = await value2; //这里等待value2的返回 后面可以do something
let three = await value3; //这里等待value3的返回 后面可以do something
第二种解决方式是
我们可以异步函数执行的结果储存起来,在需要的时候再去 await
这里其实就是上面那些函数 (里面包含着返回的 Promise 对象这个 Promise 对象里面包含了异步操作) 先调用他们 (然后看到耗时的就放入微任务 / 宏任务那一块) 然后返回值放到一个变量中,所以上面的调用以及赋值其实相当于是异步操作,所以互相之间不会有影响,不像之前前面有了 await 就必须要执行完毕然后有了 Promise 对象那个 resolve 的值才可以继续 (将之后的放入微任务)
之后再 await, 这里等的就是变量,因为这些变量可能还没接收到那个返回的 Promise 对象,所以 await 会认为是非 Promise 就直接该怎么样就怎么样的返回给我们 one,two,three 变量.
上面解释的很勉强,其实我还是没怎么搞懂这个到底为什么解决了继发关系,多做研究
# 参考题
# 1. 事件循环,同步异步,微任务宏任务
JavaScript 主线程从 “任务队列” 中读取异步任务的回调函数,放到执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop (事件循环)
宏任务与微任务
宏任务:异步 Ajax 请求,setTimeout,setInterval,文件操作,new Promise 等
微任务:Promise.then、.catch、.finally,process.nextTick 等
宏任务与微任务是交替执行的,每次执行完宏任务都会检查是否有微任务
console.log(1)
setTimeout(function(){
console.log(2)
}, 0)
new Promise(function(resolve){
console.log(3)
resolve()
}).then(function(){
console.log(4)
})
console.log(5)
答案:1、3、5、4、2
解析:
主线程判断是同步代码还是异步代码
console.log(1) // 同步任务
setTimeout(function(){
console.log(2) // 异步任务:宏任务
}, 0)
new Promise(function(resolve){
console.log(3) // 同步任务
resolve()
}).then(function(){
console.log(4) // 异步任务:微任务
})
console.log(5) // 同步任务
注意构造 Promise 实例化对象里面的那个参数函数里面的代码是同步的!就跟正常的 new 一个实例化对象一样,只不过传的是一个函数作为参数
真正是异步任务 (微任务) 的是对于这个 Promise 对象调用 then 方法,这个是回调函数,只有在这个 Promise 对象执行完 resolve 或 reject 之后,这个 then 方法也就是说需要等,js 引擎一看这个要等就把他交给宿主环境来执行 then 方法,然后 then 方法里面的回调函数会在接收到 Promise 对象的 resolve 或 reject 之后才会执行
执行同步任务
console.log(1) // 同步任务
console.log(3) // 同步任务
console.log(5) // 同步任务
执行异步任务:微任务
console.log(4) // 异步任务:微任务
执行异步任务:宏任务
console.log(2) // 异步任务:宏任务
# 2. 事件循环,同步异步,微任务宏任务
console.log('1')
setTimeout(function () {
console.log('2')
process.nextTick(function () {
console.log('3')
})
new Promise(function (resolve) {
console.log('4')
resolve()
}).then(function () {
console.log('5')
})
})
Promise.resolve().then(function () {
console.log('6')
})
new Promise(function (resolve) {
console.log('7')
resolve()
}).then(function () {
console.log('8')
})
setTimeout(function () {
console.log('9')
process.nextTick(function () {
console.log('10')
})
new Promise(function (resolve) {
console.log('11')
resolve()
}).then(function () {
console.log('12')
})
})
分析:
第一遍:
- 首先执行第一行的同步任务,打印 1
- 第三行的 setTimeout 是异步任务中宏任务,加入宏任务记为 setTimeout1
- 下面第 15 行 Promise.then 是异步任务中的微任务,加入微任务记为 then
- 第 18 行 new Promise 是同步任务,执行第一个 log 直接打印 7,后面的 .then 是微任务,存入微任务中记为 then1
- 第 25 行 setTimeout 是异步任务中宏任务,加入宏任务记为 setTimeout2
此时整个程序状态如下:
第二遍:
- 首先执行微任务区中的任务,then 打印 6,then1 打印 8
- 微任务区任务执行完成,再执行宏任务区 setTimeout1 ,打印 2,将第 5 行 process 微任务放入微任务区记作 process2
- 第 8 行 new Promise 为同步任务,立即执行打印 4,将后续 .then 微任务 放入微任务区记作 then2
此时整个程序状态如下:
第三遍:
- 首先执行微任务区,process2,打印 3,再执行 then2,打印 5
- 微任务区执行完成,再去执行宏任务区中的 setTimeout2,首先是第 26 行 log 直接打印 9
- 将第 27 行 process 微任务放入微任务区记作 process3
- 第 30 行 new Promise 为同步任务,立即执行打印 11,将后续 .then 微任务 放入微任务区记作 then3
此时整个程序状态如下:
最后一遍:
- 执行微任务区 process3,打印 10
- 执行微任务区 then3,打印 12
最终打印结果:1,7,6,8,2,4,3,5,9,11,10,12
# 3. async,await, 异步等执行顺序
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0);
async1()
new Promise(resolve => {
console.log('promise1')
resolve()
})
.then(() => {
console.log('promise2')
})
console.log('script end')
- 首先,事件循环从宏任务 (macrotask) 队列开始,这个时候,宏任务队列中,只有一个 script (整体代码) 任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:
- 然后我们看到首先定义了两个 async 函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中:
- script 任务继续往下执行,执行了 async1 () 函数,前面讲过 async 函数中在 await 之前的代码是立即执行的,所以会立即输出 async1 start。 遇到了 await 时,会将 await 后面的表达式执行一遍,所以就紧接着输出 async2,然后将 await 后面的代码也就是 console.log (‘async1 end’) 加入到 microtask 中的 Promise 队列中,接着跳出 async1 函数来执行后面的代码。
- script 任务继续往下执行,遇到 Promise 实例。由于 Promise 中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。
-
script 任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。 根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。 因而在 script 任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务 async1 end 和 promise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
-
第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。
变式一
在第一个变式中我将 async2 中的函数也变成了 Promise 函数,代码如下:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
new Promise(resolve => {
console.log('promise1')
resolve()
})
.then(() => {
console.log('promise2')
})
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0);
async1()
new Promise(resolve => {
console.log('promise3')
resolve()
})
.then(() => {
console.log('promise4')
})
console.log('script end')
运行结果入下:
在第一次 macrotask 执行完之后,也就是输出 script end 之后,会去清理所有 microtask。所以会相继输出 promise2, async1 end ,promise4,其余不再多说。
变式二
在第二个变式中,我将 async1 中 await 后面的代码和 async2 的代码都改为异步的,代码如下:
async function async1() {
console.log('async1 start')
await async2()
setTimeout(() => {
console.log('setTimeout1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('setTimeout2')
}, 0)
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout3')
}, 0);
async1()
new Promise(resolve => {
console.log('promise1')
resolve()
})
.then(() => {
console.log('promise2')
})
console.log('script end')
执行结果如下:
在输出为 promise2 之后,接下来会按照加入 setTimeout 队列的顺序来依次输出,通过代码我们可以看到加入顺序为 3 2 1,所以会按 3,2,1 的顺序来输出。
只要前面的原理看懂了,任何的变式题都不会有问题。
# 4. try catch 能不能 catch 到 reject 状态的 promise 以及其他重要的
之前有这样的疑问,不是说 try catch 能捕获错误吗?对于 promise.reject () 的怎么不行?
既然不行,为啥 await 又与 try catch 一起用?
try catch 在什么时候才会生效
啥时候用 try catch 捕获错误
结论:
- try catch 不能捕获异步代码,所以不能捕获 promise.reject () 的错误,并且 promise 期约故意将异步行为封装起来,从而隔离外部的同步代码
- try catch 能对 promise 的 reject () 落定状态的结果进行捕获
- try catch 能捕捉到的异常,必须是
主线程执行
已经进入 try catch, 但 try catch 尚未执行完的时候抛出来的,意思是如果将执行 try catch 分为前中后。只有中才能捕获到异常
- 应该只在确切知道接下来该做什么的时候捕获错误 (这里我单指 try catch)
# 总结这篇,真的非常细,一步一步带着走,大概花了一周时间吧,所以真的值得一看,喜欢的话就收藏吧,我也会继续努力的!!!冲呀
# 一 为什么要捕获错误,处理错误
默认情况下,所有浏览器都会隐藏错误信息。一个原因是除了开发者之外这些信息对别人没什么用,另一个原因是网页在正常操作中报错的固有特性。
当网页中的 JavaScript 脚本发生错误时,不同浏览器的处理方式不同,而且也只会在浏览器输出,有一个良好的错误处理策略可以让用户知道到底发生了什么,防止用户流失。为此,必须理解各种捕获和处理 JavaScript 错误的方式
# 二 错误一般出现在哪里
pc 端:所有现代桌面浏览器都会通过控制台暴露错,也就是 pc 端能够在控制台去捕获错误
移动端:移动浏览器不会直接在设备上提供控制台界面。不过,还是有一些途径可以在移动设备中检查错 — 这里我推荐 vconsole 包
具体怎么用?看我这篇文章: juejin.cn/post/692241… 为了防止你们这些臭屁的流失,我先上个图
# 三 错误捕获处理的方式 try catch 基本介绍
# 3.1.try catch 的出现,处理异常的方法之一
任何可能出错的代码都应该放到 try 块中,而处理错误的代码则放在 catch 块中
try {
// 可能出错的代码
} catch (e) {
// 出错时要做什么
}
# 3.2 try 或 catch 块无法阻止 finally 块执行
3.2.1 调用了 window 没有的方法,必然报错,走 catch, 这时候 finanlly 有返回
try {
window.someNonexistentFunction();
console.log('try')
} catch(e) {
console.log(e, 'catch')
} finally {
console.log('finally')
}
3.2.2 没有报错,这时候 finanlly 有返回
try {
console.log('try')
} catch(e) {
console.log(e, 'catch')
} finally {
console.log('finally')
}
3.2.3 那么对于 finally 我们能用他做啥呢?
1. 例如 loading 的取消,不管我报错没,最后这个 loading 必须是要取消的
2. 比如做一些清除,销毁的操作
# 3.3 return 语句无法阻止 finally 的执行
遇到 return 一般是出栈,出去这个函数,但是这里的 finally 仍然会执行
function testFinally(){
try {
return 2;
} catch (error){
return 1;
} finally {
return 0;
}
}
console.log(testFinally());
# 四 try catch 是如何捕获错误的?什么时候才能捕获到错误 ( 非常重要
)
try catch 能捕捉到的异常,必须是 主线程执行
已经进入 try catch, 但 try catch 尚未执行完的时候抛出来的,意思是如果将执行 try catch 分为前中后。只有中才能捕获到异常
# 4.1 try catch 之前 ( 捕获不到的
)
发现了没?连 6666 都没有打印,比如语法异常(syntaxError),因为语法异常是在语法检查阶段就报错了,线程执行尚未进入 try catch 代码块,自然就无法捕获到异常
# 4.2 try catch 进行之中 ( 能捕获到
)
try {
window.someNonexistentFunction();
} catch(e){
console.log("error",e);
}
console.log('我要执行')
这时候就能捕获到了
# 4.3 try catch 进行之中 ( 重点:不能捕获到
)
try {
console.log('try里面')
setTimeout(() => {
console.log('try里面的setTimeOut')
window.someNonexistentFunction(); // 变成了未捕获的错误
}, 0)
} catch (e) {
console.log('error', e)
}
setTimeout(() => {
console.log('我要执行')
}, 100)
这里没有catch到错误,是因为try catch是同步代码
(一般我们用 setTimeOut 来模拟异步,也是因为他有延迟的特性,所以我们这里视为有异步操作)
当 js 运行到 try 里面 setTimeOut 的时候,setTimeOut 先是去了 setTimeOut 线程,0 秒后去 task queue (任务队列) 进行排队,运行到 try catch 外面的 setTimeOut, 第二个 setTimeOut 也是先去 setTimeOut 线程,100/1000 秒然后去 task queue 排队,排在第一个后面
那么执行的顺序就是 try catch 同步代码在主执行栈执行完了之后,去 task queue 看看有啥要执行的没,task queue 里面是遵循先进先出的原则,自然按照图片上的顺序进行
这个例子也是说明了为啥 try catch 不能捕获到 promise.resolve 报错的一个原因
这里涉及到 eventloop, 下次补充
# 4.4 Promise 的异常捕获
如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
捕获 Promise 内部错误的两种方式以及 reject 回调后的错误捕获的一种方式
- Promise.prototype.then () reject 回调函数
- Promise.prototype.catch()
- 结合 async/await 和 try…catch
4.4.1 Promise.prototype.then () 捕获 -- reject 回调函数
特点:
1.无法捕获resolve()回调中出现的异常,需要无限链式调用then回调去捕获异常
2.无法中断后续的then操作
案例1
能够在第二个 reject 回调中捕获到,如果又有报错,能在下一个 then 里面的 reject 回调捕获到
const createPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject('promise')
}, 1000)
})
createPromise.then(res => {
console.log(res, 'resolved');
}, res => {
console.log(res, 'reject');
throw new Error('reject1')
}).then(null, res=> {
console.log(res, 'reject2');
})
案例2
如果已经是 resove 的回调了,中间有报错,也能够在下一个 then 里面的 reject 回调能捕获到
const createPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise')
}, 1000)
})
createPromise.then(res => {
// 结果1
console.log(res, 'resolved');
window.someNonexistentFunction()
}, res => {
console.log(res, 'reject');
}).then(null, res=> {
console.log(res, 'reject');
})
案例3
能够在 reject 的回调函数里面去使用 try catch, 因为这里面就是同步代码,但是不用直接去捕获 Promise.reject ()
const createPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject('promise')
}, 1000)
})
createPromise.then(res => {
console.log(res, 'resolved');
}, res => {
console.log(res, 'reject');
try {
window.test()
} catch(e) {
console.log(e, 'e');
}
}).then(null, res=> {
console.log(res, 'reject2');
})
4.4.2 Promise.prototype.catch () 捕获异常
特点:
因为 Promise 对象的错误具有 “冒泡” 性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获。
catch () 方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then () 方法。
记住一点: Promise.prototype.catch()
方法用于给期约添加拒绝处理程序,这个方法只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)
下面这两个是相等的:
let p = Promise.reject();
let onRejected = function(e) {
setTimeout(console.log, 0, 'rejected');
};
// 这两种添加拒绝处理程序的方式是一样的:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected
案例1
冒泡性质的抛错
const createPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject('promise')
}, 1000)
})
createPromise.then((res) => {
console.log(res, 'resolved')
}).then(() => {
console.log(res, 'resolved')
}).then(() => {
console.log(res, 'resolved')
}).then(() => {
console.log(res, 'resolved')
}, res => {
console.log(res, 'reject')
})
案例2
catch 也同 reject 回调一致,具有捕获错误的冒泡性质,只要有报错,不管经过创建多少个新的 promise, 后面仍然能捕获到
const createPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject('promise')
}, 1000)
})
createPromise.then((res) => {
console.log(res, 'resolved')
}).then(() => {
console.log(res, 'resolved')
}).then(() => {
console.log(res, 'resolved')
}).catch((res) => {
console.log(res, 'catch reject');
})
案例3
catch 到了错误,又是一个新的 promise,cath 回调里面没有报错,下一个 then 还是走 resolved 的回调
const createPromise = new Promise((resolve, reject) => {
reject('promise');
})
createPromise.then(res => {
console.log('resolved1', res)
})
.then(res => {
console.log('resolved2', res)
})
.catch(res=> {
console.log('catche reject', res)
})
.then(res => {
console.log('resolved3', res)
})
4.4.3 async/await 结合 try…catch 使用
什么是 async 函数?
- async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。
- async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,
等到异步操作完成,再接着执行函数体内后面的语句。
什么是 await 命令?有那些特点
- await 只能在异步函数 async function 中使用。
- await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理 (fulfilled),
其回调的 resolve 函数参数作为 await 表达式的值,继续执行 async function。- 若 Promise 处理异常 (rejected),await 表达式会把 Promise 的异常原因抛出。
- await 如果返回的是 reject 状态的 promise,如果不被捕获抛出,就会中断 async 函数的执行。
- 另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。
- async await 函数中,通常使用 try/catch 块来捕获错误。
用大白话来说 await 之后的返回值就是 resolve 和 reject 回调的结果
案例1
await 已经返回了 reject () 之后 reject 回调的结果,但是拿到结果并没有进行任何的操作 这时候你发现了没,下面吗的console.log(res, 'await result');已经不执行了,
对于调用一个请求,失败了,就要中断我们所有的代码执行,显然不是我们想要的结果,那么如何让调用接口失败,我们仍然可以继续操作呢?
const createPromise = new Promise((resove, reject) => {
setTimeout(() => {
console.log('我反正进来了')
reject('promise')
}, 1000)
})
async function awaitTest() {
const res = await createPromise
console.log(res, 'await result');
}
awaitTest()
案例2
对结果进行 try catch
const createPromise = new Promise((resove, reject) => {
setTimeout(() => {
console.log('我反正进来了')
reject('promise')
}, 1000)
})
async function awaitTest() {
try {
const res = await createPromise
} catch(e) {
console.log(e, 'await调用报错咯!');
}
console.log('可以让我执行一下吗?');
}
awaitTest()
有没有想过一个问题? 虽然说await会返回reject回调里面的结果,或者是resolve回调里面的结果,那我try catch怎么就知道返回的东西是要报错的呢? 为啥reject返回的我就要报错呢?
这里我又发现了一个好东西:
当 promise 函数里面调用 reject () 来落定拒绝契约的时候,实际上以下三个等价
throw new Error('test');
try {
throw new Error('test');
} catch(e) {
reject(e);
}
等价于
reject(new Error('test'));
那么 try catch 肯定能捕获到 throw new Error 的错误拉~请看来自 es6 的解释
案例3
reject (new Error ()) 与 throw new Error (‘我是个错误,没有 reject ()’) 在什么情况下等价
const createPromise = new Promise((resove, reject) => { reject(new Error()) })
createPromise.then((res) => {
console.log(res, 'resolved'); }
).catch(res => { console.log(res, 'catch'); })
const createPromise = new Promise((resove, reject) => { reject(new Error()) })
createPromise.then((res) => {
throw new Error('我是个错误,没有reject()')}
).catch(res => { console.log(res, 'catch'); })
以上两种情况都是能够通过 Promise.catch () 捕获到错误的,也就是对应 es6 上面说的相等的情况,但是这里有个前提,是在 new Promise 里面没有异步的情况下.
那么为什么 throw new Error (‘我是个错误,没有 reject ()’) 与 reject (new Error ()) 相等?
源码解析:segmentfault.com/a/119000001…
源码中,会对传入的函数进行 try catch. 如果是 throw new Error (‘我是个错误,没有 reject ()’) 必定会被 catch 到错误,这里时候 catch 里面自动调用了 reject (). 也就是直接调用了拒绝的落定,而上面说过了 try,catch 里面的 catch 是不能 catch 到 setTimeOut 里面的错误的
const createPromise = new Promise((resove, reject) =>
{ setTimeout(()=> { reject() }, 1000) })
createPromise.then((res) => { console.log(res, 'resolved'); })
.catch(res => { console.log(res, 'catch'); })
const createPromise = new Promise((resove, reject) => { setTimeout(()=>
{ throw new Error('我是个错误,没有reject()')
}, 1000) })
createPromise.then((res) => { console.log(res, 'resolved'); })
.catch(res => { console.log(res, 'catch'); })
为什么上面的有异步的 reject () 能捕获到,throw new Error (‘我是个错误,没有 reject ()’) 却不能 promise.catch () 到呢
上面提到了,源码中的第一个 try catch 是捕获不到 setTimeOut 的,里面有_state 四个状态
_state === 0 // pending
state === 1 // fulfilled,执行了resolve函数,并且_value instanceof Promise === true
_state === 2 // rejected,执行了reject函数
_state === 3 // fulfilled,执行了resolve函数,并且_value instanceof Promise === false
如果没有调用 resolve 函数或者 reject 函数。始终是_state === 0 的 penging 状态。没有 setTimeOut 的时候是 try catch 到了 throw new Error () 做了自动的 reject (), 而如果有 setTimeOut 第一个 try catch 已经对里面的 setTimeOut 起不了作用了,所以_state 一直是等于 0, 而源码里面针对 new Promise 做了处理:
也就说是 存在 setTimeOut, 会出现一个等待 reject () 或者 resolve () 调用的 pending 状态,如果里面没有出现一直处于 pending 状态,也就不能被 promise.catch () 到
这里最重要的要理解,try catch 是不可能 catch 到异步的,try catch 与 promise.catch () 是不一样的,如果 promise.catch () 不到错误,应该去看看 promise 源码对 promise.catch ()(即 Promise.prototype.then (null, onRejected)) 内部是如何实现的
源码必定都是遵循 js 事件轮询机制的!
案例4
同上,await 都拿不到结果,那就更加别提能用 try catch 能捕获了,await 表示的是落定状态 (fulfilled) 返回的结果
const createPromise = new Promise((resove, reject) => {
setTimeout(() => {
throw new Error('我是个错误,但是我并不想落定状态,没有reject()')
}, 1000)
})
async function awaitTest() {
try {
console.log('测试1')
const res = await createPromise // 调用的时候内部已经中止了,也不是settled的状态,所以没法去返回结果,那就更不可能进行到赋值给res的操作了
console.log(res, '测试2')
} catch(e) {
console.log(e, 'await调用报错咯!');
}
console.log('可以让我执行一下吗?');
}
awaitTest()
案例5
try catch 面对多个 await 捕获的问题
const createPromise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败1')
}, 1000)
})
const createPromise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败2')
}, 1000)
})
const createPromise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功3')
}, 1000)
})
async function awaitTest() {
try {
await createPromise1
console.log('不执行');
await createPromise2
await createPromise3
} catch(e) {
console.log(e, 'await调用报错咯!');
}
console.log('1可以让我执行一下吗?');
}
awaitTest()
console.log('2可以让我执行一下吗?');
结果就是多个 await 有几个报错,她只会 catch 到第一个,走了 catch, 但是仍然可以继续运行
但是 我们依旧推荐多个awiat的东西如果没有关联性,考虑到并发性,可以直接用promise.all
const createPromise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败1')
}, 1000)
})
const createPromise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败2')
}, 1000)
})
const createPromise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功3')
}, 1000)
})
async function awaitTest() {
const list = await Promise.all([createPromise1.catch((e) => e),
createPromise2.catch((e) => e), createPromise3.catch((e) => e)])
console.log(list, 'list');
console.log('1可以让我执行一下吗?');
}
awaitTest()
console.log('2可以让我执行一下吗?');
# 4.5 try catch 不能捕获到 promise.resolve () 的报错
上面有提到,try catch 如果分为在主线程的执行的前中后,那么只有中才能捕获到错误,其实 4.3 的例子已经很明显了 (4.4 主要还是想大家普及一下 promise 捕获错误的知识) 但是这边还是再讲解一下
我们先看看 js 高级程序设计第 11 章
4.5.1 try catch 捕获 throw new Error 这个没有问题
try {
throw new Error('foo');
} catch(e) {
console.log(e, 'catch');
}
4.5.2 try catch 捕获不到 Promise.reject ()
try {
Promise.reject()
} catch(e) {
console.log(e, 'catch');
}
首先你要知道,下面这两个契约实例其实是一样的
let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();
因为 p1,p2 是异步执行模式,而 try catch 是同步代码,同步代码先执行,异步去 task queue 排队,等同步做完再执行,而你能够在 then 的回调里面进行 try catch 或者 await 的返回数,只是对返回结果进行捕获而已,并捕获不了 promise 的异步代码
你可以将 p1,p2 看成在请求,请求成功我就写告诉用户已经成功了,用 resolve () 告诉大家,只有在 then 接受到,我可以对 then 里面的结果进行 try catch
# 五 try catch 啥时候用,以及如何抛错
# 5.1 throw new Error()
与 try/catch 语句对应的一个机制是 throw 操作符,用于在任何时候抛出自定义错误。throw 操作符必须有一个值,但值的类型不限。 使用 throw 操作符时,代码立即停止执行,除非 try/catch 语句捕获了抛出的值
try {
window.someNonexistentFunction()
} catch(e) {
throw new Error('报错了')
console.log('我要执行')
}
try {
window.someNonexistentFunction()
} catch(e) {
try {
throw new Error('报错了')
} catch(e) {
console.log('我要执行')
}
}
# 5.2 应该仔细评估每个函数,以及可能导致它们失败的情形。良好的错误处理协议可以保证只会发生你自己抛出的错误。
比如平时 utils 里面,如果传入的参数不是规定的,可以直接通过 if 判断类型,然后抛错
至于抛出错误与捕获错误的区别,可以这样想: 应该只在确切知道接下来该做什么的时候捕获错误
。捕获错误的目的是阻止浏览器以其默认方式响应; 抛出错误的目的是为错误提供有关其发生原因的
# 5.3 通过继承自定义错误类型,与提示
class CustomError extends Error {
constructor(message) {
super(message);
this.name = "CustomError";
this.message = message;
}
}
throw new CustomError("My message");
# 六 有哪些错误类型
1 Error
2 InternalError
3 EvalError
4 RangeError
5 ReferenceError
6 SyntaxError
7 TypeError
8 URIError
-
Error 是基类型,其他错误类型继承该类型。因此,所有错误类型都共享相同的属性,浏览器很少会抛出 Error 类型的错误,该类型主要用于开发者抛出自定义错误
-
InternalError 该错误在 JS 引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。示例场景通常为某些成分过大
该错误在JS引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。
示例场景通常为某些成分过大,例如:
“too many switch cases”(过多case子句);
“too many parentheses in regular expression”(正则表达式中括号过多);
“array initializer too large”(数组初始化器过大);
“too much recursion”(递归过深)。
- EvalError 类型的错误会在使用 eval () 函数发生异常时抛出,如果非法调用 eval (),则抛出 EvalError 异常
- RangeError 错误会在数值越界时抛出
new Array(-20) // 异常的捕获.html:52 Uncaught RangeError: Invalid array length
const num = 1
let res = num.toFixed(-1)
console.log(res); // 异常的捕获.html:53 Uncaught RangeError: toFixed() digits argument must be between 0 and 100
5. ReferenceError 会在找不到对象时发生
。(这就是著名的 “object expected” 浏览器错误的原因。)这种错误经常是由访问不存在的变量而导致的
console.log(num) //异常的捕获.html:56 Uncaught ReferenceError: num is not defined
- TypeError 在 JavaScript 中很常见,主要发生在变量不是预期类型,或者访问不存在的方法时
let o = new 10; // 抛出 TypeError
console.log("name" in true); // 抛出 TypeError
Function.prototype.toString.call("name"); // 抛出 TypeError
- URIError,只会在使用 encodeURI () 或 decodeURI () 但传入了格式错误的 URI 时发生
# 七 错误识别
错误处理非常重要的部分是首先识别错误可能会在代码中的什么地方发生
类型转换错误
数据类型错误
通信错误
- 使用 = = = ,而不使用 = =
- if 判断的条件,尽量避免类型转换
- 通信错误,主要是 url 传参 (主要错误是 URL 格式或发送数据的格式不正确,在把数据发送到服务器之前没有用,encodeURIComponent () 编码))
封装 url 传参
function addQueryStringArg(url, name, value) {
if (url.indexOf("?") == -1){
url += "?";
} else {
url += "&";
}
url += '${encodeURIComponent(name)=${encodeURIComponent(value)}';
return url;
}
const url = "http://www.somedomain.com";
const newUrl = addQueryStringArg(url, "redir","http://www.someotherdomain.com?a=b&c=d");
console.log(newUrl);
# 八 区分重大与非重大错误、
非重大错误和重大错误的区别主要体现在对用户的影响上
非重大错误
不会影响用户的主要任务;
只会影响页面中某个部分;
可以恢复;
重复操作可能成功。
重大错误
应用程序绝对无法继续运行;
错误严重影响了用户的主要目标;
会导致其他错误发生。
for (let mod of mods){
mod.init(); // 可能的重大错误
}
可以使用
for (let mod of mods){
try {
mod.init();
} catch (ex){
// 在这里处理错误
}
}
可以封装方法来区分重大错误和非重大错误
function logError(sev, msg) {
let img = new Image(),
encodedSev = encodeURIComponent(sev),
encodedMsg = encodeURIComponent(msg);
img.src = 'log.php?sev=${encodedSev}&msg=${encodedMsg}';
}
logError () 函数接收两个参数:严重程度和错误消息。严重程度可以是数值或字符串,具体取决于使用的日志系统。这里使用 Image 对象发送请求主要是从灵活性方面考虑的
# 九 调试技术
所有主流浏览器都有 JavaScript 控制台,该控制台可用于查询 JavaScript 错误。另外,这些浏览器都支持通过 console 对象直接把 JavaScript 消息写入控制台
# 9.1 debugger 关键字
推荐: www.cnblogs.com/xiaoqi2018/…
# 9.2 在页面中打印消息
平时我们看信息,有时候会在 template 里面去打印
# 9.3 根据需求封装 console.log ()
例如,平时我们要在浏览器上打印很多对象,但是每次都要去一级一级打开,可以去重写全局 console.log (这里我没有做类型判断哦)
重写前:
console.log({data: { name: 1, age: 18 }})
重写后
const consoleog = window.console.log
console.log = function() {
const args = JSON.stringify(...arguments)
consoleog(args)
}
console.log({data: { name: 1, age: 18 }})
# 十 抛出错误
抛出错误是调试代码的很好方式。如果错误消息足够具体,只要看一眼错误就可以确定原因。好的错误消息包含关于错误原因的确切信息,因此可以减少额外调试的工作量
function divide(num1, num2) {
if (typeof num1 != "number" || typeof num2 != "number"){
throw new Error("divide(): Both arguments must be numbers.");
}
return num1 / num2;
}