[译] ES6:理解参数默认值的实现细节

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

内容简介:在这篇文章中我们会介绍另一个 ES6 的特性,带以前的默认参数值是通过以下几种可选方式手动处理的:为了避免参数未传递的情况,通常可以看到

在这篇文章中我们会介绍另一个 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],)
复制代码

但是,这与手动处理实际默认值的方式是一样不方便的,并且最初的案例让人感到疑惑。因此,为了避免这种情况,ECMAScript 会在每次函数执行时计算默认值:

function foo(x = []) {
  x.push(1);
  console.log(x);
}
 
foo(); // [1]
foo(); // [1]
foo(); // [1]
复制代码

一切都很好,很直观。接下来你会发现,如果我们不了解默认值的工作机制,ES 语义可能会让我们感到困惑。

外部作用域的遮蔽

思考下面的例子:

var x = 1;
 
function foo(x, y = x) {
  console.log(y);
}
 
foo(2); // 2,不是 1!
复制代码

正如我们 看到 的,上面的例子输出的 y2 ,不是 1 。原因是参数中的 x 与全局的 x 不是同一个 。由于执行阶段会计算默认值,在赋值 = x 发生的时候, x 已经在 内部作用域 被解析了,并且指向了 x 参数自身 。具有相同名称的参数 x 遮蔽了 全局变量,使得对来自默认值的 x 的所有访问都指向参数。

参数的 TDZ(暂时性死区)

ES6 提到了所谓的 TDZ (表示 暂时性死区 )—— 这是程序的一部分,在这个区域内变量或者参数在 初始化 (即接受一个值)之前将 无法访问

就参数而言,一个 参数不能以自身作为默认值

var x = 1;
 
function foo(x = x) { // 抛出错误!
  ...
}
复制代码

我们上面提到的赋值 = x 在参数作用域中解析 x ,遮蔽了全局 x 。 但是,参数 x 位于 TDZ 内,在初始化之前无法访问。因此,它无法初始化为自身。

注意,上面带有 y 的例子是有效的,因为 x 已经初始化(为隐式默认值 undefined )了。我们再来看一下:

function foo(x, y = x) { // 可行
  ...
}
复制代码

之所以可行,是因为 ECMAScript 中的参数是按照 从左到右的顺序 初始化的,我们已经有可供使用的 x 了。

我们提到参数已经与“内部作用域”相关联了,在 ES5 中我们可以假定是 函数体 的作用域。但是,它实际上更加复杂:它 可能 是一个函数的作用域, 或者 是一个为了 存储参数绑定 而特别创建的 中间作用域 。我们来思考一下。

特定的参数中间作用域

事实上,如果 一些 (至少有一个)参数具有默认值,ES6 会定义一个 中间作用域 用于存储参数,并且这个作用域与 函数体 的作用域 不共享 。这是与 ES5 存在主要区别的一个方面。我们用例子来证明:

var x = 1;
 
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y(); // `x` 被共用了吗?
  console.log(x); // 没有,依然是 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); // 没有,依然是 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 ,它将会捕获内部的 x ,也即 2 。但显而易见,它应该捕获的是外部的 x ,也即 1 (除非它被同名参数 遮蔽 )。

同时,我们无法在外部作用域中创建函数,这意味着我们无法从这样的函数中访问 参数 。我们可以这样做:

var x = 1;
 
function foo(y, z = function() { return x + y; }) { // 可以看到 `x` 和 `y`
  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 (也就是参数自身,且位于相同作用域中)。

同时还要注意,那些重复声明只适用于 var 和函数。用 let 或者 const 重复声明参数是不行的:

function foo(x = 5) {
  let x = 1; // 错误
  const x = 2; // 错误
}
复制代码

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:理解参数默认值的实现细节》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

51单片机应用从零开始

51单片机应用从零开始

杨欣、王玉凤、刘湘黔 / 清华大学 / 2008-1 / 39.80元

《51单片机应用与实践丛书•51单片机应用从零开始》在分析初学者认知规律的基础上,结合国内重点大学一线教师的教学经验以及借鉴国外经典教材的写作手法,对51单片机的应用基础知识进行系统而翔实的介绍。读者学习每一章之后,"实例点拨"环节除了可以巩固所学的内容外,还开辟了单片机应用的视野;再加上"器件介绍"环节,又充实了对单片机从基础到应用所需要的知识。8051单片机不仅是国内用得最多的单片机之一,同时......一起来看看 《51单片机应用从零开始》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具