函数参数默认值的作用域问题

栏目: JavaScript · 发布时间: 5年前

内容简介:本篇是对ECMAScript 6 入门函数参数默认值一章中的作用域一节的学习总结,并且寻找了一些相关问题,同时还注意到 Babel 的一个转译问题。本节内容参考:在 ECMA-262 中的

本篇是对ECMAScript 6 入门函数参数默认值一章中的作用域一节的学习总结,并且寻找了一些相关问题,同时还注意到 Babel 的一个转译问题。

参数默认值与作用域

本节内容参考:

在 ECMA-262 中的 9.2.12 FunctionDeclarationInstantiation(func, argumentsList) 章节有相关说明。

当解析一个 JS 函数执行上下文的时候,会创建一个新的 Environment Record (之后简称 ER ),并且绑定这个 ER 中每个实例化了的形参(这里的实例化应该是指在执行函数的时候,形参才能有值,有值之后代表实例化了)。同时在函数体中的每个声明也被实例化了。

  • 形参没有任何默认值的情况下,会在 与参数相同的 ER 中实例化函数体声明 。也就是说函数体内的声明将与形参在同一 ER 中实例化。

    • 函数有形参,形参会被添加到函数的作用域中,并且 形参不会被重新定义 (用 var 声明与形参同名的变量会被忽略)

      function fun(arg1, arg2) {
        var arg1; // 声明被忽略
        var arg2 = "hello"; // var arg2 声明被忽略,arg2 = "hello" 被执行
        console.log(arg1, arg2);
      }
      fun(1, 2); // 1 "hello"
      复制代码
    • ES6 的 letconst 会因为作用域内重复声明而报错

      function fun(arg) {
        let arg;
      }
      fun(); // SyntaxError: Identifier 'arg' has already been declared
      复制代码
    • 多说一种情况,如果函数内声明一个和形参同名的函数

      ES6 之前,函数的执行可以分为 3 个阶段( ES6 之后情况变得复杂,尚未了解 ):

      • 准备。包括 形参变量创建 函数体内的预解析( var 声明和函数声明提升,也就是 Hoisting) 函数声明创建
      • 装载,也就是填充数据。装载顺序为 函数参数 > 函数声明 ,而在函数声明装载时,如果函数体内有个和参数名相同的函数声明,那么这个函数就会覆盖形参
      • 执行,略
      function fun(arg) {
        console.log(arg);
        function arg() {
          //...
        }
      }
      fun(1); // [Function: arg]
      复制代码
      • 准备阶段,创建形参变量 arg ,函数体预解析,创建函数声明
      • 装载阶段,先将形参的值 1 赋值给 argarg = 1 ,函数体内存在一个函数声明 function arg(){} ,所以将函数申明赋值给 arg ,也就是 arg = function(){}

PS:上面几种情况只是通过表现和结果进行总结,并没有严格按照规范进行分析。如有不对,请不吝赐教。

  • 在执行函数时,如果函数形参存在默认值,第二个 ER 会被建立,这个作用域是针对函数体内的声明,所以【函数体内的声明】与【形参和本身的函数声明】不在同一作用域

    因此一个定义在全局环境的、带有默认参数的函数声明,在运行时共产生至少 3 个作用域,如下图:

    函数参数默认值的作用域问题
    • 形参的 ER 中的变量只能读取形参 ER 中的变量或者函数外的变量,而函数体内的变量可以读取函数体内、形参以及外部的变量

    • 函数体内可以修改 ER 里定义的形参的值,但是不能重新定义形参

      函数参数默认值的作用域问题
      • var 声明的变量显示为 Block,并不是代表它是块级作用域,而 仅仅是为了区分形参的 ER 和函数体的 ER

一个疑问

var x = 20;
function fun(x = 1) {
  debugger;
  var x = 10;
  console.log(x);
}
fun(2);
复制代码
函数参数默认值的作用域问题

按我的理解,既然形参作用域和函数体作用域不共享,那么函数体作用域(图中 Block)中使用 var 声明的变量为什么会有一个初始值,并且和形参实例化的值相同?

希望有前辈可以答疑解惑。

分析几个小例子:

  • 参数形成单独作用域

    let x = 1;
    function fun(x, y = x) {
      console.log(y);
    }
    fun(2);
    复制代码
    • 参数 y 的默认值等于变量 x
    • 调用函数 fun 时,参数形成一个单独的作用域
    • 在这个作用域中,默认值变量指向第一个参数 x ,而不是全局环境的 x
  • 有默认值的形参创建的作用域也会沿着作用域链查找变量

    function fun(y = x) {
      let x = 2;
      console.log(y);
    }
    fun(); // ReferenceError: x is not defined
    复制代码
    • 调用函数 fun 时,参数 y=x 形成一个单独的作用域
    • 在这个作用域里,没有定义 x ,所以沿着作用域链在全局寻找变量 x
    • 由于全局环境中也没有定义变量 x ,所以会报错
    • 函数调用时, 函数体内部的局部变量 x 影响不到参数默认值变量 x
  • 避免暂时性死区( TDZ

    let x = 1;
    function fun(x = x) {}
    fun(); // Uncaught ReferenceError: x is not defined
    复制代码
    x = x
    let x = x
    

如果参数的默认值是一个函数,该函数的作用域也遵守上面的规则

let foo = "outer";
function bar(func = () => foo) {
  let foo = "inner";
  console.log(func());
}
bar(); // outer
复制代码
  • 函数 bar 的参数 func 的默认值是一个匿名函数,返回值为变量 foo
  • 形参形成的单独作用域里,并没有定义变量 foo ,所以指向外层的全局变量 foo

一个 Babel 问题

本节内容参考:

在阮一峰老师的 ECMAScript 6 入门中,有这样一个例子,本身其实是对复杂的形参默认值的展示,但是发现其经过 Babel 转译后的表现与转译前不同。

  • ES6

    var x = 1;
    function foo(
      x,
      y = function() {
        x = 2;
      }
    ) {
      var x = 3;
      y();
      console.log(x);
    }
    
    foo(); // 3
    复制代码
    • 由于参数有默认值,所以函数的参数形成一个单独的作用域
    • y 的默认值是一个匿名函数,函数内的变量 x 指向同一作用域的第一个参数 x
    • 函数体内也声明了一个内部变量 x ,该变量与第一个参数 x 由于不是同一作用域,所以不是同一个变量
    • 执行 y 后,内部变量和外部变量 x 的值都没变
  • 转译成 ES5 后(Babel@7.3.0)发现与原来的结果不同了。原因是转译后 形参和函数体的作用域没有做隔离

    "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(); // 2
    复制代码
  • 基于 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 出于某些考虑并没有修改,但是从结果上看,转译的代码与原来的结果的确不一致了。

总结

其实在分析这个问题的时候,自己还是很吃力,并不能从 ECMAScript® 2015 Language Specification 中分析原因,也就是无法从根本上解释完整的运行原理。更多的是从其他人的理解中参悟。

这个问题其实在业务场景中很少出现,研究意义大于实用意义。

参考


以上所述就是小编给大家介绍的《函数参数默认值的作用域问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Designing Programmes

Designing Programmes

Karl Gerstner / Springer Verlag / 2007 / $499.00

Karl Gerstnera (TM)s work is a milestone in the history of design. One of his most important works is Designing Programmes, which is presented here in a new edition of the original 1964 publication. I......一起来看看 《Designing Programmes》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换