关于es6函数参数默认值的理解问题?

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

foo() // 3
x // 1

上面是从阮一峰老师写的es6入门一书中函数作用域倒数第二个例子,阮一峰老师的解释是这样的

上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量y,y的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。

浏览器中的运行结果也和预期一样

图片描述

而通过Babel将这个例子转es5后为

"use strict";

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

  var x = 3;
  y();
  console.log(x);
}

foo(); // 3
x; // 1

也就是下图所展示的内容

图片描述

再放到浏览器运行时,产生了下图

图片描述

函数执行结果居然不再是es6环境当中的3

根据阮一峰老师所说

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

那么这个参数作用域应该如何理解,而es6的代码通过Babel转换后放到浏览器运行结果发生改变?这是不是说是Babel转换的问题?那么es6应当以何为标准?

阅读 3.5k
2 个回答

Babel的实现是错的。

这是ECMA-262中 9.2.12函数声明初始化 小节中的部分说明:

...If the function’s formal parameters do not include any defaultvalue 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...

"...如果函数形参不含有默认参数,那么函数体声明和参数在同一个Enviroment Record中初始化。否则将为函数体声明创建第二个Enviroment Record..."

这里可以简单地将Enviroment Record看做独立的作用域。详细说明在这里

所以当函数存在默认参数的时候,应为参数创建一个独立的作用域,然后在这个作用域之中再创建函数体的作用域,因此一个定义在全局环境的、带有默认参数的函数声明,在运行时共产生至少3个作用域,长这个样子(这里"作用域"更准确的说法是Lexical Enviroment,词法环境):

clipboard.png

(为什么说至少呢,因为function body内部可能还会有其他作用域,这个不是重点)

ES Spec之所以这么规定,是因为如果默认参数引用了函数作用域外部的变量,同时函数内部有同名的变量存在的话,那么实际所使用的变量应该是外部的变量,而不是函数内部的。这是符合人类的思考习惯的,你不会在一个变量定义之前就使用它。

所以如果要把含有默认参数的函数转为ES5写法的话,必须用另一个函数隔绝参数和函数体,以实现作用域的隔离。以题目的做法为例,转化后的函数应该长这样:

function foo(x, y) {
  if (typeof y === 'undefined') y = function () { x = 2; };
  return (function() {
    var x = 3;
    y();
    console.log(x);
  }).call(this, x, y)
}

如果在Babel基础上修改的话,应该是这样:

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

  return (function () {
    var x = 3;
    y();
    console.log(x);
  }).call(this, x, y)
}

Babel没有这么做可能是有别的考虑,也可能是没想到或者不愿意改,无论如何当前的实现都是错误的。

最后放一个Chrome运行这段代码的scope状态,可以看出是按spec实现的:

clipboard.png

Local即为上面图示中的parameters,Block就是function body

另外阮一峰说的也不全对,初始化之后作用域是不会消失的,否则运行函数y()的时候就没办法获取Local里面的x

你可能要补一下js的基础变量提升。在运行时(细讲的话有两个阶段),变量都会提升到当前作用域的顶端的,所以你的代码等价于

var x;

function foo(x) {
  // 变量提升
  var y;
  // 变量提升
  var x;
  

  y = function () { x = 2};
  x = 3;
  
  // y函数改变了x的值
  y();
  console.log(x);
}

x = 1;

foo();           // 2
console.log(x);  // 1

所以你的foo()函数里一旦有var x的存在,那么这个函数内部的x都是局部的变量

然后要谴责你的一点是:下次能用markdown贴代码吗?

推荐问题
宣传栏