Fork me on GitHub

笔试易错题整理

答笔试题就像是一个翻山越岭的过程,迈过了大坑又迈小坑,很多时候以为自己跨过重重险阻终于要成功时,却一不小心就入了出题者的坑。

下面是我整理的一些面试中我遇到的易错题,有的问题我也不是特别清楚,欢迎大家各抒己见。

写出下面程序的运行结果

(1)函数的执行环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var str = 'hello';

var fn1 = function () {
console.log(str); //HELLO
}

str = 'HELLO';

var fn2 = function () {
var str = 'world';
fn1();
str = 'WORLD';
}

fn2();

函数能访问到变量的作用域和函数定义时的环境有关。与函数执行环境无关(只有函数中含有this时,才需要注意函数的执行环境)。

函数fn1是在全局定义的,且定义时函数内部并没有定义变量,他能访问到的变量的作用域是全局window下的。

(2)Date时间问题

1
2
3
4
5
6
7
8
let message = "Hi";
let start = new Date();
wait = (message) => {
setTimeout(() => console.log(message), 1000);
}
wait("Hello");
while (new Date() - start < 1000) {};
console.log("Tom");

输出结果:
Date输出结果
首先,定时器是异步的,js是单线程的,所以定时器里面的内容会被添加到任务队列里。所以输出顺序应该是先输出Tom再输出Hello。

至于为什么while循环没有陷入死循环能继续执行后面的代码呢?可以在上面代码的基础上while循环之前打印console.log(new Date() - start)会发现每次的值都不确定,大多是0,但是值一定是小于1000的。

于是可以在循环里面打印出所有的值来查看,在上面的基础上修改代码如下:

1
2
3
while (new Date() - start < 1000) {
console.log(new Date() - start);
};

在浏览器看打印结果(结果只截取了部分):
Date输出结果
会发现每次获取的结果都是不确定的,唯一可以肯定的就是结果是随获取的时间顺序递增的。我想可能是跟浏览器的刷新速度有关,浏览器每隔16ms刷新一次。

(3)数组的复制及清空

1
2
3
4
5
6
7
8
9
var a = [6, 7, 8];
var b = a; //[6, 7, 8]
(function (x) {
x.length = 0; //[],把数组a清空
x.push(2, 3, 4, 5);
}(a))

console.log(a); //[2, 3, 4, 5]
console.log(b); //[2, 3, 4, 5]

数组存在栈里,复制时,复制的是引用值的地址。当一个值发生改变时,另外一个值也会发生改变。x.length = 0可以将数组清空为空数组。

(4)闭包

1
2
3
4
5
6
7
8
9
10
11
12
function func () {
var i = 0;
return function () {
console.log(i++);
}
}

var func1 = func(),
func2 = func();
func1(); //0
func1(); //1
func2(); //0

考察闭包。一个函数(func)的执行结果是返回另外一个函数(return function ()),返回出去的函数任然能访问到原来的作用域链(变量i)。

func1和func2保存的都为func的执行结果,即返回出去的函数return function,func1和func2互不影响。

func1()执行时,第一次访问到的i为函数func里面声明的变量i。第二次执行时,访问到的i为第一次func1()的执行结果,即i++。

需要注意前置++和后置++的执行顺序,前置++为先++再输出,后置++为先输出后++。这里如果改为console.log(++i);输出顺序应该是1, 2, 1

(5)类似考察闭包的还有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function f1 () {
var n = 99;
add = function () {
n += 1;
}
function f2 () {
console.log(n);
}
return f2;
}
var result = f1();
result(); //99
add();
result(); //100

函数f1()的执行结果是返回f2,f2任能访问到原来的作用域链。add执行时与执行环境无关,看原来的定义环境,能访问到i。所以第二次result()的执行结果为add()执行后的i

(6)定时器里的闭包

1
2
3
4
5
6
7
8
9
10
11
12
let i = 0;
var j = 0;
var k = 0;
for(; i < 10, j < 6; i++, j++) {
setTimeout(() => {
k = i + j;
console.log(i, j, k)
}, 0)
}
setTimeout(() => {
console.log(k)
}, 100)

!!!!当for循环和定时器(异步操作)联系在一起时,一定要考虑到闭包问题。

for循环每次的执行结果是返回一个定时器setTimeout()。当j == 6时,是for循环终止的条件,此时i == j == 6;k = i + j。访问到的i, k, j都是满足for循环终止条件的最后一次的值。

输出结果:
定时器的闭包问题

(7)关于this指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var id = 21;
var obj = {
id: 22
}

function fn1 () {
setTimeout(function () {
console.log(this.id);
})
}
fn1(); //21
fn1.call(obj); //21

function fn2 () {
setTimeout(() => {
console.log(this.id);
})
}
fn2(); //21
fn2.call(obj); //22

关于this指向,call()第一个参数用来改变this指向,后面的值为实参。

(8)全局环境下的this

1
2
3
4
5
6
7
8
9
10
11
12
function foo (num) {
console.log(num);
console.log('foo', this.count++);
}
foo.count = 0;
for (var i = 0; i < 8; i++) {
if(i > 3) {
foo(i);
}
}

console.log('win', this.count);

函数foo()执行时是在for循环里即全局window下执行的,此时this指向window。this.count => window.count,用window.count的方式访问一个全局没有声明的变量时,会把该变量当做全局window的属性挂载到全局window下,此时访问该变量的值为undefined。

++默认会有隐式转换,即把变量转换为数字类型再进行计算,此时undefined => NaN,所以打印出的this.count为NaN.

this.count一共经历的转换过程为:this.count => window.count => undefined => NaN.

执行结果:
函数执行结果

当变量在全局已经声明时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var count = 0;

function foo (num) {
console.log(num);
console.log('foo', this.count++);
}
foo.count = 0;
for (var i = 0; i < 8; i++) {
if(i > 3) {
foo(i);
}
}

console.log('win', this.count);

用window.count的方式访问一个全局已经声明的变量时,window.count => count,此时直接找到count的值进行相应计算即可。

执行结果:
函数执行结果

(9)对象的解构赋值及原型链

1
2
3
4
5
6
7
let x = {a: 3};
let obj = {
__proto__: null,
b: 2,
...x
}
console.log(obj);

输出结果如下:
解构输出结果

(10)数组的解构赋值

1
2
3
4
let [x, y, ...z] = ['a'];
console.log(x); //a
console.log(y); //undefined
console.log(z); //[]

(11)函数执行时的预编译

1
2
3
4
5
6
var b = 3;
(function () {
b = 4;
var b = 5;
})();
console.log(b); //3

在全局作用域声明b = 3;在函数里面也声明了b, 根据函数的执行顺序,声明b时,其值为undefined。当b = 4时,会在函数里面查找是否声明过变量b,如果声明过,则将函数里面b的值由undefined转换为4。

这并不是通常所说的在函数里面没有var则代表隐式声明全局作用域b。当函数里面没有声明过b时,则为隐式声明全局变量b。如下:

1
2
3
4
5
6
var b = 3;
(function () {
b = 4;
var c = 5;
})();
console.log(b); //4

(12)预编译和逻辑运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var foo = 'hello';
(function () {
var foo = foo || 'world';
console.log(foo); //world
})();

(function (fn) {
var foo = fn || 'world';
console.log(foo); //hello
})(foo);

(function () {
console.log(foo); //undefined
var foo = foo || 'world';
})();

此题与函数执行时查找变量的顺序有关。(1) || (2)找到真为止,(1)为假时,继续执行(2), 并把(2)的计算结果输出。