## 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']);
```
 **練習:參數處理**
* 位置參數:`exercises/callables/positional_parameters_test.js`
* 命名參數:`exercises/callables/named_parameters_test.js`
 **測驗**
參見[測驗應用程序](ch_quizzes-exercises.html#quizzes)。
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?