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'
扩展和剩余参数使用相同的语法 (...
),但它们的目的相反
Math.max()
中Math.max()
返回其零个或多个参数中最大的一个。唉,它不能用于数组,但扩展为我们提供了一种解决方法
> 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 没有将数组破坏性地附加到另一个数组的方法。再一次,我们通过扩展得救了
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
此方法调用大致等效于以下函数调用(使用扩展)
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()
。调用该函数将使用设置为 thisValue
的 this
以及以下参数调用 someFunc()
: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
提供一个值。鉴于在函数调用期间它是 undefined
,因此通常将其设置为 undefined
或 null
。
在以下示例中,我们通过将 add()
的第一个参数绑定到 8
来创建一个带有一个参数的函数 add8()
。
function add(x, y) {
return x + y;
}
const add8 = add.bind(undefined, 8);
.equal(add8(1), 9); assert
测验
参见测验应用程序。