下午看了一章 ECMA-262 by Dmitry Soshnikov, 现在稍稍来小结下ES6中的参数默认值以及由此产生的参数中间作用域。
ES6中的参数默认值用法和其他语言都差不多,直接在参数后赋值:
1 function log(message, level = 'warning') { 2 console.log(level, ': ', message); 3 } 4 5 log('low memory'); // warning: low memory 6 log('out of memory', 'error'); // error: out of memory
不过和Python有一点不太相同的是,Python的默认值是在函数定义的时候计算的,然后作为函数的一个__defaults__参数保存下来。在函数执行时,对指向这个默认值的参数的操作也会导致默认值发生改变,
从而产生下面的问题:
1 def foo(x = []): 2 x.append(1) 3 return x 4 5 # We can see that defaults are created once, when 6 # the function is defined, and are stored as 7 # just a property of the function 8 print(foo.__defaults__) # ([],) 9 10 foo() # [1] 11 foo() # [1, 1] 12 foo() # [1, 1, 1] 13 14 # The reason for this as we said: 15 print(foo.__defaults__) # ([1, 1, 1],)
解决方法是使用特殊常量None作为默认值,在函数内再判断并初始化为真正的默认值。
1 def foo(x = None): 2 if x is None: 3 x = [] 4 x.append(1) 5 print(x) 6 7 print(foo.__defaults__) # (None,) 8 9 foo() # [1] 10 foo() # [1] 11 foo() # [1] 12 13 print(foo.__defaults__) # ([None],)
ES6的实现是在函数每次调用是都执行一次默认值的计算,保证默认值不会在之前的执行过程中被更改。
参数的TDZ(Temporal Dead Zone)
ES6中提到的TDZ,指的是程序中,变量或者参数不能被访问直到初始化完成的区域。
因此在下面一段中,参数的默认值不能设置为参数本身。
1 var x = 1; 2 3 function foo(x = x) { // throws! 4 ... 5 }
=x 中的默认值x,是在参数作用域中解析的,而不是全局的作用域。所以此时x是在TDZ中,因此不能访问,从而不能赋值给x本身作为默认值。
1 function foo(x, y = x) { // OK 2 ... 3 }
这种写法是可以的,因为在赋值给y之前,x已经被初始化为undefined了。
有条件的参数中间作用域
当至少有一个变量有默认值时,ES6定义了一个中间作用域来储存这些参数变量,同时这个作用域是不和函数主体的作用域共享的,这个是和ES5的一个主要的区别。
1 var x = 1; 2 3 function foo(x, y = function() { x = 2; }) { 4 var x = 3; 5 y(); // is `x` shared? 6 console.log(x); // no, still 3, not 2 7 } 8 9 foo(); 10 11 // and the outer `x` is also left untouched 12 console.log(x); // 1
在上面这种情况下,这里共有三个作用域:全局作用域,参数作用域,以及函数本体的作用域
1 : {x: 3} // inner 2 -> {x: undefined, y: function() { x = 2; }} // params 3 -> {x: 1} // global
此时函数y中,x的值是在自身的作用域,即参数作用域中解析的。
如果Transpiling(从一种语言编译到另一种相同抽象层次的语言)成ES5的话,这三层作用域就看的更清楚了:
1 // ES6 2 function foo(x, y = function() { x = 2; }) { 3 var x = 3; 4 y(); // is `x` shared? 5 console.log(x); // no, still 3, not 2 6 } 7 8 // Compiled to ES5 9 function foo(x, y) { 10 // Setup defaults. 11 if (typeof y == 'undefined') { 12 y = function() { x = 2; }; // now clearly see that it updates `x` from params 13 } 14 15 return function() { 16 var x = 3; // now clearly see that this `x` is from inner scope 17 y(); 18 console.log(x); 19 }.apply(this, arguments); 20 }
需要定义参数作用域的原因在于,函数类型的默认值,无论放在外部的作用域或者函数内部的作用域上执行,都会产生问题。
先看看如果是放在函数内部的作用域上执行的情况,
1 var x = 1; 2 3 function foo(y = function() { return x; }) { // capture `x` 4 var x = 2; 5 return y(); 6 } 7 8 foo(); // correctly 1, not 2
如果 function() { return x; } 放在函数内部的作用域执行,那么其中的x捕获的就会是函数内部的变量x(因为在VO的静态解析过程中,内部作用域上的x已经被定义了),但显然从理解上来说,函数中的x应该对应的是外部的x而非内部的(除非参数中还定义的一个同名的变量,然后覆盖了外部的同名变量)。由此看出,不将参数作用域和函数内部作用域共享的主要原因是:函数内部作用域中的同名变量不应该影响到参数闭包中所绑定的同名变量的值。
那如果放在外部的作用域上呢,
1 var x = 1; 2 3 function foo(y, z = function() { return x + y; }) { // can see `x` and `y` 4 var x = 3; 5 return z(); 6 } 7 8 foo(1); // 2, not 4
这是问题就会变成,function() { return x + y; }这个函数由于是在外部作用域上的,所以没有办法去访问到内部函数的参数了,即y的值访问不到,因为y不是定义在外部作用域上的。
那何谓“有条件的”中间作用域,就是当函数没有定义参数默认值的时候,是不会创建这个参数的中间作用域的,此时,参数绑定是和函数的内部作用域共享的,和在ES5的模式下的方式一致。
这种实现方式算是对ES5的下手动实现参数默认值的方式的一种兼容,使得能够在函数内部访问并修改函数参数中的变量,将他们放在相同的作用域中。
基本的内容就是这些了,这一部分的内容也并不多,但还是能看出不少语言设计时的构想以及对旧版本的兼容的考虑。自己写着写着算是把核心的内容都翻译了一遍,不过有些细节上的问题,也是非反复斟酌不能够理清其中的头绪的。
之后计划接着看 Lexical environments的部分,
之前大概看了遍但不足以深入的理解,有不少名称的定义等感觉比较绕,看来,语义的这桩事情还是要靠语义才搞的定。
So far.