看js代码说结果并分析
1. let和var在for循环体的区别?
for (let i=0; i<3; i++) {
setTimeout(() => {
console.log(i);
})
}
for (var j=0; j<3; j++) {
setTimeout(() => {
console.log(j);
})
}
答案:
0
1
2
3
3
3
解析:
var说明的变量没有块级作用域,在常规任务队列for循环体执行后,由于变量保存所有的setTimeout调用的i都指向迭代后同一个i, 即3;
而let声明的变量,js引擎会为每一个迭代循环声明一个新的迭代变量,每一个setTimeout引用的j都是不同变量,即0 1 2
2. let和var的区别?
if (!('a' in window)) {
var a = 1;
}
if (!('b' in window)) {
let b = 1;
}
console.log(a);
console.log(b);
答案:
undefined
Uncaught ReferenceError: b is not defined
解析:
var声明的变量没有块级作用域,并且重载变量提升,以此该题代码等价于:
var a;
if (!('a' in window)) {
a = 1;
}
此时a为全局变量,而由var声明的全局变量会成为window的属性,因此if语句块中的代码不会执行,所以console.log(a)输出undefined
而let声明的变量存在块级作用域的概念,
所以let b不会成为全局变量,即使其成为全局变量,
也不会成为window的属性,因为let声明的全局变量不会成为window的属性,
所以在console.log(b)所在的作用域(全局)中,
未声明的变量b,因此会报错:Uncaught ReferenceError: b is not defined
3. 闭包的理解
function fn() {
var i = 0;
return function (){
console.log(i++);
}
}
var f1 = fn();
var f2 = fn();
f1();
f1();
f2();
答案:
0
1
0
解析:
var 声明的变量是不存在块级作用域,但存在局部作用域,题中的变量 i 的作用域为 fn() 作用域;
每次调用fn() 都会形成一个闭包,不同闭包之间是独立的不会互相干扰;
闭包会将 i 的之保存下来,多次调用fn()返回的函数会修改i的值,最终输出:0 1 0
拓展:
什么是闭包,什么场景下需要用闭包?如何使用闭包?闭包解决什么问题?
什么作用域?
4. 命名函数表达式的理解
var foo = function bar() {
return 123;
}
console.log(typeof foo);
console.log(typeof foo());
console.log(typeof bar());
答案:
function
number
Uncaught ReferenceError: bar is not defined
解析:
- 这是一个函数表达式,并赋值到foo变量上,
typeof foo为function - foo() 作为一个函数来调用返回结果为123,
typeof 123为number - 函数表达式在MDN文档描述如下:
命名函数表达式(Named function expression) 如果你像在函数体内部引用当前函数,则需要创建一个命名函数表达式。 然后函数名称将会(且只会)作为函数体(作用域内)的本地变量
命名函数表达式的函数名只在其函数体内有效,所以调用bar()会报错。
5. this的绑定规则
var obj = {
a: 1,
foo: function () {
return this.a;
}
}
var fun = obj.foo;
console.log(obj.foo());
console.log(fun());
答案:
1
undefined
解析:
-
直接调用 obj.fun() 时,它的前面加上了对哦 obj 的引用,当函数引用由上下文对象时,
隐式绑定规则会把函数调用的this绑定到这个上下文对象。所以this.a等价于obj.a,故输出:1 -
虽然fun是
obj.foo的一个引用,但是它引用的是foot函数本身, 因此fun()实际上是一个不带任何修饰的函数调用, 此时引用默认绑定规则,即this指向全局对象。而全局对象和不存在属性 a ,故输出:undefined
6. 原型和原型链
function A(x) {
this.x = x;
}
function B(x) {
this.x = x;
}
A.prototype.x = 1;
B.prototype = new A();
var a = new A(2);
var b = new B(3);
delete b.x;
console.log(a.x);
console.log(b.x);
答案:
2
undefined
解析:
先了解什么是原型链
- js中每个函数都有一个
prototype属性,指向一个对象(成为原型对象);- 每个对象都有一个
__proto__属性,指向该对象构造函数的原型对象;- 原型对象也有
__proto__属性,于是一个对象跨域沿着__proto__逐层向上, 直到这个对象的原型对象为null,null没有原型,即为原型链的终点。
回到题目:
a.x,对象a上存在属性x为2,故输出:2b.x,对象b的属性x被delete,所以向上寻找,首先查看b.__proto__,即B.prototype,为new A(), 因为没有传入x,所以x为undefined,找到属性x,最终输出:undefined
7. 如果钩子函数显式返回一个对象,使用new创建的对象会怎么样?
const obj = {
flag: false
};
function A() {
this.flag = true;
console.log(this)
console.log(obj)
return obj;
}
const a = new A();
console.log(a.flag);
答案:
false
解析:
- 考察
new运算符 - 如果构造函数显式返回一个对象,那么该对象胡覆盖
new创建的对象 - 于是变量
a指向obj,因此a.flag等价于obj.flag
8. 事件循环、微任务、宏任务的理解
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
Promise.resolve(4).then((b) => {
console.log(b);
});
console.log(5);
答案:
1
3
5
4
2
解析:
先了解考察范围:
- 微任务、宏任务、事件循环
- 浏览器中事件循环如下:
-----➔ 宏任务
| |
| ↓
| 1. 按照顺序执行同步任务
| 2. 将微任务和宏任务分别加入队列
| |
| ↓
| / \
| | 有微任务 |-----------
| \ / |
| | |
| ↓ |
| 按照入队顺序执行所有微任务 |
| | |
| ↓ |
|------ 浏览器渲染 ←----------|
回到题目:
- 整体代码作为第一个宏任务;
- 先按顺序执行同步任务,此时输出:1 3 5 并将微任务和宏任务压入任务队列, 此处微任务为then方法,宏任务为setTimeout方法;
- 判断有无微任务,有
then执行,此时输出:4 - 浏览器渲染完成进入下一个循环,执行宏任务队列中的setTimeout,此时输出:2
9. 变量声明提升和函数声明提升
var a = 1;
function a() {}
console.log(typeof a);
答案:
number
解析:
- 考察变量提升和函数声明提升
- var和function都会提升,且function优先级高于变量,作用在变量之前,原代码等价于:
function a() {}
var a;
a = 1;
console.log(typeof a);
- 并且变量不会重复声明,于是
a = 10赋值给了原本指向函数的变量a,故输出:number
10. 形参、局部变量、引用类型、立即执行函数
var obj = {
a: 1
};
((obj) => {
console.log(obj.a);
obj.a = 3;
var obj = {
a: 2
}
console.log(obj.a);
})(obj);
console.log(obj.a);
答案:
1
2
3
解析:
- 考察形参、局部变量、引用类型、立即执行函数
- 立即执行函数内部的var声明变量会提升到函数顶部,代码等价于:
var obj = {
a: 1
};
((obj) => {
var obj;
console.log(obj.a); // obj入参是引用类型,a的值为全局变量属性
obj.a = 3; // 入参obj的a属性被修改为3
obj = { // 函数内部给局部变量obj赋值
a: 2
}
console.log(obj.a); // a的值为2
})(obj);
console.log(obj.a); // a的值已经被修改为3