九、JavaScript 语言提升(ES6+)
大约 36 分钟约 10829 字
(一)零碎的改动
1.严格模式
2.let 和 const
- ES6 建议不再使用
var
定义变量 - 使用
let
定义变量、const
定义常量 - 均解决了长久以来变量定义的问题
let a = 1; // 使用 let 定义变量
a = 2; // 变量的值是可修改的
const b = 1; // 使用 const 定义常量
b = 2; // ❌ 报错,常量的值不可修改
对于开发的影响
均使用 const,实在需要修改变量,再改为 let
1)特点
- 全局定义的变量不再作为属性添加到全局对象中
- 在变量定义之前使用它会报错
- 不可重复定义同名变量
- 使用
const
定义变量时,必须初始化 - 变量具有块级作用域,在代码块之外不可使用
注意
- 在 for 循环中使用 let 定义变量,变量所在的作用域是循环体,因此在循环外不能使用
- for 循环会对该变量做特殊处理,让每次循环使用的都是一个独立的循环变量
- 可以解决 JS 由来已久的闭包问题
2)过去的问题
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 输出:3 3 3
3)过去的解决办法:IIFE
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
})(i);
}
// 输出:0 1 2
4)现在的做法
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 输出:0 1 2
3.幂运算
2 ** 3; // 8
2 ** 4; // 16
- 是一个右结合运算符
- 如果多级表达式连幂,应该先算右侧表达式
let i = 1;
let obj = {
get a() {
return ++i;
},
};
console.log(obj.a ** (obj.a ** obj.a));
// 2 ** 3 ** 4 = 2 ** 81
4.字符串新增 API
API | 含义 |
---|---|
String.prototype.includes(s) | 字符串中是否包含某个子串 |
String.prototype.trim() | 去掉字符串首尾空白字符 |
String.prototype.trimStart() | 去掉字符串开始的空白字符 |
String.prototype.trimEnd() | 去掉字符串末尾的空白字符 |
String.prototype.replaceAll(s, t) | 将字符串中 所有 的 s 替换为 t |
String.prototype.startsWith(s) | 判断字符串是否以 s 开头 |
String.prototype.endsWith(s) | 判断字符串是否以 s 结尾 |
5.模板字符串
- ES6 提供了一种新的字符串字面量的书写方式,即模板字符串
1)写法
`字符串内容`;
2)作用
- 模板字符串可以轻松的实现换行和拼接
- 在需要换行或拼接时,模板字符串远胜于普通字符串
const user = { name: "monica", age: 17 };
const s1 = `姓名:${user.name},年龄:${user.age}
my name is ${user.name}`;
// 等同于
const s2 = "姓名:" + user.name + ",年龄:" + user.age + "\nmy name is " + user.name;
/*
* s1和s2均为:
* 姓名:monica,年龄:17
* my name is monica
*/
3)实例
- 通常可以使用模板字符串拼接 html
const user = { name: "monica", age: 17 };
const html = `
<div>
<p><span class="k">姓名</span><span class="v">${user.name}</span></p>
<p><span class="k">年龄</span><span class="v">${user.age}</span></p>
</div>
`;
/*
* <div>
* <p><span class="k">姓名</span><span class="v">monica</span></p>
* <p><span class="k">年龄</span><span class="v">17</span></p>
* </div>
*/
4)冷知识
- 函数名后的模板字符串会当作实参传进函数形参
function test(a) {
console.log(a);
}
test`hello`;
// hello
function test(arr, v1, v2) {
console.log(arr);
console.log(v1);
console.log(v2);
}
let name = "jack";
let age = 19;
test`我的名字是${name},我今年${age}`;
// ['我的名字是', ',我今年', raw: Array(3)]
// jack
// 19
(二)数组
1.for...of 循环
- ES6 提供了一种爽翻天的方式遍历各种数组和伪数组
- 遍历的是数组和伪数组中的每一项
- 区分:for...in 遍历的是下标
1)示例 1
const arr = ["a", "b", "c"];
// 过去的方式 —— 垃圾
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
console.log(item);
}
// for of 的方式,结果一样
for (const item of arr) {
console.log(item);
}
2)示例 2
const elements = document.querySelectorAll(".item");
// for of 的方式
for (const elem of elements) {
// elem 为获取到的每一个元素
}
2.新增 API
API | 作用 | 图示 |
---|---|---|
Array.isArray(target) | 判断 target 是否为一个数组 | |
Array.from(source) | 将某个伪数组 source 转换为一个真数组返回 | |
Array.prototype.fill(n) | 将数组的某些项设置为 n | ![]() |
Array.prototype.forEach(fn) | 遍历数组,传入一个函数,每次遍历会运行该函数 | ![]() |
Array.prototype.map(fn) | 数组映射,传入一个函数,映射数组中的每一项 | ![]() |
Array.prototype.filter(fn) | 数组筛选,传入一个函数,仅保留满足条件的项 | ![]() |
Array.prototype.reduce(fn) | 数组聚合,传入一个函数,对数组每一项按照该函数的返回聚合 | ![]() |
Array.prototype.some(fn) | 传入一个函数,判断数组中是否有至少一个通过该函数测试的项 | ![]() |
Array.prototype.every(fn) | 传入一个函数,判断数组中是否所有项都能通过该函数的测试 | ![]() |
Array.prototype.find(fn) | 传入一个函数,找到数组中第一个能通过该函数测试的项 | ![]() |
Array.prototype.includes(item) | 判断数组中是否存在 item,判定规则使用的是 Object.is | ![]() |
提示
- 数组 API 实际上效率并不高
- 如果项目对数组操作需要注重效率,推荐使用
lodash
库操作数组
(三)对象
1.对象成员速写
- 在某些场景下,ES6 提供了一种更加简洁的方式书写对象成员
1)示例 1
const name = "monica",
age = 17;
const sayHello = function () {
console.log(`my name is ${this.name}`);
};
// 过去的方式
const user = {
name: name,
age: age,
sayHello: sayHello,
};
// 速写
const user = {
name,
age,
sayHello,
};
2)示例 2
// 过去的方式
const MyMath = {
sum: function (a, b) {
//...
},
random: function (min, max) {
//...
},
};
// 速写
const MyMath = {
sum(a, b) {
// ...
},
random(min, max) {
// ...
},
};
2.解构
- ES6 提供了一种特殊的语法,可以轻松的从数组或对象中取出想要的部分
1)示例 1
const user = {
name: "monica",
age: 17,
addr: {
province: "黑龙江",
city: "哈尔滨",
},
};
// 取出 user 中的 name 和 age
const { name, age } = user;
console.log(name, age); // monica 17
// 取出 user 中的 city
const {
addr: { city },
} = user;
console.log(city); // 哈尔滨
2)示例 2
const arr = [1, 2, 3, 4];
// 取出数组每一项的值,分别放到变量a、b、c、d中
const [a, b, c, d] = arr;
// 仅取出数组下标1、2的值
const [, a, b] = arr;
// 仅取出数组下标1、3的值
const [, a, , b] = arr;
// 取出数组前两位的值,放到变量a和b中,剩下的值放到一个新数组arr2中
const [a, b, ...arr2] = arr;
3)示例 3
let a = 1,
b = 2;
// 交换两个变量
[b, a] = [a, b];
4)示例 4
// 在参数位置对传入的对象进行解构
function method({ a, b }) {
console.log(a, b);
}
const obj = {
a: 1,
b: 2,
c: 3,
};
method(obj); // 1 2
5)示例 5
// 箭头函数也可以在参数位置进行解构
const method = ({ a, b }) => {
console.log(a, b);
};
const obj = {
a: 1,
b: 2,
c: 3,
};
method(obj); // 1 2
6)示例 6
const users = [
{ name: "monica", age: 17 },
{ name: "邓哥", age: 70 },
];
// 在遍历时进行解构
for (const { name, age } of users) {
console.log(name, age);
}
3.展开运算符
1)示例 1
const arr = [3, 6, 1, 7, 2];
// 对数组的展开
Math.max(...arr); // 相当于:Math.max(3, 6, 1, 7, 2)
2)示例 2
const o1 = {
a: 1,
b: 2,
};
const o2 = {
a: 3,
c: 4,
};
// 对对象的展开
const o3 = {
...o1,
...o2,
};
/*
o3:{
a: 3,
b: 2,
c: 4
}
*/
3)示例 3
const arr = [2, 3, 4];
const arr2 = [1, ...arr, 5]; // [1,2,3,4,5]
4)示例 4
const user = {
name: "monica",
age: 17,
};
const user2 = {
...user,
name: "邓哥",
};
// user2: { name:'邓哥', age: 17 }
4.属性描述符
- 对于对象中的每个成员,JS 使用属性描述符来描述它们
const user = {
name: "monica",
age: 17,
};
1)描述对象
- 上面的对象,在 JS 内部被描述为
{
// 属性 name 的描述符
name: {
value: 'monica',
configurable: true, // 该属性的描述符是否可以被重新定义
enumerable: true, // 该属性是否允许被遍历,会影响for-in循环
writable: true // 该属性是否允许被修改
},
// 属性 age 的描述符
age: {
value: 17,
configurable: true, // 该属性的描述符是否可以被重新定义
enumerable: true, // 该属性是否允许被遍历,会影响for-in循环
writable: true // 该属性是否允许被修改
},
}
2)操作属性描述符
- ES5 提供了一系列的 API,针对属性描述符进行操作
Object.getOwnPropertyDescriptor(obj, propertyName)
- 该方法用于获取一个属性的描述符
const user = {
name: "monica",
age: 17,
};
Object.getOwnPropertyDescriptor(user, "name");
/*
{
value: 'monica',
configurable: true, // 该属性的描述符是否可以被重新定义
enumerable: true, // 该属性是否允许被遍历,会影响for-in循环
writable: true // 该属性是否允许被修改
}
*/
Object.defineProperty(obj, propertyName, descriptor)
- 该方法用于定义某个属性的描述符
const user = {
name: "monica",
age: 17,
};
Object.defineProperty(obj, "name", {
value: "邓哥", // 将其值进行修改
enumerable: false, // 让该属性不能被遍历
writable: false, // 让该属性无法被重新赋值
});
3)getter 和 setter
- 属性描述符中有两个特殊的配置,分别为
get
和set
- 可以把属性的取值和赋值变为方法调用
const obj = {};
Object.defineProperty(obj, "a", {
get() {
// 读取属性a时,得到的是该方法的返回值
return 1;
},
set(val) {
// 设置属性a时,会把值传入val,调用该方法
console.log(val);
},
});
console.log(obj.a); // 输出:1
obj.a = 3; // 输出:3
console.log(obj.a); // 输出:1
5.键值对
API | 含义 |
---|---|
Object.keys(obj) | 获取对象的属性名组成的数组 |
Object.values(obj) | 获取对象的值组成的数组 |
Object.entries(obj) | 获取对象属性名和属性值组成的数组 |
Object.fromEntries(entries) | 将属性名和属性值的数组转换为对象 |
1)示例
const user = {
name: "monica",
age: 17,
};
Object.keys(user); // ["name", "age"]
Object.values(user); // ["monica", 17]
Object.entries(user); // [ ["name", "monica"], ["age", 17] ]
Object.fromEntries([
["name", "monica"],
["age", 17],
]); // {name:'monica', age:17}
6.冻结
- 使用
Object.freeze(obj)
可以冻结一个对象 - 该对象的所有属性均不可更改
- 可以使用
Object.isFrozen
来判断一个对象是否被冻结
const obj = {
a: 1,
b: {
c: 3,
},
};
Object.freeze(obj); // 冻结对象obj
obj.a = 2; // 不报错,代码无效
obj.k = 4; // 不报错,代码无效
delete obj.a; // 不报错,代码无效
obj.b = 5; // 不报错,代码无效
obj.b.c = 5; // b对象没有被冻结,有效
console.log(obj); // {a:1, b:{ c: 5 } }
7.相同性判定
Object.is
方法,可以判断两个值是否相同- 和
===
的功能基本一致
1)===
NaN !== NaN
+0 === -0
- 该 Bug 与 NaN、+0、-0 的存储格式有关
2)Object.is
NaN === NaN
+0 !== -0
Object.is(1, 2); // false
Object.is("1", 1); // false
Object.is(NaN, NaN); // true
Object.is(+0, -0); // false
8.Set
- MDN 官方文档
- ES6 新增了 Set 结构,用于保存唯一值的序列
9.Map
- MDN 官方文档
- ES6 新增了 Map 结构,用于保存键值对的映射
- 和对象的最大区别在于
- 对象的键只能是字符串
- Map 的键可以是任何类型
(四)函数
1.箭头函数
- 所有使用 函数表达式 的位置,均可以替换为箭头函数
// 完整语法
(参数列表) => {
函数体;
};
// 若有且仅有一个参数【可以省略小括号】
(参数) => {
函数体;
};
// 若函数体有且仅有一条返回语句【可以省略大括号】
(参数列表) => 返回语句;
1)示例 1
const sum = function (a, b) {
return a + b;
};
// 箭头函数写法
const sum = (a, b) => a + b;
2)示例 2
dom.onclick = function (e) {
// ....
};
// 箭头函数写法
dom.onclick = (e) => {
// ...
};
3)示例 3
setTimeout(function () {
// ...
}, 1000);
// 箭头函数写法
setTimeout(() => {
// ...
}, 1000);
4)特点
- 不能使用
new
调用 - 没有原型,即没有
prototype
属性 - 没有
arguments
- 没有
this
有些教程中会说:箭头函数的
this
永远指向箭头函数定义位置的this
,因为箭头函数会绑定this
这个说法没错,根本原因是它没有
this
,它里面的this
使用的是外层的this
const counter = {
count: 0,
start: function () {
// 这里的 this 指向 counter
setInterval(() => {
// 这里的 this 实际上是 start 函数的 this
this.count++;
}, 1000);
},
};
提示
箭头函数的这些特点,都足以说明: 箭头函数特别适用于那些临时需要函数的位置
5)特殊情况
- 立即执行函数
!function() {}
不能写成箭头函数形式!() => {}
会把!()
看作箭头函数的参数- 该形式的参数不合法
- 所以会报错
Uncaught SyntaxError: Malformed arrow function parameter list
- 生成器函数
function* a() {}
不能写成箭头函数形式*() => {}
会把*()
看作箭头函数的参数- 该形式的参数不合法
- 所以会报错
Uncaught SyntaxError: Unexpected token '*'
- 立即执行函数
(function() {})()
可以写成箭头函数形式(() => {})()
2.剩余参数
- ES6 不建议再使用
arguments
来获取参数列表 - 推荐使用剩余参数来收集未知数量的参数
// ...args为剩余参数
function method(a, b, ...args) {
console.log(a, b, args);
}
method(1, 2, 3, 4, 5, 6, 7); // 1 2 [3, 4, 5, 6, 7]
method(1, 2); // 1 2 []
警告
剩余参数只能声明为最后一个参数
3.参数默认值
- ES6 提供了参数默认值,当参数没有传递或传递为
undefined
时,会使用默认值
1)示例 1
// 对参数 b 使用了默认值1
function method(a, b = 1) {
console.log(a, b);
}
method(1, 2); // 1 2
method(1); // 1 1
method(1, undefined); // 1 1
2)示例 2
// 对参数 b 使用了默认值1, 对参数 c 使用默认值2
const method = (a, b = 1, c = 2, d) => {
console.log(a, b, c, d);
};
method(1, 2); // 1 2 2 undefined
method(1); // 1 1 2 undefined
method(1, undefined, undefined, 4); // 1 1 2 4
3)示例 3
{ page = 1, limit = 10, keyword = "" }
表示解构- 如果是空对象,解构会报错
- 所以需要解构的同时赋默认值
= {}
表示默认值为空对象
const getDatas = ({ page = 1, limit = 10, keyword = "" } = {}) =>
console.log(`获取第${page}页的数据,每页显示${limit}条,查询关键字为${keyword === "" ? "空" : keyword}`);
// 等效于
// const getDatas = (config = {}) => {
// const page = config.page || 1;
// const limit = config.limit || 10;
// const keyword = config.keyword || '';
// };
// 输出:获取第1页的数据,每页显示10条,查询关键字为空
getDatas();
// 输出:获取第2页的数据,每页显示10条,查询关键字为空
getDatas({
page: 2,
});
// 输出:获取第2页的数据,每页显示30条,查询关键字为空
getDatas({
page: 2,
limit: 30,
});
// 输出:获取第1页的数据,每页显示10条,查询关键字为js
getDatas({
keyword: "js",
});
4.类语法
1)函数两种调用方式
- 无法从定义上明确函数的用途
- ES6 推出了一种全新的语法来书写构造函数
function A() {}
A(); // 直接调用
new A(); // 作为构造函数调用
2)示例 1
// 旧的写法
function User(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.fullName = `${firstName} ${lastName}`;
}
// 静态方法
User.isUser = function (u) {
return !!u && !!u.fullName;
};
// 实例方法/原型方法
User.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.fullName}`);
};
// 新的等效写法
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.fullName = `${firstName} ${lastName}`;
}
static isUser(u) {
return !!u && !!u.fullName;
}
sayHello() {
console.log(`Hello, my name is ${this.fullName}`);
}
}
3)示例 2
function Animal(type, name) {
this.type = type;
this.name = name;
}
Animal.prototype.intro = function () {
console.log(`I am ${this.type}, my name is ${this.name}`);
};
function Dog(name) {
Animal.call(this, "狗", name);
}
Dog.prototype = Object.create(Animal.prototype); // 设置继承关系
// 新的方式
class Animal {
constructor(type, name) {
this.type = type;
this.name = name;
}
intro() {
console.log(`I am ${this.type}, my name is ${this.name}`);
}
}
class Dog extends Animal {
constructor(name) {
super("狗", name);
}
}
5.函数 API
API | 含义 |
---|---|
Function.prototype.call(obj, ...args) | 调用函数,绑定 this 为 obj 后续以列表的形式提供参数 |
Function.prototype.apply(obj, args) | 调用函数,绑定 this 为 obj args 以数组的形式提供参数 |
Function.prototype.bind(obj, ...args) | 返回一个函数的拷贝 新函数的 this 被绑定为 obj 起始参数被绑定为 args |
const arr = [1, 2, 3];
const arr1 = Array.prototype.slice.call(arr, 1, 3); // [2, 3]
const arr2 = Array.prototype.slice.apply(arr, [1, 3]); // [2, 3]
const newSlice = Array.prototype.slice.bind(arr);
newSlice(1, 3); // [2, 3]
6.函数防抖
- 让某个函数的执行推迟,如果在推迟期间执行函数,会将函数进一步推迟
const debounce = (cb, delay) => {
let timer = null;
return (...args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
cb.call(this, ...args);
}, delay);
};
};
// const debounce = (fn, duration = 1000) => {
// let timer;
// return (...args) => {
// clearTimeout(timer);
// timer = setTimeout(() => {
// fn(...args);
// }, duration);
// };
// };
(五)事件循环
1.浏览器的进程模型
1)进程
- 程序运行需要有它自己专属的内存空间
- 可以把这块内存空间简单的理解为进程
- 每个应用至少有一个进程
- 进程之间相互独立,即使要通信,也需要双方同意
2)线程
- 有了进程后,就可以运行程序的代码了
- 运行代码的 人 称之为 线程
- 一个进程至少有一个线程
- 在进程开启后会自动创建一个线程来运行代码,该线程称之为 主线程
- 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码
- 一个进程中可以包含多个线程
3)浏览器的进程和线程
- 浏览器是一个多进程多线程的应用程序
- 浏览器内部工作极其复杂
- 为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程
可以在浏览器的任务管理器中查看当前的所有进程
- 浏览器进程
- 主要负责界面显示、用户交互、子进程管理等
- 浏览器进程内部会启动多个线程处理不同的任务
- 网络进程
- 负责加载网络资源
- 网络进程内部会启动多个线程来处理不同的网络任务
- 渲染进程
- 渲染进程启动后,会开启一个 渲染主线程
- 主线程负责执行 HTML、CSS、JS 代码
- 默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响
- 渲染进程启动后,会开启一个 渲染主线程
将来该默认模式可能会有所改变,可参见 Chrome 官方说明文档
2.渲染主线程工作原理
- 渲染主线程是浏览器中最繁忙的线程
- 需要它处理的任务包括但不限于
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画 60 次
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
- ......
思考题:为什么渲染进程不适用多个线程来处理这些事情?
1)难题
- 要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?
- 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
- 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
- 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
- ......
2)解决:排队
- 渲染主线程想出了一个绝妙的主意来处理这个问题:排队
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在
- 如果有,就取出第一个任务执行,执行完一个后进入下一次循环
- 如果没有,则进入休眠状态
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务
- 新任务会加到消息队列的末尾
- 在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
- 整个过程,被称之为事件循环(消息循环)
- W3C 文档称为事件循环 Event Loop
- Chrome 源码称为消息循环 Message Loop
3.异步
1)问题
- 代码在执行过程中,会遇到一些无法立即处理的任务
- 计时完成后需要执行的任务
setTimeout
、setInterval
- 网络通信完成后需要执行的任务
XHR
、Fetch
- 用户操作后需要执行的任务
addEventListener
- 计时完成后需要执行的任务
- 如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于 阻塞 的状态,从而导致浏览器 卡死
2)解决:异步
- 渲染主线程承担着极其重要的工作,无论如何都不能阻塞!
- 浏览器选择 异步 来解决这个问题
- 使用异步的方式, 渲染主线程永不阻塞
3)面试题:如何理解 JS 的异步?
- JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个
- 而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行
- 如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行
- 这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象
- 所以浏览器采用异步的方式来避免
- 具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码
- 当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行
- 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行
重要
- 单线程是异步产生的原因
- 事件循环是异步的实现方式
4.JS 阻碍渲染
<h1>Mr.Yuan is awesome!</h1>
<button>change</button>
<script>
var h1 = document.querySelector("h1");
var btn = document.querySelector("button");
// 死循环指定的时间
function delay(duration) {
var start = Date.now();
while (Date.now() - start < duration) {}
}
btn.onclick = function () {
h1.textContent = "袁老师很帅!";
delay(3000);
};
</script>
5.任务
- 任务没有优先级,在消息队列中先进先出
- 消息队列是有优先级的
- 根据 W3C 的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列
- 在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
- 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列
1)队列
- 随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法
- 在目前 chrome 的实现中,至少包含了下面的队列
- 延时队列:用于存放计时器到达后的回调任务,优先级 中
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级 高
- 微队列:用户存放需要最快执行的任务,优先级 最高
- 添加任务到微队列的主要方式主要是使用 Promise、MutationObserver
- 浏览器还有很多其他的队列,由于和我们开发关系不大,不作考虑
// 立即把一个函数添加到微队列
Promise.resolve().then(函数);
2)面试题:阐述一下 JS 的事件循环
- 事件循环又叫做消息循环,是浏览器渲染主线程的工作方式
- 在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行
- 而其他线程只需要在合适的时候将任务加入到队列末尾即可
- 过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式
- 根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列
- 不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务
- 但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行
3)面试题:JS 中的计时器能做到精确计时吗?为什么?
- 不行
- 计算机硬件没有原子钟,无法做到精确计时
- 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差
- 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
(六)Promise 基础
1.情境引入:邓哥的烦恼
- 邓哥心中有很多女神,他今天下定决心,要向这些女神表白,他认为,只要女神够多,根据概率学原理,总有一个会接收他
- 稳妥起见,邓哥决定使用 串行 的方式进行表白:先给第 1 位女神发送短信,然后等待女神的回应,如果成功了,就结束,如果失败了,则再给第 2 位女神发送短信,依次类推
- 邓哥的女神一共有 4 位,名字分别是:李建国、王富贵、周聚财、刘人勇
- 发短信是一个重复性的劳动,邓哥是个程序员,因此决定用函数封装这个动作
/**
* 向某位女生发送一则表白短信
* @params {string} name 女神的姓名
* @params {function} onFulfilled 成功后的回调
* @params {function} onRejected 失败后的回调
*/
function sendMessage(name, onFulfilled, onRejected) {
// 模拟 发送表白短信
console.log(`邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`);
console.log(`等待${name}回复......`);
// 模拟 女神回复需要一段时间
setTimeout(() => {
// 模拟 有10%的几率成功
if (Math.random() <= 0.1) {
// 成功,调用 onFulfilled,并传递女神的回复
onFulfilled(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
} else {
// 失败,调用 onRejected,并传递女神的回复
onRejected(`${name} -> 邓哥:你是个好人😜`);
}
}, 1000);
}
- 有了这个函数后,邓哥于是开始编写程序发送短信了
// 首先向 李建国 发送消息
sendMessage(
"李建国",
(reply) => {
// 如果成功了,输出回复的消息后,结束
console.log(reply);
},
(reply) => {
// 如果失败了,输出回复的消息后,向 王富贵 发送消息
console.log(reply);
sendMessage(
"王富贵",
(reply) => {
// 如果成功了,输出回复的消息后,结束
console.log(reply);
},
(reply) => {
// 如果失败了,输出回复的消息后,向 周聚财 发送消息
console.log(reply);
sendMessage(
"周聚财",
(reply) => {
// 如果成功了,输出回复的消息后,结束
console.log(reply);
},
(reply) => {
// 如果失败了,输出回复的消息后,向 刘人勇 发送消息
console.log(reply);
sendMessage(
"刘人勇",
(reply) => {
// 如果成功了,输出回复的消息后,结束
console.log(reply);
},
(reply) => {
// 如果失败了,就彻底没戏了
console.log(reply);
console.log("邓哥命犯天煞孤星,注定孤独终老!!");
},
);
},
);
},
);
},
);
- 该程序完成后,邓哥内心是崩溃的
- 这一层一层的回调嵌套,形成了传说中的 回调地狱 callback hell
- 邓哥是个完美主义者,怎么能忍受这样的代码呢?
- 要解决这样的问题,需要 Promise 出马
2.Promise 规范
- Promise 是一套专门处理异步场景的规范
- 能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一
- 这套规范最早诞生于前端社区,规范名称为 Promise A+
1)所有的异步场景,都可以看作是一个异步任务
- 每个异步任务,在 JS 中应该表现为一个 对象
- 该对象称之为 Promise 对象,也叫做任务对象
2)每个任务对象,都应该有两个阶段、三个状态
- 任务总是从 未决阶段 变到 已决阶段,无法逆行
- 任务总是从 挂起状态 变到 完成或失败状态,无法逆行
- 时间不能倒流,历史不可改写,任务一旦完成或失败,状态就固定下来,永远无法改变
3)任务的三个状态:挂起、完成和失败
挂起 -> 完成
,称之为resolve
挂起 -> 失败
,称之为reject
- 任务完成时,可能有一个相关数据
- 任务失败时,可能有一个失败原因
4)可以针对任务进行后续处理
- 针对完成状态的后续处理称之为
onFulfilled
- 针对失败的后续处理称之为
onRejected
3.Promise API
- ES6 提供了一套 API,实现了 Promise A+规范
// 创建一个任务对象,该任务立即进入 pending 状态
const pro = new Promise((resolve, reject) => {
// 任务的具体执行流程,该函数会立即被执行
// 调用 resolve(data),可将任务变为 fulfilled 状态, data 为需要传递的相关数据
// 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});
pro.then(
(data) => {
// onFulfilled 函数,当任务完成后,会自动运行该函数,data为任务完成的相关数据
},
(reason) => {
// onRejected 函数,当任务失败后,会自动运行该函数,reason为任务失败的相关原因
},
);
4.邓哥的解决方案
- 学习了 ES6 的 Promise 后,邓哥决定对
sendMessage
函数进行改造
/**
* 向某位女生发送一则表白短信
* @params {string} name 女神的姓名
* @return {object} 返回一个任务对象
*/
function sendMessage(name) {
return new Promise((resolve, reject) => {
// 模拟 发送表白短信
console.log(`邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`);
console.log(`等待${name}回复......`);
// 模拟 女神回复需要一段时间
setTimeout(() => {
// 模拟 有10%的几率成功
if (Math.random() <= 0.1) {
// 成功,调用 resolve,并传递女神的回复
resolve(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
} else {
// 失败,调用 reject,并传递女神的回复
reject(`${name} -> 邓哥:你是个好人😜`);
}
}, 1000);
});
}
- 之后,就可以使用该函数来发送消息了
sendMessage("李建国").then(
(reply) => {
// 女神答应了,输出女神的回复
console.log(reply);
},
(reason) => {
// 女神拒绝了,输出女神的回复
console.log(reason);
},
);
相关信息
至此,回调地狱的问题仍然没能解决
要解决回调地狱,还需要进一步学习 Promise 的知识
(七)Promise 链式调用
1.catch 方法
.catch(onRejected)
=.then(null, onRejected)
2.链式调用
1)then 方法必定会返回一个新的 Promise
- 可理解为
后续处理也是一个任务
2)新任务的状态取决于后续处理
- 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据
- 若有后续处理但还未执行,新任务挂起
- 若后续处理执行了,则根据后续处理的情况确定新任务的状态
- 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
- 后续处理执行有错,新任务的状态为失败,数据为异常对象
- 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致
- 由于链式任务的存在,异步代码拥有了更强的表达力
/*
* 任务成功后,执行处理1,失败则执行处理2
*/
pro.then(处理1).catch(处理2);
/*
* 任务成功后,依次执行处理1、处理2
*/
pro.then(处理1).then(处理2);
/*
* 任务成功后,依次执行处理1、处理2,若任务失败或前面的处理有错,执行处理3
*/
pro.then(处理1).then(处理2).catch(处理3);
3.邓哥的解决方案
/**
* 向某位女生发送一则表白短信
* @params {string} name 女神的姓名
* @params {function} onFulfilled 成功后的回调
* @params {function} onRejected 失败后的回调
*/
function sendMessage(name) {
return new Promise((resolve, reject) => {
// 模拟 发送表白短信
console.log(`邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`);
console.log(`等待${name}回复......`);
// 模拟 女神回复需要一段时间
setTimeout(() => {
// 模拟 有10%的几率成功
if (Math.random() <= 0.1) {
// 成功,调用 onFulfilled,并传递女神的回复
resolve(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
} else {
// 失败,调用 onRejected,并传递女神的回复
reject(`${name} -> 邓哥:你是个好人😜`);
}
}, 1000);
});
}
sendMessage("李建国")
.catch((reply) => {
// 失败,继续
console.log(reply);
return sendMessage("王富贵");
})
.catch((reply) => {
// 失败,继续
console.log(reply);
return sendMessage("周聚财");
})
.catch((reply) => {
// 失败,继续
console.log(reply);
return sendMessage("刘人勇");
})
.then(
(reply) => {
// 成功,结束
console.log(reply);
console.log("邓哥终于找到了自己的伴侣");
},
(reply) => {
// 最后一个也失败了
console.log(reply);
console.log("邓哥命犯天煞孤星,无伴终老,孤独一生");
},
);
(八)Promise 静态方法
1.情境引入:邓哥的新问题
- 邓嫂出门时,给邓哥交待了几个任务:
- 做饭
- 可交给电饭煲完成
- 洗衣服
- 可交给洗衣机完成
- 打扫卫生
- 可交给扫地机器人完成
- 做饭
- 邓哥需要在所有任务结束后给邓嫂汇报工作,哪些成功了,哪些失败了
- 为了最大程度的节约时间,邓哥希望这些任务同时进行,最终汇总结果统一处理
- 每个任务可以看做是一个返回 Promise 的函数
// 做饭
function cook() {
return new Promise((resolve, reject) => {
console.log("邓哥打开了电饭煲");
setTimeout(() => {
if (Math.random() < 0.5) {
resolve("饭已ok");
} else {
reject("做饭却忘了加水,米饭变成了爆米花");
}
}, 2000);
});
}
// 洗衣服
function wash() {
return new Promise((resolve, reject) => {
console.log("邓哥打开了洗衣机");
setTimeout(() => {
if (Math.random() < 0.5) {
resolve("衣服已经洗好");
} else {
reject("洗衣服时停水了,洗了个寂寞");
}
}, 2500);
});
}
// 打扫卫生
function sweep() {
return new Promise((resolve, reject) => {
console.log("邓哥打开了扫地机器人");
setTimeout(() => {
if (Math.random() < 0.5) {
resolve("地板扫的非常干净");
} else {
reject("扫地机器人被哈士奇一爪掀翻了");
}
}, 3000);
});
}
2.Promise 的静态方法
方法名 | 含义 |
---|---|
Promise.resolve(data) | 直接返回一个完成状态的任务 |
Promise.reject(reason) | 直接返回一个拒绝状态的任务 |
Promise.all(任务数组) | 返回一个任务(数据成功时是数组,失败时是单个) 任务数组全部成功则成功 任何一个失败则失败 |
Promise.any(任务数组) | 返回一个任务(数据成功时是单个,失败时是数组) 任务数组任一成功则成功 任务全部失败则失败 |
Promise.allSettled(任务数组) | 返回一个任务(数据成功时是数组) 任务数组全部已决则成功 该任务不会失败 |
Promise.race(任务数组) | 返回一个任务(数据是单个) 任务数组任一已决则已决,状态和其一致 race 竞赛:找第一个有结果的返回 |
3.邓哥的解决方案
Promise.allSettled([cook(), wash(), sweep()]).then((result) => {
// 处理汇总结果
const report = result.map((r) => (r.status === "fulfilled" ? r.value : r.reason)).join(";");
console.log(report);
});
(九)async 和 await
1.消除回调
- 有了 Promise,异步任务就有了一种统一的处理方式
- 有了统一的处理方式,ES 官方就可以对其进一步优化
- ES7 推出了两个关键字
async
和await
,用于更加优雅的表达 Promise
2.async
- async 关键字用于修饰函数
- 被修饰的函数一定返回 Promise
1)返回值不是 Promise
- 该函数的返回值是 Promise 完成后的数据
async function method1() {
return 1;
}
method1(); // Promise { 1 }
2)返回值是 Promise
- method 得到的 Promise 状态和其一致
- 相当于没声明 async
async function method2() {
return Promise.resolve(1);
}
method2(); // Promise { 1 }
3)执行过程报错
- method 得到的 Promise 状态是 rejected
async function method3() {
throw new Error(1);
}
method3(); // Promise { <rejected> Error(1) }
3.await
- await 关键字表示等待某个 Promise 完成
1)必须用于 async 函数中
- 因为等待的 Promise 是异步的
- 所以包含它的函数也必须是异步的
async function method() {
const n = await Promise.resolve(1);
console.log(n); // 1
}
// 等同于
function method() {
return new Promise((resolve, reject) => {
Promise.resolve(1).then((n) => {
console.log(n);
resolve(1);
});
});
}
2)await 可以等待其他数据
async function method() {
const n = await 1; // 等同于 await Promise.resolve(1)
}
try...catch
语法
3)如果需要针对失败的任务进行处理,可以使用 async function method() {
try {
const n = await Promise.reject(123); // 这句代码将抛出异常
console.log("成功", n);
} catch (err) {
console.log("失败", err);
}
}
method(); // 输出: 失败 123
4.邓哥表白的完美解决方案
- 邓哥的女神可不是只有 4 位,而是 40 位!
- 为了更加方便的编写表白代码,邓哥决定把这 40 位女神放到一个数组中,然后利用 async 和 await 轻松完成代码
1)女神的名字数组
const beautyGirls = [
"梁平",
"邱杰",
"王超",
"冯秀兰",
"赖军",
"顾强",
"戴敏",
"吕涛",
"冯静",
"蔡明",
"廖磊",
"冯洋",
"韩杰",
"江涛",
"文艳",
"杜秀英",
"丁艳",
"邓静",
"江刚",
"乔刚",
"史平",
"康娜",
"袁磊",
"龙秀英",
"姚静",
"潘娜",
"萧磊",
"邵勇",
"李芳",
"谭芳",
"夏秀英",
"程娜",
"武杰",
"崔军",
"廖勇",
"崔强",
"康秀英",
"余磊",
"邵勇",
"贺涛",
];
2)发送一则短信
/**
* 向某位女生发送一则表白短信
* @params {string} name 女神的姓名
*/
function sendMessage(name) {
return new Promise((resolve, reject) => {
// 模拟 发送表白短信
console.log(`邓哥 -> ${name}:最近有谣言说我喜欢你,我要澄清一下,那不是谣言😘`);
console.log(`等待${name}回复......`);
// 模拟 女神回复需要一段时间
setTimeout(() => {
// 模拟 有10%的几率成功
if (Math.random() <= 0.1) {
// 成功,调用 onFuffiled,并传递女神的回复
resolve(`${name} -> 邓哥:我是九,你是三,除了你还是你😘`);
} else {
// 失败,调用 onRejected,并传递女神的回复
reject(`${name} -> 邓哥:你是个好人😜`);
}
}, 1000);
});
}
3)批量表白
async function proposal() {
let isSuccess = false;
for (const girl of beautyGirls) {
try {
const reply = await sendMessage(girl);
console.log(reply);
console.log("表白成功!");
isSuccess = true;
break;
} catch (reply) {
console.log(reply);
console.log("表白失败");
}
}
if (!isSuccess) {
console.log("邓哥注定孤独一生");
}
}
proposal();
(十)Promise 相关面试题
1.面试题考点
1)Promise 基本概念
- Promise 状态和数据一旦确定就不可再改变
2)链式调用规则
3)Promise 静态方法
4)async 和 await
5)事件循环
进入事件队列的函数 | 进入的队列(任务类型) |
---|---|
setTimeout 的回调 | 宏任务(Macro Task) |
setInterval 的回调 | 宏任务(Macro Task) |
Promise 的 then 函数回调 | 微任务(Micro Task) |
requestAnimationFrame 的回调 | 宏任务(Macro Task) |
事件处理函数 | 宏任务(Macro Task) |
2.面试题
1)下面代码的输出结果是什么
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
/**
* 1
* 2
* 4
* 3
*/
2)下面代码的输出结果是什么
setTimeout(() => {
console.log(1);
});
const promise = new Promise((resolve, reject) => {
console.log(2);
resolve();
});
promise.then(() => {
console.log(3);
});
console.log(4);
/**
* 2
* 4
* 3
* 1
*/
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log(2);
resolve();
console.log(3);
});
});
promise.then(() => {
console.log(4);
});
console.log(5);
/**
* 1
* 5
* 2
* 3
* 4
*/
3)下面代码的输出结果是什么
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
const promise2 = promise1.catch(() => {
return 2;
});
console.log("promise1", promise1);
console.log("promise2", promise2);
setTimeout(() => {
console.log("promise1", promise1);
console.log("promise2", promise2);
}, 2000);
/**
* promise1 Promise {<pending>}
* promise2 Promise {<pending>}
* promise1 Promise {<fulfilled>} undefined
* promise2 Promise {<fulfilled>} undefined
*/
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 1000);
});
const promise2 = promise1.catch(() => {
return 2;
});
console.log("promise1", promise1);
console.log("promise2", promise2);
setTimeout(() => {
console.log("promise1", promise1);
console.log("promise2", promise2);
}, 2000);
/**
* promise1 Promise {<pending>}
* promise2 Promise {<pending>}
* promise1 Promise {<rejected>} undefined
* promise2 Promise {<fulfilled>} 2
*/
4)下面代码的输出结果是什么
async function m() {
console.log(0);
const n = await 1;
console.log(n);
}
m();
console.log(2);
/**
* 0
* 2
* 1
*/
const n
和console.log(n)
其实是在同一个事件队列(微队列)中m()
立即结束,await 之后的代码进入微队列- 不会卡住在 await,继续执行后续的全局代码,等 await 后面的 Promise 完成后再执行微队列中的代码
- 相当于
function m() {
return Promise.resolve(1).then((n) => {
console.log(n);
});
}
5)下面代码的输出结果是什么
async function m() {
const n = await 1;
console.log(n);
}
(async () => {
await m();
console.log(2);
})();
console.log(3);
/**
* 3
* 1
* 2
*/
6)下面代码的输出结果是什么
async function m1() {
return 1;
}
async function m2() {
const n = await m1();
console.log(n);
return 2;
}
async function m3() {
const n = m2();
console.log(n);
return 3;
}
m3().then((n) => {
console.log(n);
});
m3();
console.log(4);
/**
* 输出
* Promise {<pending>}
* Promise {<pending>}
* 4
* 1
* 3
* 1
*/
/**
* 解析
* Promise {<pending>} // 13行,输出的是m2的状态(执行17行后的输出:m3没有await,不用等待12行m2完成,往后执行)
* Promise {<pending>} // 13行,输出的是m2的状态(执行21行后的输出:m2依旧pending)
* 4 // 23行
* 1 // 7行(微队列任务1,m2状态fulfilled并return 2)
* 3 // 18行(微队列任务2,17行的m3成功后的回调输出)
* 1 // 7行(微队列任务3,21行的m3执行时的m1的回调输出)
*/
7)下面代码的输出结果是什么
- then 方法接收到的参数不是函数
- 那么 then 返回的 Promise 就是其上一个任务的状态
- 相当于 then 无效
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
/**
* 1
*/
8)下面代码的输出结果是什么
- b 接收的是最后一个 then 返回的 Promise 的状态
- then 函数立即调用
- then 函数参数的回调会被加进微队列中
- 所以 b 接收到的 Promise 是 pending 状态
var a;
var b = new Promise((resolve, reject) => {
console.log("promise1");
setTimeout(() => {
resolve();
}, 1000);
})
.then(() => {
console.log("promise2");
})
.then(() => {
console.log("promise3");
})
.then(() => {
console.log("promise4");
});
a = new Promise(async (resolve, reject) => {
console.log(a);
await b;
console.log(a);
console.log("after1");
await a;
resolve(true);
console.log("after2");
});
console.log("end");
/**
* 输出
* promise1
* undefined
* end
* promise2
* promise3
* promise4
* Promise {<pending>}
* after1
*/
/**
* 解析
* promise1 // 延迟1s,转去执行a的new Promise
* undefined // 19行(a变量提升,a的new Promise还未执行完成,所以还未赋值给a)
* end // a在await b,转去执行全局剩余代码,全局执行完成继续执行微队列中的任务
* promise2
* promise3
* promise4
* Promise {<pending>} // 21行(await b完成,但是b没有resolve,状态未改变)
* after1 // await a,但是a的resolve在await之后,所以状态一直是pending
*/
9)下面代码的输出结果是什么
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
/**
* script start
* async1 start
* async2
* promise1
* script end
* async1 end
* promise2
* setTimeout
*/