this
.call()
、.apply()
、.bind()
.call()
.apply()
.bind()
在本章中,我們將探討可調用的 JavaScript 值:函式、方法和類別。
JavaScript 有兩種類別的函式
一般函式 可以扮演多種角色
特殊函式 只能扮演其中一種角色,例如
特殊函式是在 ECMAScript 6 中新增到語言中的。
請繼續閱讀以了解所有這些東西的含義。
以下程式碼顯示了兩種執行(大致)相同事情的方法:建立一般函式。
// Function declaration (a statement)
function ordinary1(a, b, c) {
// ···
}
// const plus anonymous (nameless) function expression
const ordinary2 = function (a, b, c) {
// ···
; }
在範圍內,函式宣告會提早啟動(請參閱 §11.8 “宣告:範圍和啟動”),並可以在宣告之前呼叫。這偶爾很有用。
變數宣告,例如 ordinary2
的宣告,不會提早啟動。
到目前為止,我們只看過匿名函式運算式,它們沒有名稱
const anonFuncExpr = function (a, b, c) {
// ···
; }
但也有命名函式運算式
const namedFuncExpr = function myName(a, b, c) {
// `myName` is only accessible in here
; }
myName
只能在函式主體內存取。函式可以使用它來參照自身(用於自我遞迴等),而與它被指定給哪個變數無關
const func = function funcExpr() { return funcExpr };
.equal(func(), func);
assert
// The name `funcExpr` only exists inside the function body:
.throws(() => funcExpr(), ReferenceError); assert
即使沒有指定給變數,命名函式運算式也有名稱(第 A 行)
function getNameOfCallback(callback) {
return callback.name;
}
.equal(
assertgetNameOfCallback(function () {}), ''); // anonymous
.equal(
assertgetNameOfCallback(function named() {}), 'named'); // (A)
請注意,透過函式宣告或變數宣告建立的函式始終都有名稱
function funcDecl() {}
.equal(
assertgetNameOfCallback(funcDecl), 'funcDecl');
const funcExpr = function () {};
.equal(
assertgetNameOfCallback(funcExpr), 'funcExpr');
函式有名稱的一個好處是這些名稱會顯示在 錯誤堆疊追蹤 中。
函式定義 是建立函式的語法
函式宣告總是產生一般函式。函式運算式產生一般函式或特殊函式
儘管函數宣告在 JavaScript 中仍然很受歡迎,但在現代程式碼中函數表達式幾乎都是箭頭函數。
讓我們透過以下範例來檢視函數宣告的部分。大多數術語也適用於函數表達式。
function add(x, y) {
return x + y;
}
add
是函數宣告的名稱。add(x, y)
是函數宣告的標頭。x
和 y
是參數。{
和 }
)及其之間的所有內容是函數宣告的主體。return
陳述式明確地從函數傳回一個值。JavaScript 一直允許並忽略陣列文字中的尾隨逗號。自 ES5 以來,它們也允許在物件文字中使用。自 ES2017 以來,我們可以將尾隨逗號新增到參數清單(宣告和呼叫)中
// Declaration
function retrieveData(
,
contentText,
keyword, ignoreCase, pageSize}, // trailing comma
{unique
) {// ···
}
// Invocation
retrieveData(
'',
null,
ignoreCase: true, pageSize: 10}, // trailing comma
{; )
考慮前一節中的以下函數宣告
function add(x, y) {
return x + y;
}
此函數宣告會建立一個一般函數,其名稱為 add
。作為一般函數,add()
可以扮演三個角色
.equal(add(2, 1), 3); assert
const obj = { addAsMethod: add };
.equal(obj.addAsMethod(2, 4), 6); // (A) assert
在 A 行中,obj
被稱為方法呼叫的接收器。
const inst = new add();
.equal(inst instanceof add, true); assert
順帶一提,建構函數(包括類別)的名稱通常以大寫字母開頭。
語法、實體和角色這幾個概念之間的區別很微妙,而且通常並不重要。但我希望讓您更了解它們
許多其他程式語言只有一個實體扮演實質函式的角色。然後,它們可以使用名稱函式來表示角色和實體。
特殊函式是一般函式的單一用途版本。它們各自專精於單一角色
箭頭函式的目的是成為實質函式
const arrow = () => {
return 123;
;
}.equal(arrow(), 123); assert
方法的目的是成為方法
const obj = {
myMethod() {
return 'abc';
};
}.equal(obj.myMethod(), 'abc'); assert
類別的目的是成為建構函式
class MyClass {
/* ··· */
}const inst = new MyClass();
除了語法更佳之外,每種特殊函式也支援新功能,讓它們比一般函式更擅長自己的工作。
表 16列出一般函式和特殊函式的功能。
函式呼叫 | 方法呼叫 | 建構函式呼叫 | |
---|---|---|---|
一般函式 | (this === undefined ) |
✔ |
✔ |
箭頭函式 | ✔ |
(字彙this ) |
✘ |
方法 | (this === undefined ) |
✔ |
✘ |
類別 | ✘ |
✘ |
✔ |
請務必注意,箭頭函式、方法和類別仍然歸類為函式
> (() => {}) instanceof Functiontrue
> ({ method() {} }.method) instanceof Functiontrue
> (class SomeClass {}) instanceof Functiontrue
將箭頭函式新增到 JavaScript 的原因有兩個
this
來參照收到方法呼叫的物件。箭頭函式可以存取周圍方法的this
,一般函式不能(因為它們有自己的this
)。我們將先探討箭頭函式的語法,然後探討this
在各種函式中的運作方式。
讓我們回顧匿名函式表達式的語法
const f = function (x, y, z) { return 123 };
(大致)等價的箭頭函式如下所示。箭頭函式是表達式。
const f = (x, y, z) => { return 123 };
這裡,箭頭函式的函式主體是一個區塊。但它也可以是一個表達式。下列箭頭函式的工作方式與前一個完全相同。
const f = (x, y, z) => 123;
如果箭頭函式只有一個參數,而且該參數是一個識別碼(不是 解構模式),則可以省略參數周圍的括號
const id = x => x;
在將箭頭函式作為參數傳遞給其他函式或方法時,這很方便
> [1,2,3].map(x => x+1)[ 2, 3, 4 ]
這個先前的範例展示了箭頭函式的一個好處 - 簡潔。如果我們使用函式表達式執行相同的任務,我們的程式碼會更冗長
1,2,3].map(function (x) { return x+1 }); [
如果你希望箭頭函式的表達式主體是一個物件文字,你必須將文字放在括號中
const func1 = () => ({a: 1});
.deepEqual(func1(), { a: 1 }); assert
如果你不這麼做,JavaScript 會認為箭頭函式有一個區塊主體(不會傳回任何東西)
const func2 = () => {a: 1};
.deepEqual(func2(), undefined); assert
{a: 1}
被解釋為一個區塊,其中包含 標籤 a:
和表達式陳述式 1
。在沒有明確的 return
陳述式的情況下,區塊主體會傳回 undefined
。
這個陷阱是由於 語法歧義 造成的:物件文字和程式碼區塊具有相同的語法。我們使用括號告訴 JavaScript 主體是一個表達式(一個物件文字),而不是一個陳述式(一個區塊)。
this
特殊變數
this
是物件導向功能
我們在此快速瀏覽特殊變數 this
,以了解為什麼箭頭函式比一般函式更好的真正函式。
但這個功能僅在物件導向程式設計中很重要,並在 §28.5「方法和特殊變數 this
」 中有更深入的介紹。因此,如果你還不完全理解它,請不要擔心。
在方法內,特殊變數 this
讓我們可以存取接收者 - 收到方法呼叫的物件
const obj = {
myMethod() {
.equal(this, obj);
assert
};
}.myMethod(); obj
一般函式可以是方法,因此也具有隱含參數 this
const obj = {
myMethod: function () {
.equal(this, obj);
assert
};
}.myMethod(); obj
當我們將一般函式作為實質函式使用時,this
甚至是一個隱含參數。然後,它的值為 undefined
(如果 嚴格模式 處於啟用狀態,這幾乎總是如此)
function ordinaryFunc() {
.equal(this, undefined);
assert
}ordinaryFunc();
這表示用作實質函式的普通函式無法存取周圍方法的 this
(A 行)。相反,箭頭函式不將 this
作為隱含參數。它們將其視為任何其他變數,因此可以存取周圍方法的 this
(B 行)
const jill = {
name: 'Jill',
someMethod() {
function ordinaryFunc() {
.throws(
assert=> this.name, // (A)
() /^TypeError: Cannot read properties of undefined \(reading 'name'\)$/);
}ordinaryFunc();
const arrowFunc = () => {
.equal(this.name, 'Jill'); // (B)
assert;
}arrowFunc();
,
};
}.someMethod(); jill
在此程式碼中,我們可以觀察到處理 this
的兩種方式
動態 this
:在 A 行中,我們嘗試從一般函式存取 .someMethod()
的 this
。在那裡,它會被函式自己的 this
(即 undefined
,由函式呼叫填入)遮蔽。由於一般函式透過(動態)函式或方法呼叫接收其 this
,因此其 this
稱為動態。
字彙 this
:在 B 行中,我們再次嘗試存取 .someMethod()
的 this
。這次我們成功了,因為箭頭函式沒有自己的 this
。this
會字彙上解析,就像任何其他變數一樣。這就是為什麼箭頭函式的 this
被稱為字彙。
通常,你應該優先使用特殊函式而非一般函式,特別是類別和方法。
不過,當涉及到實質函式時,箭頭函式和一般函式之間的選擇就不那麼明確了
對於匿名內嵌函式表達式,箭頭函式是明確的贏家,因為它們的語法緊湊,而且沒有將 this
作為隱含參數
const twiceOrdinary = [1, 2, 3].map(function (x) {return x * 2});
const twiceArrow = [1, 2, 3].map(x => x * 2);
對於獨立的命名函式宣告,箭頭函式仍然受益於字彙 this
。但是,函式宣告(產生一般函式)具有良好的語法,而且早期啟用偶爾也有用(請參閱 §11.8「宣告:範圍和啟用」)。如果 this
沒有出現在一般函式的本體中,則將其用作實質函式沒有缺點。靜態檢查工具 ESLint 可以透過 內建規則 在開發過程中警告我們何時做錯。
function timesOrdinary(x, y) {
return x * y;
}const timesArrow = (x, y) => {
return x * y;
; }
此節參考後續內容
本節主要用作對目前和後續章節的參考。如果你不了解所有內容,請不用擔心。
到目前為止,我們所見的所有(實質)函式和方法都是
後面的章節會介紹其他程式設計模式
這些模式可以結合使用,例如,有同步可迭代物件和非同步可迭代物件。
有幾種新的函式和方法有助於處理某些模式組合
這讓我們有 4 種(2 × 2)函式和方法
表格 17 提供建立這 4 種函式和方法的語法概觀。
結果 | # | ||
---|---|---|---|
同步函式 | 同步方法 | ||
function f() {} |
{ m() {} } |
值 | 1 |
f = function () {} |
|||
f = () => {} |
|||
同步產生器函式 | 同步產生器方法 | ||
function* f() {} |
{ * m() {} } |
可迭代物件 | 0+ |
f = function* () {} |
|||
非同步函式 | 非同步方法 | ||
async function f() {} |
{ async m() {} } |
Promise | 1 |
f = async function () {} |
|||
f = async () => {} |
|||
非同步產生器函式 | 非同步產生器方法 | ||
async function* f() {} |
{ async * m() {} } |
非同步可迭代物件 | 0+ |
f = async function* () {} |
(本節中提到的所有內容都適用於函數和方法。)
return
陳述式明確地從函數回傳一個值
function func() {
return 123;
}.equal(func(), 123); assert
另一個範例
function boolToYesNo(bool) {
if (bool) {
return 'Yes';
else {
} return 'No';
}
}.equal(boolToYesNo(true), 'Yes');
assert.equal(boolToYesNo(false), 'No'); assert
如果在函數的結尾處沒有明確回傳任何內容,JavaScript 會自動回傳 undefined
function noReturn() {
// No explicit return
}.equal(noReturn(), undefined); assert
再次強調,本節中我只提及函數,但所有內容也適用於方法。
參數 和 引數 這兩個術語基本上是同義詞。如果你願意,可以做以下區分
參數 是函數定義的一部分。它們也稱為形式參數和形式引數。
引數 是函數呼叫的一部分。它們也稱為實際參數和實際引數。
回呼 或 回呼函數 是函數或方法呼叫的引數的函數。
以下是回呼的範例
const myArray = ['a', 'b'];
const callback = (x) => console.log(x);
.forEach(callback);
myArray
// Output:
// 'a'
// 'b'
如果函數呼叫提供的引數數量與函數定義預期的不同,JavaScript 也不會抱怨
undefined
。例如
function foo(x, y) {
return [x, y];
}
// Too many arguments:
.deepEqual(foo('a', 'b', 'c'), ['a', 'b']);
assert
// The expected number of arguments:
.deepEqual(foo('a', 'b'), ['a', 'b']);
assert
// Not enough arguments:
.deepEqual(foo('a'), ['a', undefined]); assert
參數預設值指定在未提供參數時要使用的值,例如
function f(x, y=0) {
return [x, y];
}
.deepEqual(f(1), [1, 0]);
assert.deepEqual(f(), [undefined, 0]); assert
undefined
也會觸發預設值
.deepEqual(
assertf(undefined, undefined),
undefined, 0]); [
餘數參數是透過在識別碼之前加上三個點(...
)來宣告。在函數或方法呼叫期間,它會收到一個包含所有剩餘引數的陣列。如果最後沒有多餘的引數,它會是一個空的陣列,例如
function f(x, ...y) {
return [x, y];
}.deepEqual(
assertf('a', 'b', 'c'), ['a', ['b', 'c']]
;
).deepEqual(
assertf(), [undefined, []]
; )
與我們如何使用餘數參數相關的兩個限制
我們不能在每個函數定義中使用超過一個餘數參數。
.throws(
assert=> eval('function f(...x, ...y) {}'),
() /^SyntaxError: Rest parameter must be last formal parameter$/
; )
餘數參數必須永遠放在最後。因此,我們不能像這樣存取最後一個參數
.throws(
assert=> eval('function f(...restParams, lastParam) {}'),
() /^SyntaxError: Rest parameter must be last formal parameter$/
; )
你可以使用餘數參數來強制執行一定的引數數量。例如,以下函數
function createPoint(x, y) {
return {x, y};
// same as {x: x, y: y}
}
這是我們強制呼叫者永遠提供兩個引數的方式
function createPoint(...args) {
if (args.length !== 2) {
throw new Error('Please provide exactly 2 arguments!');
}const [x, y] = args; // (A)
return {x, y};
}
在 A 行中,我們透過解構存取 args
的元素。
當有人呼叫函數時,呼叫者提供的引數會指定給被呼叫者接收的參數。執行對應的兩個常見方式是
位置參數:如果參數和實例具有相同位置,則實例會指定給參數。僅使用位置參數的函式呼叫如下所示。
selectEntries(3, 20, 2)
命名參數:如果參數和實例具有相同名稱,則實例會指定給參數。JavaScript 沒有命名參數,但您可以模擬它們。例如,這是僅使用(模擬)命名參數的函式呼叫
selectEntries({start: 3, end: 20, step: 2})
命名參數有幾個好處
它們會產生更具說明性的程式碼,因為每個參數都有說明性的標籤。只要比較兩個版本的 selectEntries()
:使用第二個版本,更容易看出會發生什麼事。
參數的順序並不重要(只要名稱正確)。
處理多個選用參數更方便:呼叫者可以輕鬆提供所有選用參數的任何子集,而且不必知道他們省略了哪些參數(使用位置參數時,您必須填入前面的選用參數,並使用 undefined
)。
JavaScript 沒有真正的命名參數。模擬它們的官方方式是透過物件文字
function selectEntries({start=0, end=-1, step=1}) {
return {start, end, step};
}
此函式使用解構存取其單一參數的屬性。它使用的模式是下列模式的縮寫
start: start=0, end: end=-1, step: step=1} {
此解構模式適用於空的物件文字
> selectEntries({}){ start: 0, end: -1, step: 1 }
但如果您在沒有任何參數的情況下呼叫函式,它就不會運作
> selectEntries()TypeError: Cannot read properties of undefined (reading 'start')
您可以透過為整個模式提供預設值來修正此問題。此預設值與較簡單參數定義的預設值相同:如果參數遺失,則使用預設值。
function selectEntries({start=0, end=-1, step=1} = {}) {
return {start, end, step};
}.deepEqual(
assertselectEntries(),
start: 0, end: -1, step: 1 }); {
...
)到函式呼叫中如果您在函式呼叫的參數前面加上三個點(...
),則表示您散佈它。這表示參數必須是可迭代物件,而迭代值全部都變成參數。換句話說,單一參數會擴充為多個參數,例如
function func(x, y) {
console.log(x);
console.log(y);
}const someIterable = ['a', 'b'];
func(...someIterable);
// same as func('a', 'b')
// Output:
// 'a'
// 'b'
散佈和 rest 參數使用相同的語法(...
),但它們有相反的目的
Math.max()
Math.max()
會傳回其零個或多個參數中最大的那個。唉,它無法用於陣列,但 spread 提供了方法讓我們解決這個問題
> Math.max(-1, 5, 11, 3)11
> Math.max(...[-1, 5, 11, 3])11
> Math.max(-1, ...[-5, 11], 3)11
Array.prototype.push()
類似地,陣列方法 .push()
會將其零個或多個參數破壞性地新增到陣列的尾端。JavaScript 沒有方法可以將陣列破壞性地附加到另一個陣列。再一次,spread 拯救了我們
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];
.push(...arr2);
arr1.deepEqual(arr1, ['a', 'b', 'c', 'd']); assert
練習:參數處理
exercises/callables/positional_parameters_test.mjs
exercises/callables/named_parameters_test.mjs
.call()
、.apply()
、.bind()
函式是物件,而且有函式。在本節中,我們會探討其中三個函式:.call()
、.apply()
和 .bind()
。
.call()
每個函式 someFunc
都有下列函式
.call(thisValue, arg1, arg2, arg3); someFunc
這個函式呼叫大致等於下列函式呼叫
someFunc(arg1, arg2, arg3);
不過,使用 .call()
時,我們也可以指定 隱式參數 this
的值。換句話說:.call()
會讓隱式參數 this
變成明確的。
下列程式碼示範如何使用 .call()
function func(x, y) {
return [this, x, y];
}
.deepEqual(
assert.call('hello', 'a', 'b'),
func'hello', 'a', 'b']); [
正如我們之前所見,如果我們函式呼叫一般函式,其 this
會是 undefined
.deepEqual(
assertfunc('a', 'b'),
undefined, 'a', 'b']); [
因此,前一個函式呼叫等於
.deepEqual(
assert.call(undefined, 'a', 'b'),
funcundefined, 'a', 'b']); [
在箭頭函式中,透過 .call()
(或其他方法)提供的 this
值會被忽略。
.apply()
每個函式 someFunc
都有下列函式
.apply(thisValue, [arg1, arg2, arg3]); someFunc
這個函式呼叫大致等於下列函式呼叫(使用 spread)
someFunc(...[arg1, arg2, arg3]);
不過,使用 .apply()
時,我們也可以指定 隱式參數 this
的值。
下列程式碼示範如何使用 .apply()
function func(x, y) {
return [this, x, y];
}
const args = ['a', 'b'];
.deepEqual(
assert.apply('hello', args),
func'hello', 'a', 'b']); [
.bind()
.bind()
是函式物件的另一個函式。這個函式會以下列方式呼叫
const boundFunc = someFunc.bind(thisValue, arg1, arg2);
.bind()
會傳回一個新的函式 boundFunc()
。呼叫該函式會呼叫 someFunc()
,其中 this
設定為 thisValue
,且參數為:arg1
、arg2
,接著是 boundFunc()
的參數。
也就是說,以下兩個函式呼叫是等效的
boundFunc('a', 'b')
.call(thisValue, arg1, arg2, 'a', 'b') someFunc
.bind()
的替代方案預先填入 this
和參數的另一種方式是透過箭頭函式
const boundFunc2 = (...args) =>
.call(thisValue, arg1, arg2, ...args); someFunc
.bind()
的實作考量前一節,.bind()
可以實作為一個真實函式,如下所示
function bind(func, thisValue, ...boundArgs) {
return (...args) =>
.call(thisValue, ...boundArgs, ...args);
func }
將 .bind()
用於真實函式有點不直觀,因為我們必須提供一個值給 this
。由於在函式呼叫期間 this
是 undefined
,因此通常會設定為 undefined
或 null
。
在以下範例中,我們建立 add8()
,一個有一個參數的函式,方法是將 add()
的第一個參數繫結到 8
。
function add(x, y) {
return x + y;
}
const add8 = add.bind(undefined, 8);
.equal(add8(1), 9); assert
測驗
請參閱 測驗應用程式。