函数作用域你不知道的认识

112 阅读3分钟

最近在网上看到这样一个题目:

var x = 1;
function f(x, y = function() {x = 3; console.log(x)}) {
  console.log(x)
  var x = 2 
  y()
  console.log(x)
}
f()
console.log(x)

// 答案是:undefined, 3, 2, 1

当时虽然把答案作对了,但是对其中的原理其实理解得并不深入,并是抱着追求真理的态度,继续深挖。

image.png

翻译过来大概意思是:

如果函数的参数没有默认值,那么形参和函数体内的声明在同一个环境记录中,如果函数的参数有默认值,将会为函数体内的声明重新创建一条环境记录。

也就是说:

  • 如果函数参数没有默认值,则生成一个作用域。
  • 如果函数参数有默认值,但函数体没有变量声明,或者函数声明,则只生成一个作用域。
  • 如果函数参数有默认值,且函数体有变量声明, 变量名也参数名不相同,变量没有赋值,则只生成一个作用域,且忽略该变量。
  • 如果函数参数有默认值,且函数体有变量声明, 变量名与参数名相同,变量没有赋值,会生成两个作用域,一个参数作用域,一个函数体作用域。
  • 如果参数参数有默认值,且函数体有变量声明,变量有赋值,或者有函数声明,则会生成两个作用域,一个参数作用域,一个函数体作用域。

接下来,通过实际代码来验证一下上面的结论。

先来写一个简单的函数

function add(a, b) {
    return a + b
}

add(1, 2)

image.png 通过上图可以看到,add参数没有默认值时,只生成一个作用域Local:add,里面存放了a,b, this三个变量。

接下来,我们将add函数稍稍作一些调整,对函数参数b给定默认值:

function add(a, b = 2) {
    return a + b
}

add(1)

image.png 通过上图,可以看到,我们给函数add参数b赋于了默认值,但也只会生成一个作用域Local:add

接下来,我们对add函数再次进行改造,在函数体中声明一个变量。

function add(a, b = 2) {
    var x
    return a + b
}

add(1)

image.png

通过上图,可以看到,在add函数体中声明了一个变量x,但没有对其赋值,并不会在作用域Local:add中去进行声明。

接下来,再次对add函数进行改造:

function add(a, b = 2) {
   var a 
   return a + b
}

add(1)

image.png

通过上图,可以看到,当在add函数体中声明了a变量,但没有对其赋值,却会生成两个作用域,一个Local:add,一个Block:add两个作用域中都会存在一个a变量,且都会将实参传递给a变量。

接下来,再次对add函数进行改造:

function add(a, b = 2) {
    var x = 1 
    return a + b
}

add(1)

image.png

通过上图,可以看到,如果定义了变量,并对其赋了值,则会生成两个作用域,Local:add, Block:add, Local:add表示的是参数作用域,存放了a, b两个参数,而Block:add表示函数体作用域,x, this表示。

通过上面这些分析,相信已经对函数作用域有一个大概的认识了。那接下来,我们回到最开始的题目。

var x = 1; // global变量 x:1
function f(x, y = function() {x = 3; console.log(x)}) {
    // f函数使用了var声明变量,且参数赋有默认值,所以会生成两个作用域,一个函数参数作用域`Local:f`,一个函数体作用域`Block:f` 参数作用域存放了x, y两个参数变量, 函数体中存放了x, this两个变量。
  console.log(x)
  var x = 2 
  y() // 执行y函数时,会访问x, 因为y函数没有定义x,所以会向上查找,找到函数参数作用域中的x,将其修改为3,并打印
  console.log(x) // x在函数体中有定义,所以直接打印函数体作用域中的x
}
f()
console.log(x) // 打印全局的x