关于 ES6 参数默认值形成的第三作用域问题

1,217 阅读6分钟

前言

  最近系统回顾《ES6 标准入门》时,在函数的拓展一章下的作用域小节,又看到了以下代码。

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

  大约一分钟时间思考一下结果吧😁。

在这里插入图片描述

  如果你有仔细阅读文初的结论,一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域,结果想必一目了然。

foo() // 3
console.log(x) // 1

有无默认值的作用域情况

  那到底形成了一个怎样的作用域呢?

  打上断点在浏览器看下作用域情况。

在这里插入图片描述

  确实含有全局作用域GlobalLocalBlock三个作用域,LocalBlock是什么作用域暂时先不管,接着往下看。

  不指定参数默认值时的情况。

在这里插入图片描述

  仅有全局作用域Global和函数作用域Local

  暂时可以确定的是,形参指定默认值将形成第三个作用域。

ECMA 的相关规范

  还是去规范中寻找答案吧。

  在ECMA-262规范中的第 9.2.12 小节中可以看到相关说明。

9.2.12 FunctionDeclarationInstantiation(func, argumentsList)
...
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.

  大致语义就是如果函数形参不含默认值,那么参数和函数体将在同一个Environment Record中初始化。如果形参含有默认值,则将为函数体创建第二个Environment Record。可以简单将Environment Record理解为作用域。

  因此结合浏览器的调试情况可以得出,形参指定默认值后,将形成全局作用域Global、参数作用域Local和函数作用域Block。不指定默认值,仅有全局作用域Global和函数作用域Local

  另外三作用域之间是包含的关系,为全局作用域参数作用域函数作用域,进一步的,参数作用域将是函数作用域的父级。

  代码结果可大致分析为,函数foo指定了参数默认值,参数xy将形成一个参数作用域。运行y函数,执行x = 2时,由于y函数当前作用域下没有变量x,因此沿着作用域链往上查找至函数foo的参数作用域,此作用域下包含形参变量x,因此将x修改为2

  然后运行foo函数内console.log(x)时,当前作用域下含有局部变量x,因此输出3。最后全局变量x1,全局下console.log(x)将输出1

转译为 ES5 代码

  为了搞清楚ES5的实现,用babel转译以上代码。

"use strict"

var x = 1
function foo(x) {
  var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () { x = 2 }

  return function (x) {
    var x = 3
    y()
    console.log(x)
  }(x)
}
foo()
console.log(x)

  根据转译后的结果,可以明显发现,ES5利用了自执行函数,将原函数的参数与函数体隔离为了两个不同的作用域,关系上确实也是参数所在的作用域包含函数体所在的作用域,是符合ECMA的规范的。

变量的声明方式为 let 时

  既然函数包含默认值时,会形成额外的第三个作用域,即参数作用域,那么以下代码是否将是可行的呢?

function foo(x, y = 2) {
  let x
}
foo()

  按照分析参数中的x与函数体中的x位于两个不同的作用域,是可行的。

  但是运行后将会报出SyntaxError错误。

在这里插入图片描述

  解释以上错误可以根据ECMA-26214.1.2 小结中的规范。

14.1.2 Static Semantics: Early Errors
...
It is a Syntax Error if any element of the BoundNames of FormalParameters also occurs in the LexicallyDeclaredNames of FunctionBody.

  即是说参数名和函数体内的变量名相同,将会是一个SyntaxError。另外注意也是一个EarlyErrors错误,也就是说在解析阶段就会报错。

  所以只是声明函数并不执行也将报错。

function foo(x, y = 2) {
  let x
}

  也就顺带解释了以下变型报错的原因。

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

变量的声明方式为 var 时

  将代码变型为var声明。

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

  你会发现竟然能运行,并且还输出了3 1

  不是按照14.1.2的规范会在解析阶段就报错吗?

  不要慌,只能说明一个问题,那就是此规范仅针对ES6letconst等声明,对于var将没有此限制。

  但是话说回来,指定了形参默认值,参数中的x和函数体中的x位于两个不同的作用域,foo内的console.log(x)是不是应该输出undefined呢?

  啊这...,我也不知道,啊你听我狡辩。

  在浏览器打个断点看看呢。

在这里插入图片描述

  可以看到由于指定了参数默认值,形成了三个作用域。但是在函数y运行前,函数作用域中的局部变量x竟然为3了。

  啊这...

  简化以上代码,运行后将输出3

function foo(x = 3, y = 5) {
  var x
  console.log(x)
}
foo()

  然后参考规范 9.2.12 节。

NOTE vars whose names are the same as a formal parameter, initially have the same value as the corresponding initialized parameter.

  大致意思是说,对于函数内var声明的局部变量名与形参名相同时,局部变量初始值与形参值相同。

  babel转换为ES5看看。

"use strict"

function foo() {
  var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 3
  var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 5
  console.log(x)
}
foo()

  你会发现实际上var x代码行被直接丢弃了。

  所以变型中的var x代码行就可以忽略掉了,结果显然输出3 1

小结

  绕了一大堆,记住三点就可以了。

  • 函数形参指定了默认值时,将形成一个第三作用域,即参数作用域,此作用域将作为函数作用域的父级
  • 函数内let声明的局部变量名与形参名一致时,在解析阶段就会报错
  • 函数内诸如var x(未赋值)声明的局部变量名与形参名一致时,可忽略当前代码行

🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHub / GiteeGitHub Pages掘金CSDN 同步更新,欢迎关注😉~