函数参数默认值带来的作用域问题

今天看到一个神奇的题目,关于函数存在默认值会形成自己的作用域的问题

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

为什么会出现这样的答案?

在ECMAScript2015文档中9.2.12节,我们找到了这样一句话:

“When an execution context is established for evaluating an ECMAScript function a new function Environment Record is created and bindings for each formal parameter are instantiated in that Environment Record. Each declaration in the function body is also instantiated. If the function’s formal parameters do not include any default value initializers then the body declarations are instantiated in the same Environment Record as the parameters. If default value parameter initializers exist, a second Environment Record is created for the body declarations. Formal parameters and functions are initialized as part of FunctionDeclarationInstantiation. All other bindings are initialized during evaluation of the function body.”

大概意思是说,如果一个函数的参数不带有默认值,则函数体内部的声明和函数参数的声明在同一环境内运行,如果函数带有默认值,则会存在第二个环境,用于函数体内部的声明。

在阮一峰的ES6标准入门,函数拓展章节中也有类似的话:

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

根据这个参考我们来看看这个题的执行顺序。

首先,先执行var x = 1;在全局作用域创建了一个变量x = 1

然后执行f(),函数调用进入f函数,因为函数存在默认值,则此时f函数存在三个作用域,最上层是函数内层作用域,然后是函数外层的作用域以及全局作用域,因为函数体内存在var声明,则存在一个变量x,因为在默认值未赋值则,第一个console.log(x)输出的为离得最近的函数内层作用域的x值,即为undefined。

接着var x = 2,是函数体内的声明,改变的为函数内层作用域内的x = undefined的值,即函数内层作用域内x = 2,外层作用域的x 仍旧为undefined。

进入下一行,执行y(),在函数内层作用域内部查找y函数,无法找到,进入外层作用域查找,找到y函数,因为y函数产生时,存在对外部变量x的引用,则存在一个闭包{x = undefined},这个闭包的x指向f函数外层的作用域。

执行y函数,y函数的执行上下文存在三层作用域,第一层是函数外层的作用域,第二层是形成闭包的作用域,这个闭包指向f函数的函数外层作用域,第三层是全局作用域,则在执行y函数的过程中,执行x = 3 ,则在当前执行环境内无法找到x,则进入下层作用域查找,在闭包作用域中找到了x,此x指向f函数的函数外层作用域,即将闭包中的x修改为3,此时f函数的函数外层作用域内的x也会被修改为3。紧接着console.log(x),则在作用域内查找,在闭包中找到了x = 3 ,则输出3。

y函数执行完毕,y函数弹出函数调用栈,继续执行f函数,遇到console.log(x),此时x首先在当前作用域(函数内层作用域)内进行查找,找到x = 2,则输出2。

然后f函数执行完毕,执行最后一个console.log(x),此时输出的为全局作用域中的x = 1,则输出1。

既然要来探索肯定不能就这样就结束了,我们把var x = 2改成let x = 2,发现控制台报错 “Identifier 'x' has already been declared” ,这个原因应该是因为,let不能进行重复声明,这也表明,函数参数的声明会在函数内层作用域执行一次,根据这个想法,我又突发奇想,对参数x赋予默认值0,代码改变为:

var x = 1;
function f(x = 0,y = function () { x = 3; console.log(x); }) {
  console.log(x)
  var x = 2
  y()
  console.log(x)
}
f()
console.log(x)
复制代码

这个函数的最终输出为0、3、2、1,根据上面的理论,由于var x = 2,会在函数内层作用域内,创建一个x变量,而函数外层作用域也会存在一个x变量,函数创建时会进行初始化操作,会使得函数内层和函数外层两层作用域内的变量x都初始化为0,则第一个console.log(x),会输出函数内层的x = 0,即输出0。

然后var x = 2;则会将函数内层的x变为2,即x =2 ,而函数外层作用域的x仍然为x = 0 。

继续执行遇到y(),则在y函数中,x = 3,使得函数外层作用域内的x变为 0,则console.log(x),输出 3。

继续向下执行,遇到console.log(x)则此时输出的为函数内层作用域内的x,输出为2

退出f函数,执行console.log(x),此时输出的为全局环境的x,即输出1。

为了进一步进行验证,我对代码进行了进一步的修改。我们不对函数体内部进行函数声明,则代码修改为

var x = 1;
function f(x = 0,y = function () { x = 3; console.log(x); }) {
  console.log(x)
  x = 2
  y()
  console.log(x)
}
f()
console.log(x)
复制代码

这个相对于上面的,函数体内并没有进行变量声明,则我们执行这个函数,我们首先在全局作用域声明了一个变量x = 1,然后执行f()。

进入f函数,首先进行初始化,由于函数体内未进行函数声明,则不存在函数内层作用域和外层作用域之分,也就是说此时函数所在环境仅有两个作用域,即函数本身的作用域以及全局作用域,同样的y函数在初始化过程中同样会形成闭包,这个闭包指向f函数的作用域。

执行第一个console.log(x),此时在邻近的作用域内查找x值,即在函数作用域内找到初始化时定义的x = 0,则输出 0。

继续执行x = 2 ,此时修改函数作用域内的x为2。

然后执行y(),则进入y函数内执行,首先执行的是x =3,在y函数的作用域内没有x值,则向上层查找,上层作用域为形成的函数闭包,这个闭包指向的是f函数的函数作用域内的x,则修改f函数的函数作用域内的x为3,接着console.log(x),输出的为3。

f函数继续执行,遇到console.log(x),由于y函数执行过程中已经修改了f函数的函数作用域内x的值,则此时输出为3。

f函数执行完毕,执行console.log(x),此时输出的为全局作用域中的x = 1,则输出为1。

如果没有默认值的情况下又是怎么样的呢?

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

对代码又进行了一些修改,此时函数的参数不存在默认值,这个时候函数的行为又是怎么样的呢?

我们对函数打上断点继续观察,首先是第一行的var x = 1;在全局作用域内创建了一个变量x = 1;

然后执行f(function () { x = 3; console.log(x); });将y赋值了一个函数,此时观察函数的作用域,发现仍然只有两层作用域,则进入f函数后

遇到第一个console.log(x);此时x未赋值,值为undefined,则输出undefined。

然后执行var x = 2,则将函数作用域内x赋值为2

接着执行y函数,y函数的执行就很神奇了,由于y在初始化过程中并不是一个函数,则这个函数并没有产生闭包,则y函数的作用域仅有y函数本身的作用域以及全局作用域,则执行x = 3时,在函数作用域内找不到x值,则向全局作用域查找,找到x,并修改为3,同样的,接下来的console.log(x)的值也会打印的全局作用域的x,即为3。

然后执行f函数内的console.log(x),输出函数作用域内的x,即输出为2。

退出f函数,执行console.log(x),则此时打印的为全局作用域的x,此时也是3。

猜你喜欢

转载自juejin.im/post/7041507111443890206
今日推荐