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应当以何为标准?
Babel的实现是错的。
这是ECMA-262中 9.2.12函数声明初始化 小节中的部分说明:
"...如果函数形参不含有默认参数,那么函数体声明和参数在同一个
Enviroment Record
中初始化。否则将为函数体声明创建第二个Enviroment Record
..."这里可以简单地将
Enviroment Record
看做独立的作用域。详细说明在这里所以当函数存在默认参数的时候,应为参数创建一个独立的作用域,然后在这个作用域之中再创建函数体的作用域,因此一个定义在全局环境的、带有默认参数的函数声明,在运行时共产生至少3个作用域,长这个样子(这里"作用域"更准确的说法是
Lexical Enviroment
,词法环境):(为什么说至少呢,因为function body内部可能还会有其他作用域,这个不是重点)
ES Spec之所以这么规定,是因为如果默认参数引用了函数作用域外部的变量,同时函数内部有同名的变量存在的话,那么实际所使用的变量应该是外部的变量,而不是函数内部的。这是符合人类的思考习惯的,你不会在一个变量定义之前就使用它。
所以如果要把含有默认参数的函数转为ES5写法的话,必须用另一个函数隔绝参数和函数体,以实现作用域的隔离。以题目的做法为例,转化后的函数应该长这样:
如果在Babel基础上修改的话,应该是这样:
Babel没有这么做可能是有别的考虑,也可能是没想到或者不愿意改,无论如何当前的实现都是错误的。
最后放一个Chrome运行这段代码的scope状态,可以看出是按spec实现的:
Local
即为上面图示中的parameters,Block
就是function body另外阮一峰说的也不全对,初始化之后作用域是不会消失的,否则运行函数
y()
的时候就没办法获取Local
里面的x
了