守望者--AIR技术交流

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

搜索
热搜: ANE FlasCC 炼金术
查看: 263|回复: 0

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

[复制链接]
  • TA的每日心情
    擦汗
    2018-4-10 15:18
  • 签到天数: 447 天

    [LV.9]以坛为家II

    1742

    主题

    2094

    帖子

    13万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    562
    贡献
    29
    金币
    51717
    钢镚
    1422

    开源英雄守望者

    发表于 2016-8-11 11:27:49 | 显示全部楼层 |阅读模式

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

    ES5及以下手动处理默认值

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

    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检测:

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

    有时也可以检查arguments.length

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

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

    ES6默认值:基本例子

    默认参数特性在很多语言中普遍存在,其基本形式可能大多数开发者都比较熟悉:

    function log(message, level = 'warning') {
      console.log(level, ': ', message);
    }
    
    log('low memory'); // warning: low memory
    log('out of memory', 'error'); // error: out of memory

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

    实现细节

    以下为一些函数默认参数的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!

    来上例的y输出结果看起来像是1,但实际上是2,不是1。原因是参数中的x与全局的x不同。由于默认值在函数调用时求值,所以当赋值=x时,x已经在内部作用域决定了,引用的是参数x本身。也就是说,参数x被全局的同名变量遮蔽,所以每次默认值中访问x时,实际访问到的是参数中的x

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

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

    考虑到对于参数,不能将自己作为默认值:

    var x = 1;
    
    function foo(x = x) { // throws!
      ...
    }

    赋值=x正如我们上面提到的那样,x会被解释为参数级作用域中的x,而全局的x会被遮蔽。但是,x位于TDZ,在初始化前不能被访问。因此,它不能自己初始化自己。

    注意,上面之前的例子中的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默认值(它们全部只创建了一个作用域,也就是函数作用域)。默认值显然是一个有用的特性,它使得我们的代码更加优雅和明确。

    作者&鸣谢

    • 作者:Dmitry Soshnikov
    • 发布时间:2014年8月21日
    • 译者:Bosn
    • 鸣谢:感谢@紫云妃 对术语解释上的指导和帮助

    守望者AIR技术交流社区(www.airmyth.com)
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    
    关闭

    站长推荐上一条 /4 下一条

    QQ|手机版|Archiver|网站地图|小黑屋|守望者 ( 京ICP备14061876号

    GMT+8, 2019-9-15 18:21 , Processed in 0.042557 second(s), 33 queries .

    守望者AIR

    守望者AIR技术交流社区

    本站成立于 2014年12月31日

    快速回复 返回顶部 返回列表