# 一、this指向
TIPS
普通函数:
- 当函数作为对象的方法调用时,
this就是该对象。 - 当函数作为纯函数调用时,严格模式下,
this是undefined,非严格模式下是全局对象,浏览器中就是window。 this不是变量,嵌套函数中的this不会从外层继承this值。
箭头函数:
- 没有它自己的
this值,箭头函数内的this值继承自外围作用域。在箭头函数中调用this时,仅仅是简单的沿着作用域链向上寻找,找到最近的一个this拿来使用。 - 箭头函数在定义之后,
this就不会发生改变了,无论用什么样的方式调用它,this都不会改变(例如使用call和apply,bind)。
const obj1 = {
name: 'obj1_name',
print() {
return () => console.log(this.name);
},
};
const obj2 = {
name: 'obj2_name',
};
obj1.print()();
obj1.print().call(obj2);
obj1.print.call(obj2)();
2
3
4
5
6
7
8
9
10
11
12
13
DETAILS
输出:
'obj1_name'
'obj1_name'
'obj2_name'
2
3
因为obj1.print()返回一个箭头函数,此时的this已经固定了就是指向print方法,而print由obj1调用,那么this指向obj1,所以后面执行输出为'obj1_name',箭头函数的this无法改变,所以不受call的影响,但是最后一个call是改变的print方法的this,指向了obj2,所以输出为'obj2_name'。如果print方法为:
print() {
return function () {
console.log(this.name);
};
},
2
3
4
5
''
'obj2_name'
''
2
3
两个空串是因为obj1.print()返回一个普通函数,直接执行时this指向window,所以第一个和第三个打印的是window.name为空串,第二个改变了this指向。
var obj = {
name: 'abc',
print: () => {
console.log(this.name);
},
};
obj.name = 'bcd';
obj.print();
obj.print.call(obj);
2
3
4
5
6
7
8
9
DETAILS
输出:
''
''
2
箭头函数执行的时候上下文是不会绑定this的(在函数被创建的时候就确定好了),所以它里面的this取决于外层的this,这里函数执行的时候外层是全局作用域,所以this指向window,window.name为空串。(和上一题不同的点在打印方法是在obj对象声明的时候就创建(就是print,this已经确定),而上一题的打印方法是在print方法执行的时候才会创建)。
var out = 25;
var inner = {
out: 20,
func() {
var out = 30;
return this.out;
},
};
console.log((inner.func, inner.func)());
console.log(inner.func());
console.log((inner.func)());
console.log((inner.func = inner.func)());
2
3
4
5
6
7
8
9
10
11
12
DETAILS
输出:
25
20
20
25
2
3
4
- 逗号操作符会返回表达式中的最后一个值,这里为
inner.func对应的函数,注意是函数本身,然后执行该函数,该函数并不是通过对象的方法调用,而是在全局环境下调用,所以this指向window,打印出来的当然是window下的out。 - 这个显然是以对象的方法调用,那么
this指向该对象。 - 加了个括号,看起来有点迷惑人,但实际上
(inner.func)和inner.func是完全相等的,所以还是作为对象的方法调用。 - 赋值表达式和逗号表达式相似,都是返回的值本身,所以也相对于在全局环境下调用函数。
const person = {
address: {
country: 'china',
city: 'hangzhou',
},
say() {
console.log(`it's ${this.name}, from ${this.address.country}`);
},
setCountry(country) {
this.address.country = country;
},
};
const p1 = Object.create(person);
const p2 = Object.create(person);
p1.name = 'Matthew';
p1.setCountry('American');
p2.name = 'Bob';
p2.setCountry('England');
p1.say();
p2.say();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DETAILS
输出:
"it's Matthew, from England"
"it's Bob, from England"
2
Object.create方法会创建一个对象,并且将该对象的__proto__属性指向传入的对象,所以p1和p2两个对象的原型对象指向了同一个对象,接着给p1添加了一个name属性,然后调用了p1的setCountry方法,p1本身是没有这个方法的,所以会沿着原型链进行查找,在它的原型上,也就是person对象上找到了这个方法,执行这个方法会给address对象的country属性设置传入的值。接着对p2的操作也是一样,然后因为原型中存在引用值会在所有实例中共享。所以p1和p2它们引用的address也是同一个对象,一个实例修改了,会反映到所有实例上,所以p2的修改会覆盖p1的修改,最终country的值为England。
var person = {
name: 'person',
sayName1: function () {
return this.name;
},
sayName2: () => {
return this.name;
},
sayName3: function () {
return function () {
return this.name;
}
},
sayName4: function () {
return () => {
return this.name;
}
}
}
console.log('--- 1 ---', person.sayName1());
var say1 = person.sayName1
console.log('--- 2 ---', say1());
console.log('--- 3 ---', person.sayName2());
var say2 = person.sayName2
console.log('--- 4 ---', say2());
console.log('--- 5 ---', person.sayName3()());
var say3 = person.sayName3
console.log('--- 6 ---', say3()());
var say5 = person.sayName3()
console.log('--- 7 ---', say5());
console.log('--- 8 ---', person.sayName4()());
var say4 = person.sayName4
console.log('--- 9 ---', say4()());
var say6 = person.sayName4()
console.log('--- 10 ---', say6());
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
DETAILS
输出:
'--- 1 ---' 'person'
'--- 2 ---' ''
'--- 3 ---' ''
'--- 4 ---' ''
'--- 5 ---' ''
'--- 6 ---' ''
'--- 7 ---' ''
'--- 8 ---' 'person'
'--- 9 ---' ''
'--- 10 ---' 'person'
2
3
4
5
6
7
8
9
10
# 二、原型链
function Person(name) {
this.name = name
}
var p2 = new Person('king');
console.log('1', p2.__proto__);
console.log('2', p2.__proto__.__proto__);
console.log('3', p2.__proto__.__proto__.__proto__);
console.log('4', p2.__proto__.__proto__.__proto__.__proto__);
console.log('5', p2.constructor);
console.log('6', p2.prototype);
console.log('7', Person.constructor);
console.log('8', Person.prototype);
console.log('9', Person.prototype.constructor);
console.log('10', Person.prototype.__proto__);
console.log('11', Person.__proto__);
console.log('12', Function.prototype.__proto__);
console.log('13', Function.__proto__);
console.log('14', Object.__proto__);
console.log('15', Object.prototype.__proto__);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DETAILS
console.log('1', p2.__proto__ === Person.prototype);
console.log('2', p2.__proto__.__proto__ === Object.prototype);
console.log('3', p2.__proto__.__proto__.__proto__ === null);
try {
console.log('4', p2.__proto__.__proto__.__proto__.__proto__);
} catch (error) {
console.log('4', true);
}
console.log('5', p2.constructor === Person);
console.log('6', p2.prototype === undefined);
console.log('7', Person.constructor === Function);
console.log('8', Person.prototype === Person.prototype); // prototype属性
console.log('9', Person.prototype.constructor === Person);
console.log('10', Person.prototype.__proto__ === Object.prototype);
console.log('11', Person.__proto__ === Function.prototype);
console.log('12', Function.prototype.__proto__ === Object.prototype);
console.log('13', Function.__proto__ === Function.prototype);
console.log('14', Object.__proto__ === Function.prototype);
console.log('15', Object.prototype.__proto__ === null);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Foo() {
Foo.a = function () {
console.log(1);
};
this.a = function () {
console.log(2);
};
}
Foo.prototype.a = function () {
console.log(3);
};
Foo.a = function () {
console.log(4);
};
Foo.a();
const obj = new Foo();
obj.a();
Foo.a();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DETAILS
依次输出:4,2,1
const F = function () {};
const foo = {};
const bar = new F();
Object.prototype.a = 'a';
Function.prototype.b = 'b';
console.log(foo.a, foo.b, bar.a, bar.b, F.a, F.b);
2
3
4
5
6
7
DETAILS
输出:a,undefined,a,undefined,a,b
new操作符返回的是一个引用值,所以只能找到Object原型链上的属性,普通对象同理,F函数既可以找到Function原型链的属性也可以找到Object原型链上的属性(万物皆继承自Object)。
const obj = {
name: 'name',
};
Object.prototype.age = 'age';
Object.defineProperties(obj, {
gender: {
enumerable: false,
value: 'male',
},
job: {
enumerable: true,
value: 'engineer',
},
})
obj[Symbol('xx')] = 'xx';
const keys1 = [], keys2 = [];
for (const key in obj) {
keys1.push(key);
if (Object.hasOwnProperty.call(obj, key)) {
keys2.push(key);
}
}
console.log('obj', obj);
console.log('Object.keys', Object.keys(obj));
console.log('Object.getOwnPropertyNames', Object.getOwnPropertyNames(obj));
console.log('Object.getOwnPropertySymbols', Object.getOwnPropertySymbols(obj));
console.log('keys1', keys1);
console.log('keys2', keys2);
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
DETAILS
- obj:
{name: 'name', job: 'engineer', Symbol(xx): 'xx'}。(gender虽然不能枚举,但是展开对象可以看到) - Object.keys:
['name', 'job']。(返回一个由给定对象自身的可枚举的字符串键属性名组成的数组) - Object.getOwnPropertyNames:
['name', 'gender', 'job']。(返回一个由给定对象中所有自有属性(包括不可枚举属性,但不包括使用symbol值作为名称的属性)组成的数组) - Object.getOwnPropertySymbols:
[Symbol(xx)]。(返回一个包含给定对象所有自有symbol属性的数组) - keys1:
['name', 'job', 'age']。(for...in迭代一个对象的所有可枚举字符串属性(除symbol以外),包括继承的可枚举属性) - keys2:
['name', 'job']。(hasOwnProperty判断对象自有属性(而不是继承来的属性)中是否具有指定的属性)
# 三、变量赋值与提升
let a = 1;
function b(a) {
a = 2;
console.log(a);
}
b(a);
console.log(a);
2
3
4
5
6
7
DETAILS
依次输出:2,1
首先基本类型数据是按值传递的,所以执行b函数时,b的参数a接收的值为1,参数a相当于函数内部的变量,当本作用域有和上层作用域同名的变量时,无法访问到上层变量,所以函数内无论怎么修改a,都不影响上层,所以函数内部打印的a是2,外面打印的仍是1。
function a(b = c, c = 1) {
console.log(b, c);
}
a();
2
3
4
DETAILS
输出:Uncaught ReferenceError: Cannot access 'c' before initialization
给函数多个参数设置默认值实际上跟按顺序定义变量一样,所以会存在暂时性死区的问题,即前面定义的变量不能引用后面还未定义的变量,而后面的可以访问前面的。如果改成如下即可正常访问:
function a(b = 1, c = b) {
console.log(b, c);
}
a(); // 1 1
2
3
4
let a = b = 10;
(function () {
let a = b = 20;
}());
console.log(a);
console.log(b);
2
3
4
5
6
DETAILS
依次输出:10,20
连等操作是从右向左执行的,相当于b = 10、let a = b,很明显b没有声明就直接赋值了,所以会隐式创建为一个全局变量,函数内的也是一样,并没有声明b,直接就对b赋值了,因为作用域链,会一层一层向上查找,找了到全局的b,所以全局的b就被修改为20了,而函数内的a因为重新声明了,所以只是局部变量,不影响全局的a,所以a还是10。
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a.x);
console.log(b.x);
2
3
4
5
DETAILS
依次输出:undefined,{ n: 2 }
参考文章:js连续赋值及js引用类型指针 (opens new window)
如果只是为了做题,那么只需要记住对于这种类型的题,从右往左依次赋值,但在赋值的同时不会改变前面的引用(其实经过解析之后指针指向的已经不是同一个引用了)
var name = 'World';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.info(`Goodbye ${name}`);
} else {
console.info(`Hello ${name}`);
}
}());
2
3
4
5
6
7
8
9
DETAILS
输出:Goodbye Jack
var声明变量时会把变量自动提升到当前作用域顶部,所以函数内的name虽然是在if分支里声明的,但是也会提升到外层,因为和全局的变量name重名,所以访问不到外层的name,最后因为已声明未赋值的变量的值都为undefined,导致if的第一个分支满足条件。
'use strict';
a = 1;
var a = 2;
console.log(window.a);
console.log(a);
2
3
4
5
6
DETAILS
输出:2,2
var声明会把变量提升到当前作用域顶部,所以a=1并不会报错,另外在全局作用域下使用var声明变量,该变量会变成window的一个属性,以上两点都和是否在严格模式下无关。
var i = 1;
function b() {
console.log(i);
}
function a() {
var i = 2;
b();
}
a();
2
3
4
5
6
7
8
9
DETAILS
输出:1
这道题考察的是作用域的问题,作用域其实就是一套变量的查找规则,每个函数在执行时都会创建一个执行上下文,其中会关联一个变量对象,也就是它的作用域,上面保存着该函数能访问的所有变量,另外上下文中的代码在执行时还会创建一个作用域链,如果某个标识符在当前作用域中没有找到,会沿着外层作用域继续查找,直到最顶端的全局作用域,因为js是词法作用域,在写代码阶段就作用域就已经确定了,换句话说,是在函数定义的时候确定的,而不是执行的时候,所以b函数是在全局作用域中定义的,虽然在a函数内调用,但是它只能访问到全局的作用域而不能访问到a函数的作用域。
console.log(a);
var a = 1;
var getNum = function () {
a = 2;
};
function getNum() {
a = 3;
}
console.log(a);
getNum();
console.log(a);
2
3
4
5
6
7
8
9
10
11
DETAILS
输出:undefined,1,2
- 首先因为
var声明的变量提升作用,所以a变量被提升到顶部,未赋值,所以第一个打印出来的是undefined。 - 接下来是函数声明和函数表达式的区别,函数声明会有提升作用,在代码执行前就把函数提升到顶部,在执行上下文上中生成函数定义,所以第二个
getNum会被最先提升到顶部。 - 然后是
var声明getNum的提升,但是因为getNum函数已经被声明了,所以就不需要再声明一个同名变量,接下来开始执行代码,执行到var getNum = fun...时,虽然声明被提前了,但是赋值操作还是留在这里。所以getNum被赋值为了一个函数,下面的函数声明直接跳过,最后,getNum函数执行前a打印出来还是1,执行后,a被修改成了2,所以最后打印出来的2。
var scope = 'global scope';
function a() {
function b() {
console.log(scope);
}
return b;
var scope = 'local scope';
}
a()();
2
3
4
5
6
7
8
9
DETAILS
输出:undefined
虽然var声明是在return语句后面无法执行,但还是会提升到a函数作用域的顶部。然后又因为作用域是在函数定义的时候确定的,与调用位置无关,所以b的上层作用域是a函数,scope在b自身的作用域里没有找到,向上查找找到了自动提升的并且未赋值的scope变量,所以打印出undefined。
// 第1题
var a = 1
function a(){}
console.log(a)
// 第2题
var b
function b(){}
console.log(b)
// 第3题
function c(){}
var c
console.log(c)
2
3
4
5
6
7
8
9
10
11
12
DETAILS
输出:1,b函数,c函数
涉及到函数声明和var声明,这两者都会发生提升,但是函数会优先提升,所以如果变量和函数同名的话,变量的提升就忽略了。
- 提升完后,执行到赋值代码,a被赋值成了1,函数因为已经声明提升了,所以跳过,最后打印a就是1。
- 和第一题类似,只是b没有赋值操作,那么执行到这两行相当于都没有操作,b当然是函数。
- 和第二题类似,只是先后顺序换了一下,但是并不影响两者的提升顺序,仍是函数优先,同名的var声明提升忽略,所以打印出b还是函数。
function Foo() {
getName = function () { console.log(1); };
return this;
}
Foo.getName = function () { console.log(2); };
Foo.prototype.getName = function () { console.log(3); };
var getName = function () { console.log(4); };
function getName() { console.log(5); }
// 请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DETAILS
输出:2,4,1,1,2,3,3
- 执行Foo函数的静态方法,打印出2。
- getName函数声明会先提升,然后getName函数表达式提升,但是因为函数声明提升在前,所以忽略函数表达式的提升,然后开始执行代码,执行到
var getName=...时,修改了getName的值,赋值成了打印4的新函数。 - 执行Foo函数,修改了全局变量getName,赋值成了打印1的函数,然后返回this,因为是在全局环境下执行,所以this指向window,因为getName已经被修改了,所以打印出1。
- 因为getName没有被重新赋值,所以再执行仍然打印出1。
- new操作符是用来调用函数的,所以
new Foo.getName()相当于new (Foo.getName)(),所以new的是Foo的静态方法getName,打印出2。 - 因为点运算符
.的优先级和new是一样高的,所以从左往右执行,相当于(new Foo()).getName(),对Foo使用new调用会返回一个新创建的对象,然后执行该对象的getName方法,该对象本身并没有该方法,所以会从Foo的原型对象上查找,找到了,所以打印出3。 - 和上题一样,点运算符
.的优先级和new一样高,另外new是用来调用函数的,所以new new Foo().getName()相当于new ((new Foo()).getName)(),括号里面的就是上一题。所以最后找到的是Foo原型上的方法,无论是直接调用,还是通过new调用,都会执行该方法,所以打印出3。
(() => {
let x, y;
try {
throw new Error();
} catch (x) {
(x = 1), (y = 2);
console.log(x);
}
console.log(x);
console.log(y);
})();
2
3
4
5
6
7
8
9
10
11
DETAILS
输出:1,undefined,2
catch里面的x是个参数,被赋值为1,但是在外面无法访问,所以分别是1和undefined。y赋值为2,但是没有声明,所以是全局变量,在块外面可以访问。
var b = 10;
(function b() {
b = 20;
console.log(b);
})();
2
3
4
5
DETAILS
输出:b函数
- 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
- 对于一个常量进行赋值,在strict模式下会报错,非strict模式下静默失败。
- IIFE中的函数是函数表达式,而不是函数声明。
所以重点是:非匿名自执行函数,函数名只读。
var b = 10;
(function b() {
b = 20;
console.log(b); // [Function b]
console.log(window.b); // 10,不是20
})();
// 严格模式
var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."
// 给window赋值
var b = 10;
(function b() {
window.b = 20;
console.log(b); // [Function b]
console.log(window.b); // 20是必然的
})();
// 使用var(const,let也一样)
var b = 10;
(function b() {
var b = 20; // IIFE内部变量
console.log(b); // 20
console.log(window.b); // 10
})();
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
var a = 10;
(function () {
console.log(a);
a = 5;
console.log(a, window.a);
var a = 20;
console.log(a, window.a);
}());
2
3
4
5
6
7
8
DETAILS
输出:
- undefined
- 5,10
- 20,10
# 四、类型转换
console.log(1 + NaN);
console.log("1" + 3);
console.log(1 + undefined);
console.log(1 + null);
console.log(1 + {});
console.log(1 + []);
console.log([] + {});
console.log({} + []);
2
3
4
5
6
7
8
DETAILS
输出:
console.log(1 + NaN); // NaN
console.log("1" + 3); // '13'
console.log(1 + undefined); // NaN
console.log(1 + null); // 1
console.log(1 + {}); // '1[object Object]'
console.log(1 + []); // 0
console.log([] + {}); // '[object Object]'
console.log({} + []); // '[object Object]'
2
3
4
5
6
7
8
在js中,加法运算的规则很简单,只会触发两种情况:
- number + number
- string + string
- 所以,在JavaScript隐式转换规则中首先会推算两个操作数是不是
number类型,如果是则直接运算得出结果。 - 如果有一个数为
string,则将另一个操作数隐式的转换为string,然后通过字符串拼接得出结果。 - 如果为布尔值这种简单的数据类型,那么将会转换为
number类型来进行运算得出结果。 - 如果操作数为对象或者是数组这种复杂的数据类型,那么调用对象的
valueOf方法转成原始值,如果没有该方法或调用后仍是非原始值,则调用toString方法。 undefined转为number为NaN,null转为number为0。
详情参考:ToNumber (opens new window)
[].toString()为'',({}).toString()为[object Object]
[] + {}输出[object Object],加不加括号都一样。{} + []输出0。({} + [])又输出[object Object]。
解释:在js中{}代表复合语句,在一些js解释器会将开头的{}看作一个代码块,而不是一个Object(在es6以前只有函数作用域与全局作用域,还没有块级作用域)而这里的{}只是空符号,不表明任何意思。这里的+[]是一个隐式转换,所以参与运算的只有+[],在这里将[]转换成了number类型,所以得出结果为0。而加了括号会被认为是一个对象。
var a = {};
var b = { key: 'b' };
var c = { key: 'c' };
a[b] = 123;
a[c] = 456;
console.log(a[b]);
2
3
4
5
6
DETAILS
输出:456
对象有两种方法设置和引用属性,obj.name和obj['name'],方括号里可以字符串、数字和变量设置是表达式等,但是最终计算出来得是一个字符串,对于上面的b和c,它们两个都是对象,所以会调用toString()方法转成字符串,对象转成字符串和数组不一样,和内容无关,结果都是[object Obejct],所以a[b] = a[c] = a['[object Object]']。
# 五、事件循环与异步
setTimeout(() => {
console.log(1);
}, 0);
new Promise((resolve) => {
console.log(2);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(3);
}).then(() => {
console.log(4);
});
console.log(5);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DETAILS
输出:2,3,5,4,1
整体代码作为一个宏任务开始执行,遇到setTimeout,相应回调会进入宏任务队列,然后是promise,promise的回调是同步代码,所以会打印出2,for循环结束后调用了resolve。所以then的回调会被放入微任务队列,然后打印出3,最后打印出5,到这里当前的执行栈就空了,那么先检查微任务队列,发现有一个任务,那么取出来放到主线程执行,打印出4,最后检查宏任务队列,把定时器的回调放入主线程执行,打印出1。
console.log('1');
setTimeout(() => {
console.log('2');
process.nextTick(() => {
console.log('3');
});
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
});
});
process.nextTick(() => {
console.log('6');
});
new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8');
});
setTimeout(() => {
console.log('9');
process.nextTick(() => {
console.log('10');
});
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12');
});
});
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
DETAILS
输出:1,7,6,8,2,4,3,5,9,11,10,12
如果是node版本比较低则会输出:1,7,6,8,2,4,9,11,3,10,5,12(先执行所有的宏任务,再执行微任务)
从目前已获得的情报中得知,事件循环机制的分界线是nodejs V11.0.0,在此之前的版本是宏任务先执行,如同上面的结果,从此之后的版本在事件循环机制上和浏览器端的Event Loop是相同的
注意:process.nextTick要在node端运行,菜鸟工具在线运行 (opens new window)
- 首先所有代码是一个宏任务,输出1是毫无疑问的,遇到第一个setTimeout,推入待执行的宏任务队列,nextTick推入微任务队列,Promise的同步代码输出7,resolve执行时把then方法推入微任务队列,然后又是一个setTimeout,推入宏任务队列,此时宏任务执行完成,检查微任务队列有两个任务,依次输出6,8,到此第一轮结束
- 拉起宏任务队列中的第一个setTimeout,同样的逻辑,首先同步代码输出2,nextTick推入微任务队列,Promise的同步代码输出4,then方法进入微任务队列,宏任务执行完检测微任务,依次输出3,5,第二轮结束
- 宏任务队列中剩余的setTimeout以同样的逻辑输出9,11,10,12,全部结束
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');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DETAILS
参考文章:async/await在chrome环境和node环境的执行结果不一致,求解? (opens new window)
高版本的Chrome和node v11+中输出:
- script start
- async1 start
- async2
- promise1
- script end
- async1 end
- promise2
- setTimeout
在Chrome72版本之前、node v11以下的版本输出:
- script start
- async1 start
- async2
- promise1
- script end
- promise2
- async1 end
- setTimeout
解析:其他的输出都是正常的事件循环输出,显而易见,难点在于async1 end的输出时机
async函数中可能会有await表达式,这会使async函数暂停执行(实际上是让出了线程,跳出async函数体),将await的结果封装成一个Promise,等待表达式中的Promise解析完成后继续执行async函数并返回解决结果。
async函数返回一个Promise对象,当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
其中,async1函数可以写成以下方式(便于理解):
async function async1() {
console.log('async1 start');
// await async2();
// console.log('async1 end');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
2
3
4
5
6
7
8
至于为什么在node V11版本以下,promise2和async1 end输出顺序刚好相反,因为旧版V8引擎在解析await时规则不一样,如上面的async1会被转化成另一种情况:
async function async1() {
console.log('async1 start');
// await async2();
// console.log('async1 end');
// new Promise((resolve) => {
// resolve(async2());
// }).then(() => {
// console.log('async1 end');
// });
// 又因为 async2() 返回的是promise,所以还需要进一步转化
new Promise((resolve) => {
// resolve(async2());
Promise.resolve().then(() => {
async2().then(resolve);
});
}).then(() => {
console.log('async1 end');
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这也就是为什么老版本的await占三个tick和产生了两个promise,导致promise2输出在前面,因为await中间有一个没有任何输出仅做转移await产生的promise的状态的微任务。
但是又在有个地方看到了说Chrome72版本的优化:
await后面不一定会创建新的微任务,取决于await后面是立即返回还是promise。await执行之后不会强制创建新的微任务,而是继续执行。
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => 3)
.then((res) => {
console.log(res);
});
2
3
4
5
6
7
8
9
DETAILS
输出:1,2
promise在.then和.catch执行后返回新的promise,新的promise会根据前面的promise的状态和返回值进行状态转移
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once');
resolve('success');
}, 1000);
});
const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});
promise.then((res) => {
console.log(res, Date.now() - start);
});
2
3
4
5
6
7
8
9
10
11
12
13
DETAILS
输出:once,success 1006,success 1006(这两个时间不一定是相同的)
promise的.then或者.catch可以被调用多次,但这里Promise构造函数只执行一次。或者说promise内部状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch都会直接拿到该值。
Promise.resolve()
.then(() => new Error('error!!!'))
.then((res) => {
console.log('then: ', res);
})
.catch((err) => {
console.log('catch: ', err);
});
2
3
4
5
6
7
8
DETAILS
输出:then: Error: error!!! at <anonymous>
.then或者.catch中return一个error对象并不会抛出错误,所以不会被后续的.catch 捕获,需要改成其中一种:
return Promise.reject(new Error('error!!!'))throw new Error('error!!!')
因为返回任意一个非promise的值都会被包裹成promise对象,即return new Error('error!!!')等价于return Promise.resolve(new Error('error!!!'))。
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log);
2
3
4
DETAILS
输出:1
.then或者.catch的参数期望是函数,传入非函数则会发生值穿透。
Promise.resolve()
.then((res) => {
throw new Error('error');
}, (e) => {
console.error('fail1: ', e);
})
.catch((e) => {
console.error('fail2: ', e);
});
2
3
4
5
6
7
8
9
DETAILS
输出:fail2: Error: error
.then可以接收两个参数,第一个是处理成功的函数,第二个是处理错误的函数。.catch是.then第二个参数的简便写法,但是它们用法上有一点需要注意:.then的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的.catch可以捕获之前的错误。
Promise.resolve()
.then(() => console.log(1))
.catch(() => console.log(2))
.then(() => console.log(3))
// -----
Promise.reject()
.then(() => console.log(1))
.catch(() => console.log(2))
.then(() => console.log(3))
2
3
4
5
6
7
8
9
DETAILS
输出:第一个输出1 3,第二个输出2 3
Promise的状态已经确定了的
var a = 0;
var b = async () => {
a += await 10;
// a = a + await 10;
console.log('2', a); // -> ?
};
b();
a++;
console.log('1', a); // -> ?
2
3
4
5
6
7
8
9
DETAILS
输出:
- 1, 1
- 2, 10
因为函数b内部遇到await之后需要异步等待,所以会先执行a++使得a为1,并输出。函数b内部打印的a为10的原因是,加法运算会先计算左边再计算右边,因此a一开始被固定为0,不会受到外界影响,如果a的赋值变成a = await 10 + a,那么a的值则会受到外界影响为11。
let a;
const b = new Promise((resolve, reject) => {
console.log('1111');
setTimeout(() => {
resolve(2222);
}, 1000);
}).then(() => {
console.log('then 1');
}).then(() => {
console.log('then 2');
}).then(() => {
console.log('then 3');
});
a = new Promise(async (resolve, reject) => {
console.log('2222', a);
await b;
console.log('33333', a);
await a;
resolve(true);
console.log('4444');
}).then(() => {
console.log('then 4');
});
console.log('script end');
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
DETAILS
输出:
- 1111
- 2222 undefined
- script end
- then 1
- then 2
- then 3
- 33333 Promise { <pending> }
a内部使用await等待a,会造成死锁,a一直处于pending状态永远无法完成。
# 六、其他
var arr = [0, 1, 2];
arr[10] = 10;
console.log(arr.filter(function (x) {
return x === undefined
}));
2
3
4
5
DETAILS
输出:[]
filter方法返回的是过滤后的结果数组,会默认跳过empty位,虽然对这些位取值为undefined,但是在数组中表现为空(没有元素所以会跳过)。(forEach、map、reduce等这些都会跳过,ES6新增的for...of方法就不会跳过)
console.log(Object.assign([1, 2, 3], [4, 5]));
DETAILS
输出:[4, 5, 3]
Object.assign方法可以用于处理数组,不过会把数组视为对象,比如这里会把目标数组视为是属性为0、1、2的对象,所以源数组的0、1属性的值覆盖了目标对象的值。
let num = 100;
const origin = {
get foo() {
return num++;
},
set foo(value) {
num += value;
},
};
const target = {};
Object.assign(target, origin);
console.log(origin.foo, target.foo);
origin.foo = 200;
target.foo = 200;
console.log(origin.foo, target.foo);
const target2 = Object.assign(Object.create(null, Object.getOwnPropertyDescriptors(origin)), origin);
console.log(origin.foo, target2.foo);
origin.foo = 200;
target2.foo = 200;
console.log(origin.foo, target2.foo);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DETAILS
输出:
- 101,100
- 302,200
- 607,608
- 1009,1010
Object.assign无法正确拷贝get和set描述符,拷贝时是直接使用get描述符把取到的值拷贝过去。Object.getOwnPropertyDescriptors可以拷贝描述符。
Object.assign执行时会执行一次get取值,num为100,然后num++为101,origin.foo就为101,origin.foo取值后num还会执行一次自增变成102。- 此时
target就是一个拥有foo属性并且值为100普通对象,正常改变属性值为200,对origin.foo赋值会执行set方法,num变成302,origin.foo取值后num还会执行一次自增变成303。 target拷贝了get和set描述符,在Object.assign对origin取值时num为303,取值后num为304,合并到Object.create的新对象时,foo属性会执行set方法,传入的值是303,所以num的值为304+303=607,输出中两次取值分别为607和608,执行完后num为609。- 对
origin和target2两次操作都会执行set方法修改num值,两次输出为1009和1010,最后num为1011。
// 输出什么,为什么
['1', '2', '3'].map(parseInt);
2
DETAILS
输出:[1, NaN, NaN]
map函数的第一个参数callback:var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])。这个callback一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引,第三个代表数组本身。
而parseInt则是用来解析字符串的,使字符串成为指定基数的整数:parseInt(string, radix)。
接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数(2-36),10不是默认值。
parseInt可以理解两个符号:+表示正数,-表示负数(从ECMAScript1开始)。它是在去掉空格后作为解析的初始步骤进行的。如果有该符号,则去掉符号继续解析,如果去掉首尾空格(符号与数字之间有空格也会返回NaN)与正负号后的第一个字符不能转换为数字,parseInt会返回NaN。
如果radix是undefined、0或未指定的,JavaScript会假定以下情况:
- 如果输入的字符串以
'0x'或'0X'(一个0,后面是小写或大写的X)开头,那么radix被假定为16,字符串的其余部分被当做十六进制数去解析。 - 如果输入的字符串以
'0'开头,radix被假定为8(八进制)或10(十进制)。具体选择哪一个radix取决于实现。ECMAScript5澄清了应该使用10(十进制),但不是所有的浏览器都支持。因此,在使用parseInt时,一定要指定一个radix。 - 如果输入的字符串以任何其他值开头,
radix是10(十进制)。
parseInt('1', 0);
// radix为0,并且第一个参数不以0或者0x开头,则按照10进制处理
parseInt('2', 1);
// 基数为1(1进制)不在2-36中,返回NaN
parseInt('3', 2);
// 基数为2(2进制)表示的数中只有0、1是有效数字,所以无法解析,返回NaN
2
3
4
5
6
7
8
const obj = {
2: 3,
3: 4,
length: 2,
splice: Array.prototype.splice,
push: Array.prototype.push,
};
obj.push(1);
obj.push(2);
console.log(obj);
2
3
4
5
6
7
8
9
10
DETAILS
输出:{ 2: 1, 3: 2, length: 4, splice: function, push: function }
- 第一次
push,obj的push方法设置obj[obj.length] = 1; obj.length += 1。 - 第二次
push,obj的push方法设置obj[obj.length] = 2; obj.length += 1。 - 在有些chrome版本上会打印有两个空位长度为4的数组,新版本chrome打印上述对象值。
<template>
<div>
<div>
{{ person.name }}
<ul>
<li
v-for="item in arr"
:key="item"
>
{{ item }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'Temp',
data() {
return {
person: {
name: '张三',
age: 18,
},
arr: ['甲'],
};
},
mounted() {
setTimeout(() => {
this.person.age = 20;
this.arr[0] = '乙';
}, 1000);
// 请说出1s后组件的渲染结果
setTimeout(() => {
this.person.name = '李四';
this.arr[0] = '丙';
}, 2000);
// 请说出2s后组件的渲染结果
},
};
</script>
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
DETAILS
- 1s后组件结果:张三 甲
- 2s后组件结果:李四 丙
Vue 不能检测以下数组的变动: 1.当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue 2.当你修改数组的长度时,例如:vm.items.length = newLength
但是其他的响应式数据触发了更新,会连带其他非响应式的数据更新,重新渲染页面
每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据property记录为依赖。之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。
<template>
<div ref="app">
{{ name }}
</div>
</template>
<script>
export default {
name: 'Temp',
data() {
return {
name: 'a',
};
},
beforeCreate() {
console.log(this.name);
},
created() {
console.log(this.name);
},
mounted() {
console.log(this.name);
this.name = 'b';
console.log(this.name);
console.log(this.$refs.app.innerText);
this.$nextTick(() => {
console.log(this.name);
console.log(this.$refs.app.innerText);
});
},
};
</script>
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
DETAILS
输出:undefined,a,a,b,a,b,b
beforeCreate是实例初始化后,但是在数据侦听和事件/侦听器的配置之前,所以data里面还没有数据created开始,可以访问数据,方法,计算属性等- Vue在更新DOM时是异步执行的,所以必须在下一个时间循环中才能访问到更新后的DOM
// a.js
let a = 1;
setTimeout(() => {
a = 2;
}, 2);
module.exports.a = a;
// b.js
const a = require('./a').a;
console.log(a);
setTimeout(() => {
const b = require('./a').a
console.log(b);
}, 2000);
2
3
4
5
6
7
8
9
10
11
12
13
14
DETAILS
输出:1,1
commonJS基础类型是拷贝,对象类型是引用。这里的原始值不会互相影响,如果导出的是引用值则会互相影响。
// a.js
let a = 1;
setTimeout(() => {
a = 2;
}, 2);
export {
a,
};
// b.js
import { a } from './a';
console.log(a);
setTimeout(() => {
import('./a').then((data) => {
console.log(data.a);
});
}, 3000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DETAILS
输出:1,2
ESModule导出的基础类型和对象类型都是引用,基础类型是只读引用,修改报错,引用类型互相影响。
P.S.:如果启用了ESLint,它会要求export导出的必须是静态变量,不应该是动态的。
const str = '1A,2B,3A,4B';
console.log(str.split());
console.log(str.split(undefined));
console.log(str.split(''));
console.log(''.split(''));
console.log(str.split(','));
console.log(str.split(/[A-Z]/g));
console.log(str.split(/([A-Z])/g));
console.log(str.split(2));
2
3
4
5
6
7
8
9
DETAILS
['1A,2B,3A,4B']。['1A,2B,3A,4B']。省略separator或传递undefined会导致split()返回一个只包含所调用字符串数组。['1', 'A', ',', '2', 'B', ',', '3', 'A', ',', '4', 'B']。如果separator是一个空字符串'',str被转换为一个由其UTF-16字符组成的数组,形成的字符串的两端没有空字符。[]。''.split('')是唯一一种字符串作为separator参数传入的生成空数组的方法。['1A', '2B', '3A', '4B']。正常字符串拆分。['1', ',2', ',3', ',4', '']。正常正则匹配(开不开起全局匹配不影响)。['1', 'A', ',2', 'B', ',3', 'A', ',4', 'B', '']。如果separator是包含捕获括号的正则表达式,则每次separator匹配时,捕获括号的结果(包括任何undefined的结果)将被拼接到输出数组中。['1A,', 'B,3A,4B']。任何其他值在作为separator使用之前都将被强制转换为字符串。
扩展:如果separator是一个具有Symbol.split方法的对象,该方法被调用,目标字符串和limit作为参数,this设置为该对象。它的返回值成为split的返回值。
const exp = {
pat: '_',
[Symbol.split](string) {
return string.split(this.pat);
},
}
console.log('1_2_3_4_5'.split(exp));
// ['1', '2', '3', '4', '5']
2
3
4
5
6
7
8
9
const res1 = [].reduce((prev, cur) => {
console.log(prev, cur);
return 1;
});
console.log(res1);
const res2 = [].reduce((prev, cur) => {
console.log(prev, cur);
return 1;
}, 2);
console.log(res2);
2
3
4
5
6
7
8
9
10
11
DETAILS
- 第一个
reduce执行报错,TypeError: Reduce of empty array with no initial value。 - 第二个
reduce执行完之后,res2输出初始值2。reduce内部的console都不会执行。
function f(z) {
delete z;
return z;
}
console.log(f(10));
2
3
4
5
DETAILS
输出:10。
delete运算符用于删除对象的一个属性;如果该属性的值是一个对象,并且没有更多对该对象的引用,该属性所持有的对象最终会自动释放。
删除成功返回true,否则返回false。如果属性是自身不可配置的属性且处于严格模式中,则会抛出该异常。
- 如果你试图删除的属性不存在,那么
delete将不会起任何作用,但仍会返回true。 delete只影响自身属性。如果对象的原型链上有一个与待删除属性同名的属性,那么删除属性之后,对象会使用原型链上的那个属性。- 不可配置的属性不能被移除。这意味着像
Math、Array、Object这些内置对象的属性以及使用Object.defineProperty()方法设置为不可配置的属性不能被删除。 - 删除包括函数参数内的变量永远不会奏效。delete variable会在严格模式下抛出
SyntaxError错误,非严格模式下不会有任何效果。 - 任何使用
var声明的属性不能从全局作用域或函数的作用域中删除,因为即使它们可能附加到全局对象上,它们也是不可配置的。 - 任何使用
let或const声明的属性不能够从它被声明的作用域中删除,因为它们没有附加到任何对象上。 - 当你删除一个数组元素时,数组的长度(length)不受影响。即便你删除了数组的最后一个元素也是如此。
将下列js的类改写成es5的函数
class Test {
a() { console.log(this.c) }
b = () => { console.log(this.c) }
c = '1'
d = {}
static d = 10
}
2
3
4
5
6
7
DETAILS
function Test() {
var _this = this;
this.b = () => { console.log(_this.c) }
this.c = '1'
this.d = {}
}
Test.prototype.a = function() {
console.log(this.c);
}
Test.d = 10
2
3
4
5
6
7
8
9
10
constructor内部的值是挂在类或者函数的this上(c = '1'等价于constructor() { this.c = '1' })。- 类方法是函数原型上。
- 类的静态方法在函数体本身。
try {
(async function () { a().b().c(); }());
} catch (e) {
console.log(`执行出错:${e.message}`);
}
2
3
4
5
DETAILS
- 执行a函数会报错
a is not a function。 - 在
async内部发生报错显示错误in promise,会导致Promise状态转移为rejected。 try...catch只能捕获同步代码的异常,因此不会走到catch里面。
try {
await (async function () { a().b().c(); }());
} catch (e) {
console.log(`执行出错:${e.message}`);
}
2
3
4
5
如果改成这样,则会输出“执行出错:a is not a function”。(最新版chrome已经支持全局await)