JS补档

var,let,const 区别

var的作用域是方法作用域,声明之前变量是 undefined.可以重复声明.
let的作用域是块级作用域.在声明之前使用会报错,禁止重复声明.
const是常量声明方式,声明变量时必须初始化,后面不再修改该常量的值.(声明时必须赋值)
const 不是变量的值不能改动,而是变量指向的那个内存地址不能改动.

作用域

ES2015 前,ES 只有两种作用域,分别是全局作用域和函数作用域;在 ES2015 中新增了一个块级作用域.
以前块没有独立的作用域,所以在块中定义的变量,块的外面也可以访问,如:

1
2
3
4
if (true) {
var foo = "foo";
}
console.log(foo);

这对于代码是非常不利的、不安全的,有了块级作用域,可以通过新的关键字 let 去声明变量,用法跟传统一样,只是 let 声明的变量只能在声明的代码块中使用,外部无法访问的,如:

1
2
3
4
if (true) {
let foo = "foo";
}
console.log(foo); // foo is not defined

undefined 和 null 区别

undefined 是未定义的值,是变量最原始的状态
null 是人为声明为空的值.希望表示** 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。** 在内存里的表示就是,栈中的变量没有指向堆中的内存对象

闭包

我的理解: 函数内部包含存在外部作用域的变量,且调用这个函数就形成闭包.
外部调用函数内部的变量
单纯有个函数算闭包环境,被调用了,所使用的外部变量也就无法释放了,这才形成闭包.
注意闭包的函数是 return 出来的.不 return 也可以产生闭包.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function main(i) {
return function (n) {
console.log(i);
return (i += n);
};
}
var result = 10;
var cb = main(result);
console.log(cb(2));
console.log(cb(3));
console.log(result);

// 解释
function main(i) {
// 形参i存在当前作用域
return function (n) {
console.log(i);
// 相当于
i = i + n;
return i;
};
}
// cb(2)时,先打印10,注意function(n)已经是闭包了,i经过计算变为12,但是没有释放.
// cb(3)时,先打印12, i因为闭包没有释放依旧是12, 再计算后是15.
// result 一直未被修改还是10.

位于全局作用域的闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var result = [];
var a = 3;
var total = 0;

function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a;
console.log(total);
};
}
}
//tip:这里也形成了闭包。total 被外层引用没有被销毁。
foo(1);
result[0](); // 3
result[1](); // 6
result[2](); // 9

疑问:下面这个算闭包吗,答:不算,把 var 换成 let 就算闭包了.
for 循环不是函数,所以 i 是全局作用域中的变量。
如果换成 let,其实内部也是闭包的机制,当 onclick 执行是循环早已执行完毕,i 早已销毁,因为闭包的机制我们才能拿到 i 对应的值.也就是说循环的 i 已经完事了,但是块级作用域的 i 还在.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
//data是数组,数组中存储的是函数,函数中的i其实早已为3,打印出来都是3
//为什么输出3 ?
//因为在执行函数的时候 i 已经完成遍历了,
//data[i]执行后寻找i, 内部没有,向上寻找,这时i 是全局变量, 并且此时的值为3
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function () {
// 暂时取名 fn
console.log(i);
}; // i是自由变量,所以这是一个闭包
})(i);
}

data[0]();
data[1]();
data[2]();
//在data[0]执行,即fn执行,此时的全局变量 i依旧是 3(但跟fn中的i无关)
//fn内部没有i,向上查找, 找到在for内,匿名函数传进来的i值0,它依旧存在在内存中。
//所以到此不会再向上去到全局作用域中查找。所以此时会打印 0

局部变量:在函数中声明,且在函数返回后不会被其他作用域所使用的对象。下面代码中的 local* 都是局部变量。(scopes 是一块堆内存)
全局变量 ,在浏览器上为 window ,在 node 里为 global。全局变量会被默认添加到函数作用域链的最底端,也就是上述函数中 [[Scopes]] 中的最后一个,可以看下上面局部变量例子中 Scopes 的最后一个。

var:全局的 var 变量其实仅仅是为 global 对象添加了一条属性。
let / const:全局的 let/const 变量不会修改 window 对象,而是将变量的声明放在了一个特殊的对象下(与 Scope 类似)。
被捕获变量就是局部变量的反面:在函数中声明,但在函数返回后仍有未执行作用域(函数或是类)使用到该变量,那么该变量就是被捕获变量。

一般如何产生闭包

  • 返回函数
  • 函数当做参数传递

函数科里化是一种闭包.

for 循环

如果是 var 声明,for 循环不是函数,所以 i 是全局作用域中的变量。

使用 let 关键字时会产生块级作用域,for 每次循环的大括号都是一个独立的块级作用域,由于后面还要用到 i ,所以这几个块级作用域都不会销毁。
elements[0].onclick = function() { alert(i); } // 在此块级作用域中,i = 0
elements[1].onclick = function() { alert(i); } // 在此块级作用域中,i = 1
elements[2].onclick = function() { alert(i); } // 在此块级作用域中,i = 2
elements[3].onclick = function() { alert(i); } // 在此块级作用域中,i = 3

1
2
3
4
5
6
7
var element = document.getElementsByTagName("li");
var length = element.length;
for (let i = 0; i < length; i++) {
element[i].onclick = function () {
alert(i);
};
}

for 循环是同步代码,先执行,事件绑定函数是异步代码.后执行.此时 for 执行完毕,i 是最后一个值.

性能

for > for-of > forEach > map > for-in

  • for 循环当然是最简单的,因为它没有任何额外的函数调用栈和上下文;
  • for...of只要具有 Iterator 接口的数据结构,都可以使用它迭代成员。它直接读取的是键值。
  • forEach,因为它其实比我们想象得要复杂一些,它实际上是 array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;
  • map() 最慢,因为它的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大。
  • for...in需要穷举对象的所有属性,包括自定义的添加的属性也能遍历到。且 for…in 的 key 是 String 类型,有转换过程,开销比较大。

如果你需要将数组按照某种规则映射为另一个数组,就应该用 map。
如果你需要进行简单的遍历,用 forEach 或者 for of。
如果你需要对迭代器进行遍历,用 for of。
如果你需要过滤出符合条件的项,用 filterr。
如果你需要先按照规则映射为新数组,再根据条件过滤,那就用一个 map 加一个 filter

for…in

是 ES5 版本发布的。以任意顺序遍历一个对象的除 Symbol 以外的可枚举属性。
迭代数组时,for...in是下标,for...of是值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 遍历对象
let profile = {name:"April",nickname:"二十七刻",country:"China"};
for(let i in profile){
let item = profile[i];
console.log(item) // 对象的键值
console.log(i) // 对象的键对应的值
// 遍历数组
let arr = ['a','b','c'];
for(let i in arr){
let item = arr[i];
console.log(item) // 数组下标所对应的元素
console.log(i) // 索引,数组下标
// 遍历字符串
let str = "abcd"
for(let i in str){
let item = str[i];
console.log(item) // 字符串下标所对应的元素
console.log(i) // 索引 字符串的下标
}

for…of

我是 ES6 版本发布的。在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 迭代数组数组
let arr = ['a','b','c'];
for(let item of arr){
console.log(item)
}
// logs 'a'
// logs 'b'
// logs 'c'
// 迭代字符串
let str = "abc";
for (let value of str) {
console.log(value);
}
// logs 'a'
// logs 'b'
// logs 'c'
// 迭代map
let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]
for (let entry of iterable) {
console.log(entry);
}
// logs ["a", 1]
// logs ["b", 2]
// logs ["c", 3]
// 迭代map获取键值
for (let [key, value] of iterable) {
console.log(key)
console.log(value);
}
// 迭代set
let iterable = new Set([1, 1, 2, 2, 3, 3,4]);
for (let value of iterable) {
console.log(value);
}
// logs 1
// logs 2
// logs 3
// logs 4
// 迭代 DOM 节点
let articleParagraphs = document.querySelectorAll('.article > p');
for (let paragraph of articleParagraphs) {
paragraph.classList.add("paragraph");
// 给class名为“article”节点下的 p 标签添加一个名为“paragraph” class属性。
}
// 迭代arguments类数组对象
(function() {
for (let argument of arguments) {
console.log(argument);
}
})(1, 2, 3);
// logs:
// 1
// 2
// 3
// 迭代类型数组
let typeArr = new Uint8Array([0x00, 0xff]);
for (let value of typeArr) {
console.log(value);
}
// logs:
// 0
// 255

函数柯里化

高阶函数: 函数可以作为参数传递 && 函数可以作为返回值输出
柯里化(Currying): 把接受多个参数的函数变换成接受一个单一参数(或部分)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
示例:
编写一个 add 函数,使得 add(1,2)和 add(1)(2)都可以执行,并返回 3.

1
2
3
4
//需要判断参数值的长度
add(a,b){
if(arguments.length === 1 ? b => a + b : a+b
}

string 和 String 的区别

String 是包装类,是一个特殊的 object.
也就是说当不用 new 的时候,String(…) === toString(…)

new String() 和 String()区别

我们知道 new 关键字的过程涉及到新对象的创建,所以,new String(str)的结果返回的一个新的 String 实例,所以,b 和 b2 保存的是两个对象的引用,他们的引用地址不一样,直接比较的话,逻辑引用类型的比较是一样的,结果就是不相等。

switch 语句

注意: 如果没写 break,下面的语句也会执行,不管是否符合条件。
default 是条件都不满足才执行。

1
2
3
4
5
6
7
8
9
10
11
switch (expression) {
case 条件1:
console.log("满足条件1");
// 如果这里没写break,下面条件2的语句也会执行,不管是否符合条件2
case 条件2:
console.log("满足条件2");
break;
default:
console.log("和上两个条件都不相等");
break;
}

expression  中是可能变化的量,变化的可能性有三种,条件 1,2,和不满足条件 12 的其他种类.

空数组 push 后,再次调用记得赋值为空.

map 和 forEach

map 返回处理后的新数组,而不是原数组的处理.原数组是不变的.

能用 forEach()做到的,map()同样可以。反过来也是如此。
map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
forEach()允许 callback 更改原始数组的元素。map()返回新的数组。

区别

forEach 执行后返回 undefined
map 执行后返回新数组

共同点

只能遍历数组并参数都一样
不改变原函数(引用类型除外)
无法中断循环;return 只是结束本次循环,进入下一次循环
break 或 continue 都将会报错

forEach 和 map 可以通过return false跳出本次循环,但是不能中断整个循环。
break 和 continue 也会报错。想要中断,可以使用thorow new Error
for/for…of: break 跳出本次循环;continue 结束本次循环执行下一次循环,没有 return。
for…in:会忽略 break || continue。没有 return。

1
2
3
4
5
6
7
8
9
10
11
12
13
let list = [
{ id: "1", time: "10:00.000" },
{ id: "2", time: "11:00.000" },
{ id: "3", time: "12:00.000" },
];
let listMap = list.map((item) => item.time.split(".").splice(0, 1).join(""));
//listMap返回时被处理的时间的字符串数组,原数组list没有变化
console.log(listMap); //["10:00", "11:00", "12:00"]
let listMap2 = list.forEach((item) =>
item.time.split(".").splice(0, 1).join("")
);
//forEach不返回新数组
console.log(listMap2); //undefined

数组返回

注意: map,filter,find,findIndex等必须要return才能生效.

类数组转数组方法

  1. Array.prototype.slice.call(obj) // 因为 slice 返回一个新数组,原数组不变。
  2. Array.from
  3. 扩展运算符

Array.from

该方法对一个类数组或可迭代对象创建一个浅拷贝的数组实例。
方法接收 3 个参数,
第一个是类数组或可迭代对象,
第二个参数是个回调,新数组的每个元素都会执行该回调,
第三个参数是执行第二个回调函数的 this 对象。

1
2
Array.from([1, 2, 3], (x) => x * 2); // [2,4,6]
Array.from({ length: 5 }, (v, i) => i); // [1,2,3,4,5]

reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let people = [
{ name: "Alice", age: 21 },
{ name: "Max", age: 20 },
{ name: "Jane", age: 20 },
];

function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
let key = obj[property];

if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj);
// console.log(acc)
//这里acc[key]是value,也就是value里push
return acc;
}, {});
}

let groupedPeople = groupBy(people, "age");
// groupedPeople is:
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }

reduce 的理解

reduce 接收由数组调用,接收一个函数,和一个可选参数,初始值。如果没有初始值,默认初始值是函数的第一个参数。函数有 3 个参数,分别是累加器 acc,当前值 cur,当前值的索引 index。

reduce 有几种常用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 累加
const arr = [1, 2, 3];

arr.reduce((acc, cur) => {
// return 就是下一次累加的结果
return acc + cur;
}, 0);
// 流程就是第一次0+1
// 第二次 1 + 2
// 第三次 3 + 3
// 最终结果6

// 多维数组转化一维数组(数组扁平化)
const arr2 = [
[1, 2],
[3, [4, 5]],
]; // => [1,2,3,4,5]

const flat = (arr) => {
arr.reduce((prev, cur) => {
// 判断cur是不是数组,如果不是数组,就把它拼接到空数组里
// concat是可以连接数组或值的
// 进入递归的数组,最后返回的也是一个一维数组
return prev.concat(Array.isArray(cur) ? flat(cur) : cur);
}, []);
};

// 统计数组中值的个数
const arr3 = ["a", "b", "c", "a", "c"]; // => {a: 2, b: 1, c: 2}
// 上面说明最终要返回一个对象

arr3.reduce((prev, cur) => {
// 如果对象中没有这个值,那就把它放进去,赋值一次
if (!prev[cur]) {
prev[cur] = 1;
} else {
// 如果已经出现了,次数+1
prev[cur]++;
}
// 把这个对象返回出去
return prev;
}, {});

// 实现Promise按顺序执行
const arr = [p1, p2, p3];
arr.reduce((prev, cur) => {
// 就是把链式调用使用reduce循环了
return prev.then(cur);
}, Promise.resolve());

// 实现map
if (!Array.prototype.mapUseReduce) {
Array.prototype.mapUseReduce = function (callback, thisArgs) {
// this就是调用的数组
return this.reduce(function (prev, cur, index, array) {
// map中回调需要的参数正好是reduce的后三个参数
// map执行就是循环每一项,执行回调
prev[index] = callback.call(thisArgs, cur, index, array);
return prev;
}, []);
};
}

// 实现组合函数
const compose =
(...fns) =>
(value) =>
fns.reverse().reduce((prev, acc) => acc(prev), value);

对象类型的指针问题

js 中,参数传递只有一种规则:按值传递,基于值的复制。原始类型复制的是值本身,所以这两份数据互不影响;引用类型复制的是引用值,所以形参和实参指向同一个对象,通过一个饮用修改了对象,那么通过另外一个引用访问的对象就是修改后的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function test(person) {
person.age = 26;
person = {
name: "yyy",
age: 30,
};

return person;
}
const p1 = {
name: "yck",
age: 25,
};
const p2 = test(p1);
console.log(p1); // -> ?
console.log(p2); // -> ?

解释:
p1 =>指向对象{name:'yck',age:25}的指针00001.
test(p1)传指针,person.age = 26修改该对象.此时p1 = {name: 'yck', age:26}
接下来要给person重新赋值,但是person指向p1的指针被覆盖,重新开辟一块内存,新的指针00002.
person拥有一个新的地址.person = {name: 'yyy', age: 30}.返回出来.

运算符

逻辑非(!)

逻辑非的规则:
操作数能被转化为true的,都返回false.否则返回true.
举例:
下面都被转换成false:
null,NaN,0, '',undefined.
所以遇到!会取反转为true.

遇到对象会返回false.

1
2
3
4
let a = "0";
!a == false; // 字符串取反,就是false, 所以判断结果为true
a == false; // 使用==转化false为0, "0"和0比较,"0"被转化为0,结果为true
a == 0; //跟上一条一样

双逻辑非(!!)

用于始终返回true或者false
比如!!undefined返回false,否则直接会返回undefined.

空值合并运算符(??)

当左侧为nullundefined,返回右侧。
||的区别,当左侧是''或者0,是可以取左侧的值的。

(,)逗号运算符

逗号表达式的值是成员表达式最右侧的值。

1
2
3
4
5
6
7
8
9
let a, b, c;

(a = b = 3), (c = 4); // 值 4 返回到控制台
console.log(a); // 3 (left-most)

let x, y, z;

x = ((y = 5), (z = 6)); // 值 6 返回到控制台
console.log(x); // 6 (right-most)

二进制运算符

<<:左偏移,将数值转换为二进制,然后向左偏移多少位.
|:按位或.按照二进制转换后,位数有则计算.

1
2
3
4
5
6
7
let a = 1 << 1;
//二进制: 0000 0001 => 左偏移1位 => 0000 0010 => 2
let b = 2 << 2;
//二进制: 0000 0010 => 左偏移2位 => 0000 1000 => 8
let c = a | b;
//二进制: 0000 0010
//二进制: 0000 1000 => 0000 1010 =>10

in运算符

指定的属性在指定的对象或其原型链中,则in运算符返回 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 数组
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees; // 返回true
3 in trees; // 返回true
6 in trees; // 返回false
"bay" in trees; // 返回false (必须使用索引号,而不是数组元素的值)

"length" in trees; // 返回true (length是一个数组属性)

Symbol.iterator in trees; // 返回true (数组可迭代,只在ES2015+上有效)

// 内置对象
"PI" in Math; // 返回true

// 自定义对象
var mycar = { make: "Honda", model: "Accord", year: 1998 };
"make" in mycar; // 返回true
"model" in mycar; // 返回true

如果只是将一个属性值赋值为undefined,而没有删除,则in运算符仍然会返回 true.

判断对象上是否有某个属性

方法有: for…in,Object.keys,in 操作符, hasownProperty。
for…in:可以遍历包括原型链上的属性
Object.keys: 遍历所有可枚举属性,不包括原型链
in 操作符: 可以检查原型链
hasOwnProperty:无法检查原型链.(返回布尔值,判断对象中是否有该值)

类型判断

值类型用**typeof**,引用类型用**instanceof**

typeof

运算符返回一个字符串,表示操作数的类型。
原始类型: 除了 null 判断为 object,其他都正常.因为 null 是全零,被判断为 object.
引用类型: 除了函数显示function,其他都是object.

instanceof

判断对象类型就可以用 instanceof,它会通过原型链判断.
但是原始类型就不行了.

判断是否是数组

  1. Array.isArray()
  2. Object.prototype.toString.call(arr)
  3. instanceof

类型转换

类型 boolean Number String
Boolean true true 1 “true”
Boolean false false 0 “false”
Number 123 true 123 “123”
Number Infinity true Infinity “Infinity”
Number 0 false 0 “0”
Number NaN false NaN “NaN”
String “” false 0 “”
String “123” true 123 “123”
String “123abc” true NaN “123abc”
String “abc” true NaN “abc”
Null null false 0 “null”
Undefined undefined false NaN “undefined”
Function function(){} true NaN “function(){}”
Object {} true NaN “[object Object]”
Array [] true 0 “”
Array [“abc”] true NaN “abc”
Array [“123”] true 123 “123”
Array [“123”,”a”] true NaN “123,a”

加法(特殊)

四则运算符合两个规则:

  1. 运算符一方为字符串,会把另一方转化为字符串.
  2. 如果一方不是字符串或数字,就会把它转化为字符串或数字.
1
2
3
1 + "1"; // "11"
true + true; // 2
4 + [1, 2, 3]; // "41,2,3"
  • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 ‘11’
  • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
  • 对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3

另外对于加法还需要注意这个表达式 ‘a’ + + ‘b’:

`a++b`返回`aNaN`.因为`+'b'`返回`NaN`.
`+ '1'`快速获取number类型.

除了加法

只要其中一方是数字,那么另一方就会被转为数字

1
2
3
4 * "3"; // 12
4 * []; // 0
4 * [1, 2]; // NaN

==运算符

  1. 如果一个操作数是布尔值,在比较之前要转成 number.
  2. 如果一个操作数是字符串,另一个是 number,要把字符串转成 number

解释[] == ![]为 true.

1
2
3
4
// 首先转化![] 为false
// 布尔值要先转成number => false->0
// [] 就要转成number -> 0
// 0 == 0 //true

解释{} == !{}为 false

1
2
3
// 基本和上面相同
// 差异在于{} 转为number是NaN
// NaN == 0 就是false

基本类型

undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。
{ }是一个不完全空对象,原型链上有Objectnull为原型链顶端,因此Object.prototype.__proto__ === null为 true。

null 是完全空对象,原型链也没有。

相等性

严格相等:===
非严格:==
Object.is:判断是否是同一个值

0.1+0.2 != 0.3

因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。
0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。

那么这些循环的数字被裁剪了,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了,而是变成了 0.100000000000000002.

那么可能你又会有一个疑问,既然 0.1 不是 0.1,那为什么 console.log(0.1) 却是正确的呢?

因为在输入内容的时候,二进制被转换为了十进制,十进制又被转换为了字符串,在这个转换的过程中发生了取近似值的过程,所以打印出来的其实是一个近似值.

如何解决

1
parseFloat((0.1 + 0.2).toFixed(10));

this 的指向

this 的指向与函数声明无关,取决于函数调用.

  1. 普通函数调用(fn()) -> this 指向 window
  2. 对象方法调用(xx.fn()) -> this 指向对象
  3. 构造函数调用(new fn()) -> this 指向 new 创建的实例对象
  4. 匿名函数中的 this:匿名函数的执行具有全局性,则匿名函数中的 this 指向是 window,而不是调用该匿名函数的对象

例如:
box.onclick = function(){}这就属于对象方法调用,this 也就指向 box.

函数调用的优先级

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

箭头函数的 this

  1. 箭头函数中的 this 是在函数定义的时候就确定下来的,而不是在函数调用的时候确定的;
  2. 箭头函数中的 this 指向父级作用域的执行上下文;(技巧:因为 javascript 中除了全局作用域,其他作用域都是由函数创建出来的,所以如果想确定 this 的指向,则找到离箭头函数最近的 function,与该 function 平级的执行上下文中的 this 即是箭头函数中的 this
  3. 箭头函数无法使用 apply、call 和 bind 方法改变 this 指向,因为其 this 值在函数定义的时候就被确定下来
1
2
3
4
5
6
7
8
9
let obj = {
//此处的this即是箭头函数中的this
getThis: function () {
return () => {
console.log(this);
};
},
};
obj.getThis()(); //obj
1
2
3
4
5
6
7
8
9
//代码中有两个箭头函数,由于找不到对应的function,所以this会指向window对象。
let obj = {
getThis: () => {
return () => {
console.log(this);
};
},
};
obj.getThis()(); //window

call,apply,bind

笨方法记不同:call=>list, apply=>arr.
不同点:

  1. 传参不同,call->多个参数,apply->数组,bind->就一个 this
  2. 执行机制不同: call,apply 立即执行,bind 不执行.

call的使用场景:
检测数据类型:

  1. typeof,缺点:无法检测null,array
  2. 使用Object.prototype.toString.call().该方法会返回固定类型格式[object type],其中 type 是类型
1
2
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call([1, 2, 3]); // [object Array]

实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(uname, age) {
this.uname = uname;
this.age = age;
}
Person.prototype.getValue = function(){
console.log(this.uname)
}
function Son(uname, age) {
Person.call(this, uname, age);
}
Son.prototype = Object.create(Parent.prototype, {
constructor() {
value: Son, // 设置constructor的值是Son,说明原型对象的构造函数是Son
enumerable: false,
writable: true,
configurable: true
}
})
var son = new Son("zhang", 12);
console.log(son);

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数
apply使用场景:
类数组转真数组:

ES5:上下文

1
2
let newArr = [];
newArr.push.apply(newArr, obj);

ES6: Array.from()
该方法可以将类数组直接转化为真数组.
取数组最大值:
ES5:

1
2
let arr = [1, 2, 3];
Math.max.apply(Math, arr);

ES6:Math.max(...arr)

bind不会立即调用函数,而是得到一个修改this后的新函数.(一次修改,终身受用)
细节: 如果在 bind 后传递参数,参数也会绑定,但是调用函数所传参数就会失效了.
修改不需要立即执行的函数:
比如事件处理函数,定时器

如果对一个函数进行多次 bind,那么上下文会是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
let a = {};
let fn = function () {
console.log(this);
};
fn.bind().bind(a)();

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function () {
return fn.apply();
}.apply(a);
};
fn2();

无论bind多少次,this 永远指向第一次 bind.

  • 对于 new 的方式来说,this 被永远绑定在了实例上面,不会被任何方式改变 this
  • 箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

    首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo()这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

原型和原型链

每个实例对象都有一个私有属性__proto__,指向构造函数的原型对象prototype
该原型对象也有自己的原型对象,层层向上直到一个对象的原型对象为 null。这构成了原型链。
null 没有原型,是原型链的最后一环。

继承

当访问一个对象的属性时,不只从该对象上寻找,还从原型链上查找,直到找到或者到达原型链的末端。

constructor 和原型对象,构造函数,实例对象

prototype属于构造函数,指向原型对象. 作用:解决内存浪费和变量污染.
constructor属于原型对象,指向构造函数.作用:可以让实例对象知道自己是被谁创建的.
__proto__属于实例对象,指向原型对象.作用:实例对象访问原型对象的成员.
image.png

如何方便理解三者关系,构造函数是父级,父级的 prototype(老婆)就是原型对象,原型对象的 constructor(老公)就是构造函数,子级实例对象的proto(母亲)就是原型对象.

实例对象是无条件继承原型对象.
原型链的作用就是继承.也就说,js 是利用原型链实现面向对象继承的.

1
2
3
4
5
6
7
8
9
10
11
//构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 原型对象
Person.prototype.eat = function () {
console.log("吃东西");
};
// 实例对象
const p1 = new Person("张三", 30);

构造函数中的方法会导致内存的浪费.
两个实例对象虽然都可以调用 eat()方法,但是两个方法堆地址不同.每次调用函数都会在堆内存中生成一块新的空间.
image.png
解决方法 1: 使用全局变量.即将函数在全局书写,在构造函数内赋值,也即只获取地址.
缺点: 导致变量污染.
解决方法 2: 使用对象将全局变量函数包裹,但是治标不治本.
最终解决方法: 原型对象.

原型对象

原型对象是构造函数的默认属性 prototype 所指向的一个空对象.用于解决内存浪费和变量污染的问题.

1
Person.prototype.eat = function () {};

与 ES6 中继承的关系

ES6 中继承的方法属于 ES5 的语法糖.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent {
constructor(value) {
this.val = value;
}
getValue() {
console.log(this.val);
}
}
class Child extends Parent {
constructor(value) {
super(value);
this.val = value;
}
}
let child = new Child(1);
child.getValue(); // 1
child instanceof Parent; // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。

instanceof

用法:检测构造函数的原型在不在实例对象的原型链上.
arr 的原型链: arr ->Array.prototype -> Object.prototype -> null

1
2
3
arr instanceof Array; //true
arr instanceof Object; //true
arr instanceof null; //typeError,右侧不是一个object

工厂函数和构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createPerson(name, age) {
let p = {};
p.name = name;
p.age = age;
return p;
}

// 构造函数
function CreatePerson(name, age) {
this.name = name;
this.age = age;
}
// 通过new关键字调用的是构造函数
let p1 = new CreatePerson("张三", 30);

new 的用途

  1. 创建空对象.方法很多种,对象字面量const obj = {},或者Object.create()
  2. 将空对象的原型指向构造函数的原型. obj.__proto__ = constructor.prototype
  3. 将空对象作为构造函数的上下文(改变 this 指向).const res = constructor.apply(obj, args)
  4. 对构造函数的返回值进行处理判断return res instanceof Object ? res : obj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 模仿new关键词实现
* @param {Function} constructor 构造函数
* @param {...any} argument 任意参数
*/
const _new = (constructor, ...argument) => {
const obj = {}; //创建一个空的简单对象
obj.__proto__ = constructor.prototype; //设置原型
//新创建的对象作为this的上下文传递给构造函数
const res = constructor.apply(obj, argument);
//如果该函数没有返回对象,则返回this(这个this指constructor执行时内部的this))。
return typeof res === "object" ? res : obj;
};

function Person(name, sex) {
this.name = name;
this.sex = sex;
}

const people = new Person("Ben", "man");
const peopleOther = _new(Person, "Alice", "woman");
console.info("people", people); // people Person { name: 'Ben', sex: 'man' }
console.info("peopleOther", peopleOther); // peopleOther Person { name: 'Alice', sex: 'woman' }
console.info("people.__proto__", people.__proto__); //people.__proto__ Person {}
console.info("peopleOther.__proto__", peopleOther.__proto__); //peopleOther.__proto__ Person {}

new 的细节

  1. 构造函数的首字母大写,为了提醒别人不要忘记 new 关键字
  2. 关于构造函数主动写 return.对于值类型,无效,还是返回 new 创建的对象,对于引用类型,会覆盖.
  3. new 的 this 问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
function User() {
this.name = 'John';

setTimeout(function greet() {
console.log(`Hello, my name is ${this.name}`); // Hello, my name is
console.log(this); // window
}, 1000);
setTimeout(()=>{
console.log(`name is ${this.name});
},1000)
}

const user = new User();

对象字面量,new Object(),Object.create()区别

字面量和 new 关键字创建的对象是 Object 的实例,原型指向 Object.prototype,继承内置对象 Object

Object.create(arg, pro)创建的对象的原型取决于 arg,arg 为 null,新对象是空对象,没有原型,不继承任何对象;arg 为指定对象,新对象的原型指向指定对象,继承指定对象

1
2
3
4
5
6
7
8
9
10
11
var company = {
address: "beijing",
};
var yideng = Object.create(company);
delete yideng.address;
console.log(yideng.address);
// 写出执行结果,并解释原因
//beijing
//这里的 yideng 通过 prototype 继承了 company的 address。
//yideng自己并没有address属性。所以delete操作符的作用是无效的。
//yideng.address只是通过原型链去继承company的address

Object.assign()

语法: Object.assign(target, ...source)
解释: 将所有可枚举属性的值从一个或多个对象分配到目标对象.最后返回目标对象.

1
2
3
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }

Object.assign()只能用于浅拷贝,无法用于深拷贝.

1
let obj = { a: 1, b: { c: 0 } };

对于这种就无法拷贝到对象 c.只能使用JSON.parse(JSON.string(obj)).
继承属性和不可枚举属性是不能被拷贝的.
原始类型会被包装,只有字符串的包装对象才可能有可枚举属性.

浅拷贝

可以使用扩展运算符实现浅拷贝

1
2
3
4
let obj = { a: 1 };
let obj2 = { ...obj };
obj.a = 2;
console.log(obj2.a); //1

深拷贝

通常可以通过 JSON.parse(JSON.stringify(object))来解决。
缺点:
忽略undefined,Symbol,函数,循环引用对象.

Event loop

执行 JS 代码就是往执行栈中放函数.遇到异步代码就挂起,需要执行的时候放到 TASK 中(有多种 task).
执行栈空了之后,从 task 中拿出要执行的代码放到执行栈中执行.

所以本质上说,异步还是同步行为.

浏览器的 event loop

  • js 引擎首先从宏任务中取出第一个(一般是 script),
  • 执行完毕后,将微任务中所有的任务取出,按顺序执行
  • 如果在这一步产生新的微任务也要执行
  • 执行完后,会进行 DOM 渲染
  • 进入下一轮 event loop,再取出一个宏任务,,执行完后取出所有的微任务,执行.
    一次eventloop循环会处理一个宏任务和所有这次循环产生的微任务

异步和同步

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

  • 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

进程和线程

本质上两个名词都是 CPU 工作时间片的描述.

  • 进程是 CPU 在运行指令,加载和保存上下文所需的时间.放在应用上就代表了一个程序.
  • 线程是进程中更小的单位,描述执行一段指令所需的时间.

执行栈

可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,
当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题

执行顺序

  • 先执行同步代码,属于宏任务.
  • 执行完后,执行栈空,查询是否有异步代码
  • 执行微任务
  • 执行微任务后,如有必要,会重新渲染页面
  • 开始下一轮 Event loop,执行宏任务中的异步代码,

TASK(微任务和宏任务)

在 ES6 中,微任务叫jobs,宏任务叫task.
微任务包括: process.nextTick(), Promise,MutationObserver.
宏任务包括: script,setTimeout, setInterval,setImmediate,I/O,UI rendering,DOM 事件,DOM 渲染,AJAX 请求.
而且微任务并非快于宏任务,因为宏任务包括script,浏览器会先执行一个宏任务,接下来有异步代码才会执行微任务.

微任务和宏任务之间隔了一个 DOM 渲染.
执行顺序:
微任务 > DOM 渲染 > 宏任务

关于 promise

promise 内部是同步代码,then()才是异步代码,遇到 then 就放到微任务队列中.
疑惑:
多个 then,嵌套 then 的顺序?
先将第一个 then 加入到微任务队列中,后面的拿不到状态,因为前面还是 pending,所以还无法放入队列.
等到前一个执行完毕,才加入队列.
then 中如果又 return 一个 promise,则前一个 promise 的其他 then 就挂到新的 promise 下.如果新的 promise 没有 return,那就要看有没有resolve(),有resolve()就继续走一个 then,因为resolve表示上一个 promise 状态 fulfilled 了,可以继续.而 then 中或者 await 中的 promise 就看有没有 return 了,如果是return Promise那么就直接跳出这个包裹函数了,包裹函数后面的异步就放在最后才执行.
setTimeout 中含有 promise 的顺序?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
console.log("script start");

async function async1() {
await async2(); //2.回到这里,遇到await,让出线程,执行之后的代码,所以下面的也不放到微任务队列
console.log("async1 end"); //3. 当同步代码执行完毕,回到这里,
//将resolve放到微任务队列中,执行后面then中的回调.在两次tick后执行这里的微任务.
}
async function async2() {
console.log("async2 end"); //1.立即执行,执行完返回一个Promise
await Promise.resolve(console.log("res"));
}
async1();

setTimeout(function () {
console.log("setTimeout");
}, 0);

new Promise((resolve) => {
console.log("Promise");
resolve();
})
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});

console.log("script end");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// => 代码一执行就开始执行了一个宏任务-宏0
console.log("script start");

setTimeout(() => {
// 宏 1
console.log("setTimeout");
}, 1 * 2000);

Promise.resolve()
.then(function () {
// 微1-1
console.log("promise1");
})
.then(function () {
// 微1-4 =>
// 这个then中的会等待上一个then执行完成之后得到其状态才会向Queue注册状态对应的回调,
//假设上一个then中主动抛错且没有捕获,那就注册的是这个then中的第二个回调了。
console.log("promise2");
});

async function foo() {
await bar(); // => await(promise的语法糖),会异步等待获取其返回值
// => 后面的代码可以理解为放到异步队列微任务中。 这里可以保留疑问后面会详细说
console.log("async1 end"); // 微1-2
}
foo();

function bar() {
console.log("async2 end");
}

async function errorFunc() {
try {
await Promise.reject("error!!!");
} catch (e) {
// => 从这后面开始所有的代码可以理解为放到异步队列微任务中
console.log(e); // 微1-3
}
console.log("async1");
return Promise.resolve("async1 success");
}
errorFunc().then((res) => console.log(res)); // 微1-5

console.log("script end");

// run:
// 执行微1-1: promise1
// 执行微1-2: async1 end
// 执行微1-3: error!!!、async1 。
// 当前异步回调执行完毕才Promise.resolve('async1 success'),
// 然后注册then()中的成功的回调-微1-5
// 执行微1-4: promise2
// 执行刚刚注册的微1-5: async1 success

如果 Promise 中有 setTimeout,那就看 resolve 在不在 setTimeout 里了,如果在,因为后面 then 拿不到 value,无法继续,就得等着,如果不在,那就走 then,回来再走 setTimeout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
new Promise((resolve, reject) => {
console.log(1)
resolve()
})
.then(() => { // 微1-1
console.log(2)
new Promise((resolve, reject) => {
console.log(3)
setTimeout(() => { // 宏2
reject();
}, 5 * 1000);
resolve() // TODO 注1
})
.then(() => { // 微1-2 TODO 注2
console.log(4)
new Promise((resolve, reject) => {
console.log(5)
resolve();
})
.then(() => { // 微1-4
console.log(7)
})
.then(() => { // 微1-6
console.log(9)
})
})
.then(() => { // 微1-5 TODO 注3
console.log(8)
})
})
.then(() => { // 微1-3
console.log(6)
})
// 注意then后面想执行需要前面的状态是fufilled,否则并不会放入队列
//主要是关于resolve()后进队列的问题

// 1-1进队列,
// 1-1内部的promise 是同步代码,
// 1-2进队列,同时1-1的返回值是resolve(undivided)
// 当有返回值时,1-3进队列
// 清空微任务,1-2执行,1-4进队列,同时1-2的resolve(undivided),1-5进队列,
// 执行微任务,1-3执行,1-4执行,1-6进队列,1-5执行.
// 最后1-6进执行

// 我觉得大概就是这样,then执行一定要记住then在resolve之后才会把then回调放到微任务队列去
```</div>warning
前面说过promise.finally()也是微任务,finally可以理解为不管promise的状态是成功或失败都要执行我。但是我不接受任何结果。因此finally接受不到返回值res为undefined</div>

## 任务队列中async/await的运行机制

- async定义的是一个Promise函数和普通函数一样只要不调用就不会进入事件队列。
- async内部如果没有主动return Promise,那么async会把函数的返回值用Promise包装。
- await关键字必须出现在async函数中,await后面不是必须要跟一个异步操作,也可以是一个普通表达式。
- 遇到await关键字,await右边的语句会被立即执行然后await下面的代码进入等待状态,等待await得到结果。await后面如果不是 promise 对象, await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果。await后面如果是 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
- await后面的代码放不放到最后主要看,await 的代码是不是Promise,如果是,先不放到微任务队中,也就是等同步代码执行完,再放,也就到了微任务队列的最后了,如果不是,可以放到微任务队列中.
- `await bar()` => `Promise.resolve(bar())`, 后面的代码相当于`Promise.then(...)`
- `await new Promise`如果promise没有返回值,是不执行后面内容.</div>warning
await的真实意思是 async wait(异步等待的意思)await表达式相当于调用后面返回promise的then方法,异步(等待)获取其返回值。即 await<==>promise.then</div>
<div style="background: #E8F7FF;padding:10px;border: 1px solid #ABD2DA;border-radius:5px;margin-bottom:5px;">try/catch捕获不到异步的异常,只能捕获同步代码.</div>

# 定时器

## setTimeout
setTimeout并不一定准确.
其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,我们可以通过代码去修正 setTimeout,从而使定时器相对准确.
```javascript
let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval

function loop() {
count++
// 代码执行所消耗的时间
let offset = new Date().getTime() - (startTime + count * interval);
let diff = end - new Date().getTime()
let h = Math.floor(diff / (60 * 1000 * 60))
let hdiff = diff % (60 * 1000 * 60)
let m = Math.floor(hdiff / (60 * 1000))
let mdiff = hdiff % (60 * 1000)
let s = mdiff / (1000)
let sCeil = Math.ceil(s)
let sFloor = Math.floor(s)
// 得到下一次循环所消耗的时间
currentInterval = interval - offset
console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval)

setTimeout(loop, currentInterval)
}

setTimeout(loop, currentInterval)

setInterval

接下来我们来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。
第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。
第二,它存在执行累积的问题.

requestAnimationFrame

通过 requestAnimationFrame 来实现 setInterval.
首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function setInterval(callback, interval) {
let timer;
const now = Date.now;
let startTime = now();
let endTime = startTime;
const loop = () => {
timer = window.requestAnimationFrame(loop);
endTime = now();
if (endTime - startTime >= interval) {
startTime = endTime = now();
callback(timer);
}
};
timer = window.requestAnimationFrame(loop);
return timer;
}

let a = 0;
setInterval((timer) => {
console.log(1);
a++;
if (a === 3) cancelAnimationFrame(timer);
}, 1000);

垃圾回收机制

新生代算法

存活时间短,采用 Scavenge GC 算法.
分为两部分空间,From 和 To,总是有一个空闲.
新分配的对象 => From 空间,放满为止. => 启动 GC,检查 From 空间,
存活的,复制到 To 空间, 失活的,清除. => From 和 To 空间互换.

老生代算法

  • 经历过一次 GC 的对象进入老生代空间
  • To 空间的对象占比大小超过 25% =>老生代空间

老生代采用标记-清除, 标记-紧缩算法.

JS 异步

我们都知道 JavaScript 是单线程的,如果 JS 都是同步代码执行意味着什么呢?这样可能会造成阻塞,如果当前我们有一段代码需要执行时,如果使用同步的方式,那么就会阻塞后面的代码执行;而如果使用异步则不会阻塞,我们不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。因此在 JS 编程中,会大量使用异步来进行编程

同步

所谓的同步就是在执行某段代码时,在该代码没有得到返回结果之前,其他代码暂时是无法执行的,但是一旦执行完成拿到返回值之后,就可以执行其他代码了。换句话说,在此段代码执行完未返回结果之前,会阻塞之后的代码执行,这样的情况称为同步。

异步

所谓异步就是当某一代码执行异步过程调用发出后,这段代码不会立刻得到返回结果。而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果。异步调用发出后,不会影响阻塞后面的代码执行,这样的情形称为异步

方案

  • 回调函数
  • Promise
  • Generator
  • async/await

还有事件监听,发布订阅模式是异步操作.