你真的懂作用域吗?

作用域是 JS 中非常基础的概念,在 ES5 中只有全局作用域和函数作用域,在 ES6 中增加了块级作用域。可是,你真的懂作用域吗?

先不要着急回答,先看下面三道题。做不出来或做错了的话,就是不懂作用域,都做对了,作用域大概懂了三分之一吧,因为还有很多概念在里面呢!

题目一

请分别说出下面代码的执行结果:

1
2
console.log(fn)
function fn() {}
1
2
3
4
console.log(fn)
if(true) {
function fn() {}
}
1
2
3
4
console.log(fn)
if(false) {
function fn() {}
}

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是到了 ES6,是允许在块级作用域内声明函数的,它有以下两个特点:

  1. 函数名会提升到全局作用域或函数作用域的头部声明(类似于var)
  2. 函数体提升到所在的块级作用域的头部声明

我们都知道,在 ES5 中函数存在变量提升,但是需要注意函数声明(function declaration)和函数表达式(function expression)的区别:

1
2
3
4
console.log(fnd) // [Function: fnd]
console.log(fne) // undefined
function fnd() {}
var fne = function() {}

可以看到,函数声明是把函数名和方法体都提升了,而函数表达式只是提升了函数名。而在 ES6 中,如果在块级作用域中定义函数的话,只会提升函数名而不会提升方法体,其效果相当于用 var 声明的函数表达式:

1
2
3
4
if(true) {
function fn(){} // 等价于下面的函数表达式
var fn = function(){}
}

所以答案是:[Function: fn]undefinedundefined

题目二

请说出下面代码的执行结果:

1
2
3
4
5
6
7
8
9
10
11
var x = 1, y = 1, z = 1
function func(x, y, f = ()=>{x=2, y=2, z=2}) {
var x = 3
y = 3
z = 3
console.log(x, y, z)
f()
console.log(x, y, z)
}
func(4, 4)
console.log(x, y, z)

在 ES6 中,如果函数设置了参数的默认值,那么函数进行声明初始化时,参数会形成一个单独的作用域。因此上面的代码在执行到函数内部的时候,已经有了三个作用域,它们由下到上构成了作用域链:

  1. 全局作用域 x=1,y=1,z=1
  2. 函数参数作用域 x=4,y=4
  3. 函数体作用域 x=3

var x=3 改的就是函数体作用域, y=3 改的是函数参数作用域,而 z=3 则改的是全局作用域。此时第一次打印的结果是:

  • x 是函数体作用域,值为 3
  • y 是函数参数作用域,值为 3
  • z 是全局作用域,值为 3

f() 执行的时候,它会形成一个新的作用域,要注意,该函数的作用域的上级不是 func 函数体作用域,而是 func 函数参数作用域。因为在 JS 中是词法作用域,即函数在代码中声明和定义的位置,而不是函数执行时的位置。所以执行之后:

  • x=2 改的是函数参数作用域下的 x
  • y=2 改的是函数参数作用域下的 y
  • z=2 改的是全局作用域

然后再打印的时候,结果是:3、2、2。最后函数执行完出栈,由于只有 z 被改了,所以打印结果是:1、1、2。最终答案:

1
2
3
3 3 3
3 2 2
1 1 2

题目三

请说出下面代码的执行结果:

1
2
{}+0 ? console.log('yes') : console.log('no')
0+{} ? console.log('yes') : console.log('no')

此题考查了运算符的优先级、类型转换和 {} 的二义性。三元运算符的优先级是比较低的,因此要先算问号左边的部分。但是此题坑就坑在第一行的 {} 不是空对象而是块级作用域,第二行的 {} 才是空对象。因此第一行等价于:

1
2
3
{
}
+0 ? console.log('yes') : console.log('no')

这就比较明显了,打印 no。而第二行关键是计算 0+{},先尝试调用空对象的 valueOf() 方法,返回不是原始值,继续调用 toString() 方法,得到字符串 [object Object],所以 0+{} 的结果是 0[object Object],因此第二行输出 yes,最终答案是:

1
2
no
yes