11. 参数处理
目录
请支持本书:购买 (PDF, EPUB, MOBI)捐赠
(广告,请不要屏蔽。)

11. 参数处理



11.1 概述

ECMAScript 6 中的参数处理功能得到了显著提升。它现在支持参数默认值、剩余参数 (varargs) 和解构。

此外,展开运算符有助于函数/方法/构造函数调用和数组字面量。

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 剩余参数

如果在参数名称前加上剩余运算符 (...),则该参数将通过数组接收所有剩余参数

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 2012 年 7 月 24 日的 TC39 会议记录),我们不必在 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 表示空值。

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 在默认值中引用“内部”变量

默认值存在于它们自己的作用域中,该作用域位于函数周围的“外部”作用域和函数体的“内部”作用域之间。因此,您无法从默认值访问“内部”变量

const x = 'outer';
function foo(a = x) {
    const x = 'inner';
    console.log(a); // outer
}

如果在前面的示例中没有外部 x,则默认值 x 将产生 ReferenceError(如果被触发)。

如果默认值是闭包,则此限制可能是最令人惊讶的

const QUX = 2;
function bar(callback = () => QUX) { // returns 2
    const QUX = 3;
    callback();
}
bar(); // ReferenceError

11.4 剩余参数

将剩余运算符 (...) 放在最后一个形式参数前面意味着它将在数组中接收所有剩余的实际参数。

function f(x, ...y) {
    ···
}
f('a', 'b', 'c'); // x = 'a'; y = ['b', 'c']

如果没有剩余参数,则剩余参数将设置为数组

f(); // x = undefined; y = []

11.4.1 不再使用 arguments

剩余参数可以完全替换 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 中是可迭代的5,这意味着您可以使用 for-of 和展开运算符

> (function () { return typeof arguments[Symbol.iterator] }())
'function'
> (function () { return Array.isArray([...arguments]) }())
true

11.5 模拟命名参数

在编程语言中调用函数(或方法)时,必须将实际参数(由调用者指定)映射到形式参数(函数定义)。有两种常见的方法可以做到这一点

命名参数有两个主要优点:它们为函数调用中的参数提供描述,并且它们适用于可选参数。我将首先解释这些好处,然后向您展示如何通过对象字面量在 JavaScript 中模拟命名参数。

11.5.1 命名参数作为描述

一旦函数有多个参数,您可能会混淆每个参数的用途。例如,假设您有一个函数 selectEntries(),它从数据库返回条目。给定函数调用

selectEntries(3, 20, 2);

这三个数字是什么意思?Python 支持命名参数,它们可以很容易地弄清楚发生了什么

# Python syntax
selectEntries(start=3, end=20, step=2)

11.5.2 可选命名参数

可选位置参数仅在末尾省略时才有效。在其他任何地方,您都必须插入占位符,例如 null,以便其余参数具有正确的位置。

使用可选命名参数,这不是问题。您可以轻松省略其中任何一个。以下是一些示例

# Python syntax
selectEntries(step=2)
selectEntries(end=20, start=3)
selectEntries()

11.5.3 在 JavaScript 中模拟命名参数

与 Python 和许多其他语言不同,JavaScript 本身不支持命名参数。但是有一个相当优雅的模拟:每个实际参数都是对象字面量中的一个属性,其结果作为单个形式参数传递给被调用者。当您使用此技术时,selectEntries() 的调用如下所示。

selectEntries({ start: 3, end: 20, step: 2 });

该函数接收一个具有属性 startendstep 的对象。您可以省略其中任何一个

selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();

在 ECMAScript 5 中,您将按如下方式实现 selectEntries()

function selectEntries(options) {
    options = options || {};
    var start = options.start || 0;
    var end = options.end || -1;
    var step = options.step || 1;
    ···
}

在 ECMAScript 6 中,您可以使用解构,如下所示

function selectEntries({ start=0, end=-1, step=1 }) {
    ···
}

如果使用零个参数调用 selectEntries(),则解构将失败,因为您无法将对象模式与 undefined 匹配。这可以通过默认值来解决。在以下代码中,如果缺少第一个参数,则对象模式与 {} 匹配。

function selectEntries({ start=0, end=-1, step=1 } = {}) {
    ···
}

您还可以将位置参数与命名参数组合在一起。通常后者排在最后

someFunc(posArg1, { namedArg1: 7, namedArg2: true });

原则上,JavaScript 引擎可以优化此模式,以便不创建中间对象,因为调用站点上的对象字面量和函数定义中的对象模式都是静态的。

11.6 解构在参数处理中的示例

11.6.1 forEach() 和解构

您可能在 ECMAScript 6 中主要使用 for-of 循环,但数组方法 forEach() 也受益于解构。或者更确切地说,它的回调函数受益于解构。

第一个示例:解构数组中的数组。

const items = [ ['foo', 3], ['bar', 9] ];
items.forEach(([word, count]) => {
    console.log(word+' '+count);
});

第二个示例:解构数组中的对象。

const items = [
    { word:'foo', count:3 },
    { word:'bar', count:9 },
];
items.forEach(({word, count}) => {
    console.log(word+' '+count);
});

11.6.2 转换 Map

ECMAScript 6 Map 没有 map() 方法(像数组一样)。因此,必须

如下所示。

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 处理通过 Promise 返回的数组

工具方法 Promise.all() 的工作原理如下

解构有助于处理 Promise.all() 结果被兑现的数组

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()XMLHttpRequest 的基于 Promise 的版本。它是 Fetch 标准的一部分

11.7 编码风格提示

本节介绍了一些用于描述性参数定义的技巧。它们很巧妙,但也有缺点:它们增加了视觉混乱,并可能使您的代码更难理解。

11.7.1 可选参数

有些参数没有默认值,但可以省略。在这种情况下,我有时会使用默认值 undefined 来明确表示该参数是可选的。这是多余的,但具有描述性。

function foo(requiredParam, optionalParam = undefined) {
    ···
}

11.7.2 必选参数

在 ECMAScript 5 中,您有几种选择来确保提供了必选参数,但它们都相当笨拙

function foo(mustBeProvided) {
    if (arguments.length < 1) {
        throw new Error();
    }
    if (! (0 in arguments)) {
        throw new Error();
    }
    if (mustBeProvided === undefined) {
        throw new Error();
    }
    ···
}

在 ECMAScript 6 中,您可以(滥用)默认参数值来实现更简洁的代码(致谢: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;
}

交互

> foo()
Error: Missing parameter
> foo(123)
123

11.7.3 强制最大参数个数

本节介绍三种强制执行最大参数个数的方法。示例函数 f 的最大参数个数为 2,如果调用者提供了超过 2 个参数,则应抛出错误。

第一种方法是将所有实际参数收集到形式上的剩余参数 args 中,并检查其长度。

function f(...args) {
    if (args.length > 2) {
        throw new Error();
    }
    // Extract the real parameters
    let [x, y] = args;
}

第二种方法依赖于出现在形式上的剩余参数 empty 中的多余实际参数。

function f(x, y, ...empty) {
    if (empty.length > 0) {
        throw new Error();
    }
}

第三种方法使用一个哨兵值,如果存在第三个参数,则该值将消失。需要注意的是,如果第三个参数的值为 undefined,则默认值 OK 也会被触发。

const OK = Symbol();
function f(x, y, arity=OK) {
    if (arity !== OK) {
        throw new Error();
    }
}

遗憾的是,这些方法中的每一种都会引入明显的视觉和概念混乱。我倾向于建议检查 arguments.length,但我也希望 arguments 消失。

function f(x, y) {
    if (arguments.length > 2) {
        throw new Error();
    }
}

11.8 展开运算符 (...)

展开运算符 (...) 看起来与剩余运算符完全相同,但作用相反

11.8.1 展开到函数和方法调用中

Math.max() 是一个很好的例子,用于演示展开运算符如何在方法调用中工作。 Math.max(x1, x2, ···) 返回值最大的参数。它接受任意数量的参数,但不能应用于数组。展开运算符解决了这个问题

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11

与剩余运算符相比,您可以在部分序列中的任何位置使用展开运算符

> Math.max(-1, ...[5, 11], 3)
11

另一个例子是 JavaScript 没有办法将一个数组的元素破坏性地追加到另一个数组中。但是,数组确实有方法 push(x1, x2, ···),它将其所有参数追加到其接收器。以下代码显示了如何使用 push()arr2 的元素追加到 arr1

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

arr1.push(...arr2);
// arr1 is now ['a', 'b', 'c', 'd']

11.8.2 展开到构造函数中

除了函数和方法调用之外,展开运算符也适用于构造函数调用

new Date(...[1912, 11, 24]) // Christmas Eve 1912

这在 ECMAScript 5 中很难实现

11.8.3 展开到数组中

展开运算符也可以在数组字面量中使用

> [1, ...[2,3], 4]
[1, 2, 3, 4]

这为您提供了一种连接数组的便捷方法

const x = ['a', 'b'];
const y = ['c'];
const z = ['d', 'e'];

const arr = [...x, ...y, ...z]; // ['a', 'b', 'c', 'd', 'e']

展开运算符的一个优点是它的操作数可以是任何可迭代值(与不支持迭代的数组方法 concat() 相反)。

11.8.3.1 将可迭代对象或类数组对象转换为数组

展开运算符允许您将任何可迭代值转换为数组

const arr = [...someIterableObject];

让我们将一个 Set 转换为一个数组

const set = new Set([11, -1, 6]);
const arr = [...set]; // [11, -1, 6]

您自己的可迭代对象可以以相同的方式转换为数组

const obj = {
    * [Symbol.iterator]() {
        yield 'a';
        yield 'b';
        yield 'c';
    }
};
const arr = [...obj]; // ['a', 'b', 'c']

请注意,就像 for-of 循环一样,展开运算符仅适用于可迭代值。所有内置数据结构都是可迭代的:数组、映射和集合。所有类数组 DOM 数据结构也是可迭代的。

如果您遇到不可迭代但类似数组的内容(索引元素加上属性 length),则可以使用 Array.from()6 将其转换为数组

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];
下一篇:III 模块化