ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 11. 参数处理 在本章,对解构过程熟悉会很有帮助(该过程在上一章[第10章 解构](%E7%AC%AC10%E7%AB%A0%E8%A7%A3%E6%9E%84.md)讲解了)。 ## 11.1 概览 在 ECMASCript 6 中,参数处理得到了显著地升级。现在支持默认参数值,剩余参数( rest parameters )(可变参数 varargs )和解构。 此外,扩展运算符有助于函数/方法/构造函数调用和 数组常量 (Array literals)。 ### 11.1.1 参数默认值 通过等号(`=`)为参数指定默认参数值。 如果调用者未提供参数值,则使用默认值。 在以下示例中,`y` 的默认参数值为 0: ``` function func(x, y=0) { return [x, y]; } func(1, 2); // [1, 2] func(1); // [1, 0] func(); // [undefined, 0] ``` ### 11.1.2 剩余(rest)参数 如果您用rest运算符(`...`),该参数将通过数组接收所有剩余参数: ``` function format(pattern, ...params) { return {pattern, params}; } format(1, 2, 3); // { pattern: 1, params: [ 2, 3] } format(); // { pattern: undefined, params: [] } ``` ### 11.1.3 通过解构命名参数 如果在参数列表中使用对象模式进行解构,则可以模拟命名参数: ``` function selectEntries({ start=0, end=-1, step=1 } = {}) { // (A) // The object pattern is an abbreviation of: // { start: start=0, end: end=-1, step: step=1 } // Use the variables `start`, `end` and `step` here ··· } selectEntries({ start: 10, end: 30, step: 2 }); selectEntries({ step: 3 }); selectEntries({}); selectEntries(); ``` A行中的 `= {}`使您可以调用 不带参数的 `selectEntries()`。 ### 11.1.4 展开操作符 (`...`) 在函数和 构造函数调用 中,展开运算符将可迭代值转换为参数: ``` > Math.max(-1, 5, 11, 3) 11 > Math.max(...[-1, 5, 11, 3]) 11 > Math.max(-1, ...[-5, 11], 3) 11 ``` 在数组常量中,扩展运算符将可迭代值转换为数组元素: ``` > [1, ...[2,3], 4] [1, 2, 3, 4] ``` 我们接下来看一下具体的功能。 ## 11.2 参数解构 在 ES6 中,处理参数和通过形参解构实参的过程是一样的。也就是说,下面的函数调用: ``` function func(«FORMAL_PARAMETERS») { «CODE» } func(«ACTUAL_PARAMETERS»); ``` 大致就是: ``` { let [«FORMAL_PARAMETERS»] = [«ACTUAL_PARAMETERS»]; { «CODE» } } ``` 例子 - 下面的函数调用: ``` function logSum(x=0, y=0) { console.log(x + y); } logSum(7, 8); ``` 变成: ``` { let [x=0, y=0] = [7, 8]; { console.log(x + y); } } ``` ## 11.3 默认参数值 ECMAScript 6 允许给参数指定默认值: ``` function f(x, y=0) { return [x, y]; } ``` 省略第二个参数就会触发默认值: ``` > f(1) [1, 0] > f() [undefined, 0] ``` 注意 -`undefined`也会触发默认值: ``` > f(undefined, undefined) [undefined, 0] ``` 默认值是按需计算的,仅在实际需要时才计算: ``` > const log = console.log.bind(console); > function g(x=log('x'), y=log('y')) {return 'DONE'} > g() x y 'DONE' > g(1) y 'DONE' > g(1, 2) 'DONE' ``` ### 11.3.1 为什么`undefined`会触发默认值? 为什么`undefined`应该被当成一个缺失的参数,或者对象或数组的缺失部分?这个问题不是显而易见的。这么做的原理是它使你能够把默认值的定义转移到其它地方去。让我们看两个例子。 第一个例子(来源:[Rick Waldron’s TC39 meeting notes from 2012-07-24](https://github.com/rwaldron/tc39-notes/blob/master/es6/2012-07/july-24.md#413-destructuring-issues)),我们不必在`setOptions()`中定义一个默认值,可以将该任务委托给 `setLevel()`。 ``` function setLevel(newLevel = 0) { light.intensity = newLevel; } function setOptions(options) { // Missing prop returns undefined => use default setLevel(options.dimmerLevel); setMotorSpeed(options.speed); ··· } setOptions({speed:5}); ``` 第二个例子,`square()`不必为`x`定义一个默认值,可以将这个任务转移到`multiply()`: ``` function multiply(x=1, y=1) { return x * y; } function square(x) { return multiply(x, x); } ``` 默认值更进一步强调了`undefined`指代不存在的内容,而`null`指代空( emptiness )的语意。 ### 11.3.2 在默认值中使用其它变量 在参数的默认值中,可以使用任何变量,包括其它参数: ``` function foo(x=3, y=x) { ··· } foo(); // x=3; y=3 foo(7); // x=7; y=7 foo(7, 2); // x=7; y=2 ``` 然后,顺序很重要:**参数从左到右声明**,因此在默认值中,如果去访问一个还没声明的参数,将会抛出`ReferenceError`异常。 ``` function bar(x=y, y=4) {} bar(3); // OK bar(); // ReferenceError: y is not defined ``` ### 11.3.3 在默认值中引用“内部(inner)”变量 默认值存在于它们自己的作用域中,该作用域介于包裹函数的“外部”作用域和函数体“内部”作用域之间。因此,在默认值中不能访问函数内部的变量: ``` const x = 'outer'; function foo(a = x) { const x = 'inner'; console.log(a); // outer } ``` 在上面的例子中,如果没有外部的`x`,默认值`x`将会产生一个`ReferenceError`异常 (如果触发)。 如果默认值是闭包,该限制可能让人非常吃惊: ``` const QUX = 2 ; function bar ( callback = () => console.log(QUX) ) { // 2 const QUX = 3; callback (); // 输出 2 } bar(); ``` > 下面是笔记: 以下几点需要注意: 1. 定义了默认参数后,函数的`length`属性会减少,即默认参数不包含在 length 的计算当中: ``` function calc(x=0, y=0) { // ... console.log(x, y) } function ajax(url, async=true, dataType="JSON") { // ... console.log(url, async, dataType) } console.log(calc.length); // 0 console.log(ajax.length); // 1 ``` 2. 不能用 let 和 const 再次声明默认值,`var`可以 ``` function ajax(url="../user.action", async=true, success) { var url = ''; // 允许 let async = 3; // 报错 const success = function(){}; // 报错 } ``` > JS 不具有动态作用域,只有词法作用域 > [[译] ES6:理解参数默认值的实现细节 (juejin.cn)](https://juejin.cn/post/6844903839280136205) > [ES6 函数的扩展 - ES6 文档](http://caibaojian.com/es6/function.html) > [Babel · The compiler for next generation JavaScript (babeljs.io)](https://babeljs.io/repl/#?browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=GYVwdgxgLglg9mABAQwFbIB4AoQCcA2AvAEQB0pA9CAM4Cmupy08YxANCtQJ6SFS4haHaiAgRa1agEpEAbwBQiJYgBuyXIjz5EhRAHI9AbkQUKiQIKKgDujFytRuTdIOxAGZjpxIFKjQJipNpXcQRMQlqZ1BIWAQsKVkAX3czH3lYoA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=es2015%2Ces2016%2Ces2017%2Cstage-0%2Cstage-1%2Cstage-2%2Cstage-3%2Ces2015-loose%2Ctypescript&prettier=false&targets=&version=7.12.9&externalPlugins=) > ## 1.4 剩余参数( Rest parameters ) 将剩余操作符(`...`)放在最后一个形参的前面意味着该形参会接收到所有剩下的实参(这些实参放置在一个数组中,然后传给这个形参)。 ``` function f(x, ...y) { ··· } f('a', 'b', 'c'); // x = 'a'; y = ['b', 'c'] ``` 如果没有剩余的参数,剩余参数将会被设置为空数组: ``` f(); // x = undefined; y = [] ``` > 扩展操作符(`...`)看起来和剩余操作符一模一样,但是它用于函数调用和 数组字面量(而不是在解构模式里面) ### 11.4.1 不再需要 `arguments`! 剩余(rest)参数 可以完全取代 JavaScript 中 不令人满意的特殊变量 `arguments`。 它们具有永远是数组的优点: ``` // ECMAScript 5: arguments function logAllArguments() { for (var i=0; i < arguments.length; i++) { console.log(arguments\[i\]); } } // ECMAScript 6: rest parameter function logAllArguments(...args) { for (const arg of args) { console.log(arg); } } ``` #### 11.4.1.1 混合解构过程和访问解构值 `arguments` 的一个有趣特性就是定义普通参数的同时,你也可以拿到所有参数的一个数组: ``` function foo(x=0, y=0) { console.log('Arity: '+arguments.length); ··· } ``` 如果将 剩余参数 与 数组解构 结合使用,则可以避免在这种情况下使用 `arguments`。最终的代码相对较长,但更清晰明确: ``` function foo(... args){      let [x = 0,y = 0] = args;      console.log('Arity:'+ args.length);      ... } ``` 同样的技术也适用于命名参数(选项对象): ``` function bar(options = {}) { let { namedParam1, namedParam2 } = options; ··· if ('extra' in options) { ··· } } ``` #### 11.4.1.2 `arguments` 是可迭代的 `arguments` 在 ECMAScript 6 中是 iterable 的,这意味着您可以使用 `for-of` 和 展开操作符: ``` > (function () { return typeof arguments[Symbol.iterator] }()) 'function' > (function () { return Array.isArray([...arguments]) }()) true ``` ## 11.5 模拟命名参数 [http://es6-org.github.io/exploring-es6/#./11.5.md](http://es6-org.github.io/exploring-es6/#./11.5.md) When calling a function (or method) in a programming language, you must map the actual parameters (specified by the caller) to the formal parameters (of a function definition). There are two common ways to do so: - *Positional parameters* are mapped by position. The first actual parameter is mapped to the first formal parameter, the second actual to the second formal, and so on: `` `selectEntries``(``3``,` `20``,` `2``)` - *Named parameters* use *names* (labels) to perform the mapping. Formal parameters have labels. In a function call, these labels determine which value belongs to which formal parameter. It does not matter in which order named actual parameters appear, as long as they are labeled correctly. Simulating named parameters in JavaScript looks as follows. `` `selectEntries``({` `start``:` `3``,` `end``:` `20``,` `step``:` `2` `})` Named parameters have two main benefits: they provide descriptions for arguments in function calls and they work well for optional parameters. I’ll first explain the benefits and then show you how to simulate named parameters in JavaScript via object literals. ### 11.5.1 作为描述的命名参数 As soon as a function has more than one parameter, you might get confused about what each parameter is used for. For example, let’s say you have a function, `selectEntries()`, that returns entries from a database. Given the function call: ``` selectEntries(3, 20, 2); ``` what do these three numbers mean? Python supports named parameters, and they make it easy to figure out what is going on: ``` # Python syntax selectEntries(start=3, end=20, step=2) ``` ### 11.5.2 可选的命名参数 Optional positional parameters work well only if they are omitted at the end. Anywhere else, you have to insert placeholders such as `null` so that the remaining parameters have correct positions. With optional named parameters, that is not an issue. You can easily omit any of them. Here are some examples: ``` # Python syntax selectEntries(step=2) selectEntries(end=20, start=3) selectEntries() ``` ### 11.5.3 在 JavaScript 中模拟命名参数 JavaScript does not have native support for named parameters, unlike Python and many other languages. But there is a reasonably elegant simulation: Each actual parameter is a property in an object literal whose result is passed as a single formal parameter to the callee. When you use this technique, an invocation of `selectEntries()` looks as follows. ```selectEntries``({` `start``:` `3``,` `end``:` `20``,` `step``:` `2` `});` The function receives an object with the properties `start`, `end`, and `step`. You can omit any of them: ```selectEntries``({` `step``:` `2` `});` `selectEntries``({` `end``:` `20``,` `start``:` `3` `});` `selectEntries``();` In ECMAScript 5, you’d implement `selectEntries()` as follows: ```function` `selectEntries``(``options``)` `{` `options` `=` `options` `||` `{};` `var` `start` `=` `options``.``start` `||` `0``;` `var` `end` `=` `options``.``end` `||` `-``1``;` `var` `step` `=` `options``.``step` `||` `1``;` `···` `}` In ECMAScript 6, you can use destructuring, which looks like this: ```function` `selectEntries``({` `start``=``0``,` `end``=-``1``,` `step``=``1` `})` `{` `···` `}` If you call `selectEntries()` with zero arguments, the destructuring fails, because you can’t match an object pattern against `undefined`. That can be fixed via a default value. In the following code, the object pattern is matched against `{}` if the first parameter is missing. ``` function selectEntries({ start=0, end=-1, step=1 } = {}) { ··· } ``` You can also combine positional parameters with named parameters. It is customary for the latter to come last: ``` someFunc(posArg1, { namedArg1: 7, namedArg2: true }); ``` In principle, JavaScript engines could optimize this pattern so that no intermediate object is created, because both the object literals at the call sites and the object patterns in the function definitions are static. In JavaScript, the pattern for named parameters shown here is sometimes called *options* or *option object* (e.g., by the jQuery documentation). ## 11.6 Examples of destructuring in parameter handling ### 11.6.1 forEach() and destructuring You will probably mostly use the `for-of` loop in ECMAScript 6, but the Array method `forEach()` also profits from destructuring. Or rather, its callback does. First example: destructuring the Arrays in an Array. ```const` `items` `=` `[` `[``'foo'``,` `3``],` `[``'bar'``,` `9``]` `];` `items``.``forEach``(([``word``,` `count``])` `=>` `{` `console``.``log``(``word``+``' '``+``count``);` `});` Second example: destructuring the objects in an Array. ```const` `items` `=` `[` `{` `word``:``'foo'``,` `count``:``3` `},` `{` `word``:``'bar'``,` `count``:``9` `},` `];` `items``.``forEach``(({``word``,` `count``})` `=>` `{` `console``.``log``(``word``+``' '``+``count``);` `});` ### 11.6.2 Transforming Maps An ECMAScript 6 Map doesn’t have a method `map()` (like Arrays). Therefore, one has to: - Step 1: Convert it to an Array of `[key,value]` pairs. - Step 2: `map()` the Array. - Step 3: Convert the result back to a Map. This looks as follows. ```const` `map0` `=` `new` `Map``([` `[``1``,` `'a'``],` `[``2``,` `'b'``],` `[``3``,` `'c'``],` `]);` `const` `map1` `=` `new` `Map``(` `// step 3` `[...``map0``]` `// step 1` `.``map``(([``k``,` `v``])` `=>` `[``k``*``2``,` `'_'``+``v``])` `// step 2` `);` `// Resulting Map: {2 -> '_a', 4 -> '_b', 6 -> '_c'}` ### 11.6.3 Handling an Array returned via a Promise The tool method `Promise.all()` works as follows: - Input: an iterable of Promises. - Output: a Promise that is fulfilled with an Array as soon as the last input Promise is fulfilled. That Array contains the fulfillments of the input Promises. Destructuring helps with handling the Array that the result of `Promise.all()` is fulfilled with: ```const` `urls` `=` `[` `'http://example.com/foo.html'``,` `'http://example.com/bar.html'``,` `'http://example.com/baz.html'``,` `];` `Promise``.``all``(``urls``.``map``(``downloadUrl``))` `.``then``(([``fooStr``,` `barStr``,` `bazStr``])` `=>` `{` `···` `});` `// This function returns a Promise that is fulfilled` `// with a string (the text)` `function` `downloadUrl``(``url``)` `{` `return` `fetch``(``url``).``then``(``request` `=>` `request``.``text``());` `}` `fetch()` is a Promise-based version of `XMLHttpRequest`. It is [part of the Fetch standard](https://fetch.spec.whatwg.org/#fetch-api). ## 11.7 Coding style tips This section mentions a few tricks for descriptive parameter definitions. They are clever, but they also have downsides: they add visual clutter and can make your code harder to understand. ### 11.7.1 Optional parameters Some parameters have no default values, but can be omitted. In that case, I occasionally use the default value `undefined` to make it obvious that the parameter is optional. That is redundant, but descriptive. ```function` `foo``(``requiredParam``,` `optionalParam` `=` `undefined``)` `{` `···` `}` ### 11.7.2 Required parameters In ECMAScript 5, you have a few options for ensuring that a required parameter has been provided, which are all quite clumsy: ```function` `foo``(``mustBeProvided``)` `{` `if` `(``arguments``.``length` `<` `1``)` `{` `throw` `new` `Error``();` `}` `if` `(``!` `(``0` `in` `arguments``))` `{` `throw` `new` `Error``();` `}` `if` `(``mustBeProvided` `===` `undefined``)` `{` `throw` `new` `Error``();` `}` `···` `}` In ECMAScript 6, you can (ab)use default parameter values to achieve more concise code (credit: idea by Allen Wirfs-Brock): ```/**` ` * Called if a parameter is missing and` ` * the default value is evaluated.` ` */` `function` `mandatory``()` `{` `throw` `new` `Error``(``'Missing parameter'``);` `}` `function` `foo``(``mustBeProvided` `=` `mandatory``())` `{` `return` `mustBeProvided``;` `}` Interaction: ``` ``> foo() Error: Missing parameter > foo(123) 123 ``` ### 11.7.3 Enforcing a maximum arity This section presents three approaches to enforcing a maximum arity. The running example is a function `f` whose maximum arity is 2 – if a caller provides more than 2 parameters, an error should be thrown. The first approach is to collect all actual parameters in the formal rest parameter `args` and to check its length. ```function` `f``(...``args``)` `{` `if` `(``args``.``length` `>` `2``)` `{` `throw` `new` `Error``();` `}` `// Extract the real parameters` `let` `[``x``,` `y``]` `=` `args``;` `}` The second approach relies on unwanted actual parameters appearing in the formal rest parameter `empty`. ```function` `f``(``x``,` `y``,` `...``empty``)` `{` `if` `(``empty``.``length` `>` `0``)` `{` `throw` `new` `Error``();` `}` `}` The third approach uses a sentinel value that is gone if there is a third parameter. One caveat is that the default value `OK` is also triggered if there is a third parameter whose value is `undefined`. ```const` `OK` `=` `Symbol``();` `function` `f``(``x``,` `y``,` `arity``=``OK``)` `{` `if` `(``arity` `!==` `OK``)` `{` `throw` `new` `Error``();` `}` `}` Sadly, each one of these approaches introduces significant visual and conceptual clutter. I’m tempted to recommend checking `arguments.length`, but I also want `arguments` to go away. ```function` `f``(``x``,` `y``)` `{` `if` `(``arguments``.``length` `>` `2``)` `{` `throw` `new` `Error``();` `}` `}` ## 11.8 The spread operator (`...`) The spread operator (`...`) looks exactly like the rest operator, but is its opposite: - Rest operator: collects the remaining items of an iterable into an Array and is used for [rest parameters](ch_parameter-handling.html#sec_rest-parameters) and [destructuring](ch_destructuring.html#sec_rest-operator). - Spread operator: turns the items of an iterable into arguments of a function call or into elements of an Array. ### 11.8.1 Spreading into function and method calls `Math.max()` is a good example for demonstrating how the spread operator works in method calls. `Math.max(x1, x2, ···)` returns the argument whose value is greatest. It accepts an arbitrary number of arguments, but can’t be applied to Arrays. The spread operator fixes that: ``` ``> Math.max(-1, 5, 11, 3) 11 > Math.max(...[-1, 5, 11, 3]) 11 ``` In contrast to the rest operator, you can use the spread operator anywhere in a sequence of parts: ``` ``> Math.max(-1, ...[-1, 5, 11], 3) 11 ``` Another example is JavaScript not having a way to destructively append the elements of one Array to another one. However, Arrays do have the method `push(x1, x2, ···)`, which appends all of its arguments to its receiver. The following code shows how you can use `push()` to append the elements of `arr2` to `arr1`. ```const` `arr1` `=` `[``'a'``,` `'b'``];` `const` `arr2` `=` `[``'c'``,` `'d'``];` `arr1``.``push``(...``arr2``);` `// arr1 is now ['a', 'b', 'c', 'd']` ### 11.8.2 Spreading into constructors In addition to function and method calls, the spread operator also works for constructor calls: ```new` `Date``(...[``1912``,` `11``,` `24``])` `// Christmas Eve 1912` That is something that is [difficult to achieve in ECMAScript 5](http://speakingjs.com/es5/ch17.html#apply_constructors). ### 11.8.3 Spreading into Arrays The spread operator can also be used inside Array literals: ``` ``> [1, ...[2,3], 4] [1, 2, 3, 4] ``` That gives you a convenient way to concatenate Arrays: ```const` `x` `=` `[``'a'``,` `'b'``];` `const` `y` `=` `[``'c'``];` `const` `z` `=` `[``'d'``,` `'e'``];` `const` `arr` `=` `[...``x``,` `...``y``,` `...``z``];` `// ['a', 'b', 'c', 'd', 'e']` One advantage of the spread operator is that its operand can be any iterable value (in contrast to the Array method `concat()`, which does not support iteration). #### 11.8.3.1 Converting iterable or Array-like objects to Arrays The spread operator lets you convert any iterable value to an Array: ```const` `arr` `=` `[...``someIterableObject``];` Let’s convert a Set to an Array: ```const` `set` `=` `new` `Set``([``11``,` `-``1``,` `6``]);` `const` `arr` `=` `[...``set``];` `// [11, -1, 6]` Your own iterable objects can be converted to Arrays in the same manner: ```const` `obj` `=` `{` `*` `[``Symbol``.``iterator``]()` `{` `yield` `'a'``;` `yield` `'b'``;` `yield` `'c'``;` `}` `};` `const` `arr` `=` `[...``obj``];` `// ['a', 'b', 'c']` Note that, just like the `for-of` loop, the spread operator only works for iterable values. All built-in data structures are iterable: Arrays, Maps and Sets. All Array-like DOM data structures are also iterable. Should you ever encounter something that is not iterable, but Array-like (indexed elements plus a property `length`), you can use `Array.from()`[6](leanpub-endnotes.html#fn-parameter-handling_2) to convert it to an Array: ```const` `arrayLike` `=` `{` `'0'``:` `'a'``,` `'1'``:` `'b'``,` `'2'``:` `'c'``,` `length``:` `3` `};` `// ECMAScript 5:` `var` `arr1` `=` `[].``slice``.``call``(``arrayLike``);` `// ['a', 'b', 'c']` `// ECMAScript 6:` `const` `arr2` `=` `Array``.``from``(``arrayLike``);` `// ['a', 'b', 'c']` `// TypeError: Cannot spread non-iterable value` `const` `arr3` `=` `[...``arrayLike``];` Next: [III Modularity](pt_modularity.html)