[译] 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! 复制代码
正如我们 看到
的,上面的例子输出的 y
是 2
,不是 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:理解参数默认值的实现细节》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- MQTT Essential 细节笔记总结(深入理解MQTT细节)
- 深度学习 SSD的理解和细节分析
- MetInfo 7.0.0 20200326 细节优化补丁,主要优化商城相关细节
- MetInfo7.0.0 20200407 细节优化补丁,修复编辑及手机端细节
- php 的小细节
- 2021 Raft 实现细节
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。