ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 23.可调用值 > 原文: [http://exploringjs.com/impatient-js/ch_callables.html](http://exploringjs.com/impatient-js/ch_callables.html) > > 贡献者:[阿纯](https://github.com/rechun) ### 23.1。各种功能 JavaScript 有两类功能: * 普通函数 可以扮演几个角色: * 实际功能(在其他语言中,你只需使用术语“功能”;在 JavaScript 中,我们需要区分角色“真实功能”和可以扮演该角色的实体“普通功能”) * 方法 * 构造函数 * 专用功能 只能扮演其中一个角色。例如: * 箭头功能 只能是实际功能。 * 方法 只能是一种方法。 * 类 只能是构造函数。 接下来的部分将解释所有这些内容的含义。 ### 23.2。普通功能 以下代码显示了三种方法(大致)相同的事情:创建一个普通的函数。 ```js // Function declaration (a statement) function ordinary1(a, b, c) { // ··· } // Anonymous function expression const ordinary2 = function (a, b, c) { // ··· }; // Named function expression const ordinary3 = function myName(a, b, c) { // `myName` is only accessible in here }; ``` 正如我们在[中看到的关于变量](ch_variables-assignment.html#hoisting)的章节,函数声明被提升,而变量声明(例如通过`const`)则没有。我们将在本章后面探讨其后果。 函数声明和函数表达式的语法非常相似。上下文决定哪个是哪个。有关这种语法歧义的更多信息,请参阅[关于语法](ch_syntax.html#ambiguous-syntax)的章节。 #### 23.2.1。函数声明的一部分 让我们通过一个例子来检查函数声明的各个部分: ```js function add(x, y) { return x + y; } ``` * `add`是函数声明的 _ 名称 _。 * `add(x, y)`是函数声明的 _ 头 _。 * `x`和`y`是 _ 参数 _。 * 花括号(`{`和`}`)以及它们之间的所有东西都是函数声明的 _ 体 _。 * `return`运算符显式返回函数的值。 #### 23.2.2。普通功能的名称 函数表达式的名称只能在函数内部访问,函数可以使用它来引用自身(例如,用于自递归): ```js const func = function funcExpr() { return funcExpr }; assert.equal(func(), func); // The name `funcExpr` only exists inside the function: assert.throws(() => funcExpr, ReferenceError); ``` 相反,函数声明的名称可在当前范围内访问: ```js function funcDecl() { return funcDecl } // The name `funcDecl` exists inside the current scope assert.equal(funcDecl(), funcDecl); ``` #### 23.2.3。普通功能扮演的角色 请考虑上一节中的以下函数声明: ```js function add(x, y) { return x + y; } ``` 该函数声明创建一个名为`add`的普通函数。作为一个普通的功能,`add()`可以扮演三个角色: * 实函数:通过函数调用调用。这是大多数编程语言认为简单的 _ 函数 _。 ```js assert.equal(add(2, 1), 3); ``` * 方法:存储在属性中,通过方法调用调用。 ```js const obj = { addAsMethod: add }; assert.equal(obj.addAsMethod(2, 4), 6); ``` * 构造函数/类:通过`new`调用。 ```js const inst = new add(); assert.equal(inst instanceof add, true); ``` (顺便说一句,类的名称通常以大写字母开头。) ### 23.3。专业功能 专业功能是普通功能的专用版本。他们每个人只扮演一个角色: * 箭头函数 只能是一个真正的函数: ```js const arrow = () => { return 123 }; assert.equal(arrow(), 123); ``` * 方法 只能是一种方法: ```js const obj = { method() { return 'abc' } }; assert.equal(obj.method(), 'abc'); ``` * 类 只能是构造函数: ```js class MyClass { /* ··· */ } const inst = new MyClass(); ``` 除了更好的语法之外,每种专用功能还支持新功能,使其在工作中比普通功能更好。 * 本章稍后将解释箭头功能[。](ch_callables.html#arrow-functions) * 方法在[关于单个对象](ch_single-objects.html#methods)的章节中进行了解释。 * 在[关于原型链和类](ch_proto-chains-classes.html#classes)的章节中解释了类。 TBL。 [18](#tbl:capabilities-of-functions) 列出了普通和专用功能的功能。 Table 18: Capabilities of four kinds of functions. | | 普通功能 | 箭头功能 | 方法 | 类 | | --- | --- | --- | --- | --- | | 函数调用 | `✔` | `✔` | `✔` | `✘` | | 方法调用 | `✔` | 词汇`this` | `✔` | `✘` | | 构造函数调用 | `✔` | `✘` | `✘` | `✔` | #### 23.3.1。专业功能仍然是功能 值得注意的是,箭头函数,方法和类仍然归类为函数: ```js > (() => {}) instanceof Function true > ({ method() {} }.method) instanceof Function true > (class SomeClass {}) instanceof Function true ``` #### 23.3.2。建议:更喜欢专门的功能 通常,您应该优先使用专用函数而不是普通函数,尤其是类和方法。但是,箭头功能和普通功能之间的选择不那么明确: * 箭头函数没有`this`作为隐式参数。如果你使用真正的函数,这几乎总是你想要的,因为它避免了一个重要的`this`相关的陷阱(详见[关于单个对象](ch_single-objects.html#avoiding-pitfalls-of-this)的章节)。 * 但是,我喜欢语法上的函数声明(它产生一个普通的函数)。如果你不在其中使用`this`,它大部分相当于`const`加箭头功能: ```js function funcDecl(x, y) { return x * y; } const arrowFunc = (x, y) => { return x * y; }; ``` #### 23.3.3。箭头功能 Arrow 函数被添加到 JavaScript 中有两个原因: 1. 为创建函数提供更简洁的方法。 2. 更容易使用实际函数:不能在普通函数中引用周围范围的`this`(详见[很快](ch_callables.html#arrow-functins-lexical-this))。 ##### 23.3.3.1。箭头函数的语法 让我们回顾一下匿名函数表达式的语法: ```js const f = function (x, y, z) { return 123 }; ``` (粗略)等效箭头函数如下所示。箭头函数是表达式。 ```js const f = (x, y, z) => { return 123 }; ``` 这里,箭头函数的主体是一个块。但它也可以是一种表达方式。以下箭头功能与前一个功能完全相同。 ```js const f = (x, y, z) => 123; ``` 如果箭头函数只有一个参数且该参数是标识符(不是解构模式),那么您可以省略参数周围的括号: ```js const id = x => x; ``` 将箭头函数作为参数传递给其他函数或方法时,这很方便: ```js > [1,2,3].map(x => x+1) [ 2, 3, 4 ] ``` 最后一个例子展示了箭头功能的第一个好处 - 简洁。相反,这是相同的方法调用,但带有函数表达式: ```js [1,2,3].map(function (x) { return x+1 }); ``` ##### 23.3.3.2。箭头功能:词汇`this` 普通函数既可以是方法,也可以是实际函数。唉,这两个角色是冲突的: * 由于每个普通函数都可以是一个方法,因此它有自己的`this`。 * 自己的`this`使得无法从普通函数内部访问周围范围的`this`。这对于真正的功能来说是不方便的。 以下代码演示了一种常见的解决方法: ```js const prefixer = { prefix: '==> ', prefixStringArray(stringArray) { const that = this; // (A) return stringArray.map( function (x) { return that.prefix + x; // (B) }); }, }; assert.deepEqual( prefixer.prefixStringArray(['a', 'b']), ['==> a', '==> b']); ``` 在 B 行中,我们想要访问`.prefixStringArray()`的`this`。但我们不能,因为周围的普通函数有自己的`this` _ 阴影 _(阻止访问)方法的`this`。因此,我们将方法的`this`保存在额外变量`that`(A 行)中,并在 B 行中使用该变量。 箭头函数没有`this`作为隐式参数,它从周围环境中获取其值。也就是说,`this`的行为与任何其他变量一样。 ```js const prefixer = { prefix: '==> ', prefixStringArray(stringArray) { return stringArray.map( x => this.prefix + x); }, }; ``` 总结一下: * 在普通函数中,`this`是隐式(_ 动态 _)参数([中有关单个对象](ch_single-objects.html#methods)的章节中的详细信息)。 * 箭头函数从其周围的范围获得`this`(_ 词法 _)。 ##### 23.3.3.3。语法陷阱:从箭头函数返回一个对象字面值 如果希望箭头函数的表达式主体是对象字面值,则必须将字面值放在括号中: ```js const func1 = () => ({a: 1}); assert.deepEqual(func1(), { a: 1 }); ``` 如果不这样,JavaScript 认为,箭头函数有一个块体(不返回任何内容): ```js const func2 = () => {a: 1}; assert.deepEqual(func2(), undefined); ``` `{a: 1}`被解释为[标签`a:`](ch_control-flow.html#labels) 和表达式语句`1`的块。 这个陷阱是由[语法模糊](ch_syntax.html#ambiguous-syntax)引起的:对象字面值和代码块具有相同的语法,我们必须帮助 JavaScript 区分它们。 ### 23.4。吊装功能 函数声明是 _ 悬挂 _(内部移动到顶部): ```js assert.equal(foo(), 123); // OK function foo() { return 123; } ``` 提升允许您在声明之前调用`foo()`。 变量声明不会被挂起:在以下示例中,您只能在声明后使用`bar()`。 ```js assert.throws( () => bar(), // before declaration ReferenceError); const bar = () => { return 123; }; assert.equal(bar(), 123); // after declaration ``` 类声明不会悬挂,也可以: ```js assert.throws( () => new MyClass(), ReferenceError); class MyClass {} assert.equal(new MyClass() instanceof MyClass, true); ``` #### 23.4.1。在没有吊装的情况下提前召唤 注意函数`f()`仍然可以在声明之前调用非提升函数`g()` - 如果在声明`g()`之后调用`f()`: ```js const f = () => g(); const g = () => 123; // We call f() after g() was declared: assert.equal(f(), 123); ``` 通常在执行模块的完整主体之后调用模块的功能。因此,您很少需要担心模块中的函数顺序。 #### 23.4.2。吊装的陷阱 如果您在声明之前依赖于提升来调用函数,那么您需要注意它不能访问非提升数据。 ```js hoistedFunc(); const MY_STR = 'abc'; function hoistedFunc() { assert.throws( () => MY_STR, ReferenceError); } ``` 和以前一样,如果你在最后调用函数`hoistedFunc()`,问题就会消失。 ### 23.5。从函数返回值 使用`return`运算符从函数返回值: ```js function func() { return 123; } assert.equal(func(), 123); ``` 另一个例子: ```js function boolToYesNo(bool) { if (bool) { return 'Yes'; } else { return 'No'; } } assert.equal(boolToYesNo(true), 'Yes'); assert.equal(boolToYesNo(false), 'No'); ``` 如果在函数末尾没有显式返回任何内容,JavaScript 会为您返回`undefined`: ```js function noReturn() { // No explicit return } assert.equal(noReturn(), undefined); ``` ### 23.6。参数处理 #### 23.6.1。术语:参数(parameters)与参数(arguments) 术语 参数 (parameters) 和术语 参数 (arguments)基本上意思相同。如果您愿意,可以进行以下区分: * 参数 (parameters) 是函数定义的一部分。它们也被称为 形式参数 (formal parameters )和 形式参数 (formal arguments.)。 * 参数 (arguments) 是函数调用的一部分。它们也被称为 实际参数 actual parameters 和 实际参数 actual arguments.。 #### 23.6.2。术语:回调 _ 回调 _ 或 _ 回调函数 _ 是作为参数传递给另一个函数或方法的函数。此术语经常在 JavaScript 社区中广泛使用。 以下是回调的示例: ```js const myArray = ['a', 'b']; const callback = (x) => console.log(x); myArray.forEach(callback); // Output: // 'a' // 'b' ``` #### 23.6.3。参数太多或不够 如果函数调用提供的参数数量不同于函数定义所预期的数量,则 JavaScript 不会抱怨: * 额外的参数被忽略。 * 缺少的参数设置为`undefined`。 例如: ```js function foo(x, y) { return [x, y]; } // Too many arguments: assert.deepEqual(foo('a', 'b', 'c'), ['a', 'b']); // The expected number of arguments: assert.deepEqual(foo('a', 'b'), ['a', 'b']); // Not enough arguments: assert.deepEqual(foo('a'), ['a', undefined]); ``` #### 23.6.4。参数默认值 参数默认值指定在未提供参数时要使用的值。例如: ```js function f(x, y=0) { return [x, y]; } assert.deepEqual(f(1), [1, 0]); assert.deepEqual(f(), [undefined, 0]); ``` `undefined`也会触发默认值: ```js assert.deepEqual( f(undefined, undefined), [undefined, 0]); ``` #### 23.6.5。休息参数 通过在标识符前面添加三个点(`...`)来声明 rest 参数。在函数或方法调用期间,它接收包含所有剩余参数的数组。如果最后没有额外的参数,那么它是一个空数组。例如: ```js function f(x, ...y) { return [x, y]; } assert.deepEqual( f('a', 'b', 'c'), ['a', ['b', 'c']]); assert.deepEqual( f(), [undefined, []]); ``` ##### 23.6.5.1。通过 rest 参数强制执行一定数量的参数 您可以使用 rest 参数来强制执行一定数量的参数。例如,以下功能。 ```js function bar(a, b) { // ··· } ``` 这就是我们强制调用者总是提供两个参数的方法: ```js function bar(...args) { if (args.length !== 2) { throw new Error('Please provide exactly 2 arguments!'); } const [a, b] = args; // ··· } ``` #### 23.6.6。命名参数 当有人调用函数时,调用者提供的参数将分配给被调用者接收的参数。执行映射的两种常用方法是: 1. 位置参数:如果参数具有相同的位置,则将参数分配给参数。仅具有位置参数的函数调用如下所示。 ```js selectEntries(3, 20, 2) ``` 2. 命名参数:如果参数具有相同的名称,则将参数分配给参数。 JavaScript 没有命名参数,但您可以模拟它们。例如,这是一个只有(模拟)命名参数的函数调用: ```js selectEntries({start: 3, end: 20, step: 2}) ``` 命名参数有几个好处: * 它们导致更加自我解释的代码,因为每个参数都有一个描述性标签。只需比较`selectEntries()`的两个版本:使用第二个版本,可以更容易地看到发生了什么。 * 参数顺序无关紧要(只要名称正确)。 * 处理多个可选参数更方便:调用者可以轻松提供所有可选参数的任何子集,而不必知道它们省略的那些(使用位置参数,您必须使用`undefined`填写前面的可选参数])。 #### 23.6.7。模拟命名参数 JavaScript 没有真正的命名参数。模拟它们的官方方法是通过对象字面值: ```js function selectEntries({start=0, end=-1, step=1}) { return {start, end, step}; } ``` 此函数使用 _ 解构 _ 来访问其单个参数的属性。它使用的模式是以下模式的缩写: ```js {start: start=0, end: end=-1, step: step=1} ``` 这种解构模式适用于空对象字面值: ```js > selectEntries({}) { start: 0, end: -1, step: 1 } ``` 但是如果你在没有任何参数的情况下调用函数它就不起作用: ```js > selectEntries() TypeError: Cannot destructure property `start` of 'undefined' or 'nu ``` 您可以通过为整个模式提供默认值来解决此问题。此默认值与更简单的参数定义的默认值相同:如果缺少该参数,则使用默认值。 ```js function selectEntries({start=0, end=-1, step=1} = {}) { return {start, end, step}; } assert.deepEqual( selectEntries(), { start: 0, end: -1, step: 1 }); ``` #### 23.6.8。将(`...`)传播到函数调用中 spread 参数的前缀(`...`)与 rest 参数的前缀相同。在调用函数或方法时使用前者。它的操作数必须是可迭代的对象。迭代的值转换为位置参数。例如: ```js function func(x, y) { console.log(x); console.log(y); } const someIterable = ['a', 'b']; func(...someIterable); // Output: // 'a' // 'b' ``` 因此,扩展参数和 rest 参数用于相反的目的: * 定义函数或方法时使用 Rest 参数。他们在数组中收集参数。 * 调用函数或方法时使用 Spread 参数。他们将可迭代对象转换为参数。 ##### 23.6.8.1。示例:传播到`Math.max()` `Math.max()`返回其零个或多个参数中最大的一个。唉,它不能用于数组,但传播给了我们一条出路: ```js > Math.max(-1, 5, 11, 3) 11 > Math.max(...[-1, 5, 11, 3]) 11 > Math.max(-1, ...[-5, 11], 3) 11 ``` ##### 23.6.8.2。示例:传播到`Array.prototype.push()` 类似地,Array 方法`.push()`破坏性地将其零个或多个参数添加到其 Array 的末尾。 JavaScript 没有破坏性地将数组附加到另​​一个数组的方法,但我们再次通过传播保存: ```js const arr1 = ['a', 'b']; const arr2 = ['c', 'd']; arr1.push(...arr2); assert.deepEqual(arr1, ['a', 'b', 'c', 'd']); ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:参数处理** * 位置参数:`exercises/callables/positional_parameters_test.js` * 命名参数:`exercises/callables/named_parameters_test.js` ![](https://img.kancloud.cn/ff/a8/ffa8e16628cad59b09c786b836722faa.svg) **测验** 参见[测验应用程序](ch_quizzes-exercises.html#quizzes)。