ES6笔记之参数默认值(译)

  • 原文连接:http://dmitrysoshnikov.com/
  • 原文做者:Dmitry Soshnikov
  • 译者作了少许补充。这样的的文字是译者加的,能够选择忽略。
  • 做者微博:@Bosn

在这个简短的笔记中咱们聊一聊ES6的又一特性:带默认值的函数参数。正如咱们即将看到的,有些较为微妙的CASE。javascript

ES5及如下手动处理默认值

在ES6默认值特性出现前,手动处理默认值有几种方式:java

function log(message, level) {
  level = level || 'warning';
  console.log(level, ': ', message);
}

log('low memory'); // warning: low memory
log('out of memory', 'error'); // error: out of memory

为了处理参数未传递的状况,咱们常看到typeof检测:es6

if (typeof level == 'undefined') {
  level = 'warning';
}

有时也能够检查arguments.length编程

if (arguments.length == 1) {
  level = 'warning';
}

这些方法均可以很好的工做,但都过于手动且缺乏抽象。ES6规范了直接在函数头定义参数默认值的句法结构。闭包

ES6默认值:基本例子

默认参数特性在不少语言中广泛存在,其基本形式可能大多数开发者都比较熟悉:app

function log(message, level = 'warning') {
  console.log(level, ': ', message);
}

log('low memory'); // warning: low memory
log('out of memory', 'error'); // error: out of memory

参数默认值使用方便且毫无违和感。接下来让咱们深刻细节实现,扫除默认参数所带来的一些困惑。ecmascript

实现细节

如下为一些函数默认参数的ES6实现细节。编程语言

执行时求值

相对其它一些语言(如Python)在定义时一次性对默认值求值,ECMAScript在每次函数调用的执行期才会计算默认值。这种设计是为了不在复杂对象做为默认值使用时引起一些困惑。接下来请看下面Python的例子:函数

def foo(x = []):
  x.append(1)
  return x

# 咱们能够看到默认值在函数定义时只建立了一次
# 而且存于函数对象的属性中
print(foo.__defaults__) # ([],)

foo() # [1]
foo() # [1, 1]
foo() # [1, 1, 1]

print(foo.__defaults__) # ([1, 1, 1],)

为了不这种现象,Python开发者一般把默认值定义为None,而后为这个值作显式检查:优化

def foo(x = None):
  if x is None:
    x = []
  x.append(1)
  print(x)

print(foo.__defaults__) # (None,)

foo() # [1]
foo() # [1]
foo() # [1]

print(foo.__defaults__) # ([None],)

就目前,很好很直观。接下来你会发现,若不了解默认值的工做方式,ES5语义上会产生一些困惑。

外层做用域的遮蔽

来看下面的例子:

var x = 1;

function foo(x, y = x) {
  console.log(y);
}

foo(2); // 2, 不是 1!
来上例的输出结果看起来像是,但其实是,不是。缘由是参数中的与全局的不一样。因为默认值在函数调用时求值,因此当赋值时,已经在内部做用域决定了,引用的是参数自己。也就是说,参数被全局的同名变量遮蔽,因此每次默认值中访问时,实际访问到的是参数中的y121xx=xxxxxx

参数的TDZ(Temporal Dead Zone,暂存死区)

ES6提到所谓的TDZ(暂存死区),意指这样的程序区域:初始化前的变量或参数不能被访问。

考虑到对于参数,不能将本身做为默认值:

var x = 1;

function foo(x = x) { // throws!
  ...
}
赋值正如咱们上面提到的那样,会被解释为参数级做用域中的,而全局的会被遮蔽。可是,位于TDZ,在初始化前不能被访问。所以,它不能本身初始化本身。=xxxxx

注意,上面以前的例子中的y倒是合法的,由于x在以前已经初始化了(隐式的默认值undefined)。因此咱们再看下:

function foo(x, y = x) { // OK
  ...
}

这样不会出问题,由于在ECMAScript中,参数的解析顺序是从左到右,因此在对y求值时x已经可用。

咱们提到过参数是和”内部做用域”相关的,在ES5中咱们可假设这个”内部做用域”就是函数做用域。但更复杂的状况:多是函数的做用域,或者,一个只为存储参数绑定的当即做用域。让咱们继续探索。

有条件的参数当即做用域

事实上,对于一些参数(至少一个)有默认值的状况,ES6会定义一个当即做用域来存储这些参数,而且这个做用域并不会与函数做用域共享。在这方面这是ES6与ES5的一个主要区别。有点晕?没关系,看下例子你就懂。

var x = 1;

function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y(); // 局部变量`x`会被改写乎?
  console.log(x); // no, 依然是3, 不是2
}

foo();

// 并且外层的`x`也未变化
console.log(x); // 1

在这个例子中,咱们有三个做用域:全局环境、参数环境、函数环境:

: {x: 3} // 函数 -> {x: undefined, y: function() { x = 2; }} // 参数 -> {x: 1} // 全局

如今咱们应该清楚了,看成为参数的函数对象y执行时,它内部的x会被就近解析(也就是上面说的参数环境),函数做用域对其并不可见。

编译到ES5

若是咱们想把ES6代码编译到ES5,而且须要搞清楚这个当即做用域到底是什么样的,咱们能够获得像这样的东东:

// ES6
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y(); // 局部变量`x`会被改写吗?
  console.log(x); // no, 依然是3, 不是2
}

// 编译到ES5
function foo(x, y) {
  // 设置默认参数
  if (typeof y == 'undefined') {
    y = function() { x = 2; }; // 如今弄清楚了,将会更新参数中的`x`
  }

  return function() {
    var x = 3; // 这里的`x`是函数做用域的
    y();
    console.log(x);
  }.apply(this, arguments);
}

 

参数级做用域的存在缘由

设计参数级做用域的目的到底是什么?为何不能像ES5那样能够访问到函数做用域中的变量?缘由:参数默认值是函数时,其函数体内的同名变量不该该影响被捕获闭包中的同名绑定。

例:

var x = 1;

function foo(y = function() { return x; }) { // 捕获 `x`
  var x = 2;
  return y();
}

foo(); // 正确的应该是 1, 不是 2

若是咱们在函数体内建立函数y,它内部的return x中的x会捕获函数做用域下的x,也就是2。可是,很明显,参数y函数中的x应该捕获到全局的x,也就是1(除非被同名参数遮蔽)。

同时,这里不能在外部做用域下建立函数,由于这样就意味着没法访问这个函数的参数了,因此咱们应该这样作:

var x = 1;

function foo(y, z = function() { return x + y; }) { // 如今全局`x` 和参数`y`均在参数`z`函数中可见
  var x = 3;
  return z();
}

foo(1); // 2, 不是 4

若不建立参数级做用域

上面的描述的默认值工做方式,在语义上与最开始咱们手动实现默认值彻底不一样,例:

var x = 1;

function foo(x, y) {
  if (typeof y == 'undefined') {
    y = function() { x = 2; };
  }
  var x = 3;
  y(); // 局部变量`x`会被改写么?
  console.log(x); // 此次被改写了!输出2
}

foo();

// 而全局的`x`仍然未变化
console.log(x); // 1

这个事实颇有趣:若是函数无默认值,它不会建立这个当即做用域,而且与函数环境共享参数绑定,也就是像ES5那样处理。这也是为何说是『有条件的参数当即做用域』

为何会这样?为何不每次建立参数级做用域?只是为了优化?非也非也。这么作的缘由实际上是为了向后兼容ES5:上面手动模拟默认值机制的代码应该更新函数体的x(也就是参数x在相同做用域下实际是同一个变量被重复声明,一次是参数定义,一次是局部变量`x`)。

另外,须要注意到只有变量和函数容许重复声明,而用let/const重复声明参数是不容许的:

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

undefined的检测

另一个有趣的事情是:是否默认值会被应用将取决于初始值也就是传参是否为undefined(在进入上下文时被赋值)。例:

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

foo(); // undefined, 2
foo(1); // 1, 2

foo(undefined, undefined); // undefined, 2
foo(1, undefined); // 1, 2

一般状况下在一些编程语言中,带默认值参数会在必选参数的后面,可是,在JavaScript中容许下面的构造:

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

foo(1); // 1, undefined
foo(undefined, 1); // 2, 1

解构组件的默认值

另外一个默认值涉及到的地方是解构组件的默认值。解构赋值的讨论不在本文中详述,但咱们能够看一些简单的例子。对于在函数参数中使用解构的处理,与上面描述过的默认值处理相同:也就是必要时会建立两个做用域:

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

foo({}); // undefined, 5
foo({x: 1}); // 1, 5
foo({x: 1, y: 2}); // 1, 2

固然,解构的默认值更加通用,不仅在函数参数默认值中可用:

var {x, y = 5} = {x: 1};
console.log(x, y); // 1, 5

结论

但愿这个简短的记录能帮助你们理解ES6中的默认值特性的细节。须要注意的是,因为这个”第二做用域”是最近才加入到规范草稿中的,所以截至本文撰写时(2014年8月21日),没有任何引擎正确的实现了ES6默认值(它们所有只建立了一个做用域,也就是函数做用域)。默认值显然是一个有用的特性,它使得咱们的代码更加优雅和明确。

做者

相关文章
相关标签/搜索