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 };
assert.equal(func(), func);
// The name `funcExpr` only exists inside the function body:
assert.throws(() => funcExpr(), ReferenceError);即使未分配给变量,命名函数表达式也有名称(第 A 行)
function getNameOfCallback(callback) {
return callback.name;
}
assert.equal(
getNameOfCallback(function () {}), ''); // anonymous
assert.equal(
getNameOfCallback(function named() {}), 'named'); // (A)请注意,通过函数声明或变量声明创建的函数始终具有名称
function funcDecl() {}
assert.equal(
getNameOfCallback(funcDecl), 'funcDecl');
const funcExpr = function () {};
assert.equal(
getNameOfCallback(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,
{unique, ignoreCase, pageSize}, // trailing comma
) {
// ···
}
// Invocation
retrieveData(
'',
null,
{ignoreCase: true, pageSize: 10}, // trailing comma
);请考虑上一节中的以下函数声明
function add(x, y) {
return x + y;
}此函数声明创建了一个名为 add 的普通函数。作为普通函数,add() 可以扮演三种角色
assert.equal(add(2, 1), 3);const obj = { addAsMethod: add };
assert.equal(obj.addAsMethod(2, 4), 6); // (A)在第 A 行中,obj 被称为方法调用的接收者。
const inst = new add();
assert.equal(inst instanceof add, true);顺便说一句,构造函数(包括类)的名称通常以大写字母开头。
语法、实体和作用这三个概念之间的区别很微妙,而且通常无关紧要。但我想让你对此更加敏锐
许多其他编程语言只有一个实体扮演真实函数的作用。然后,它们可以使用名称函数来表示作用和实体。
专用函数是普通函数的单一用途版本。它们中的每一个都专门用于一种角色
箭头函数的目的是作为真实函数
const arrow = () => {
return 123;
};
assert.equal(arrow(), 123);方法的目的是作为方法
const obj = {
myMethod() {
return 'abc';
}
};
assert.equal(obj.myMethod(), 'abc');类的目的是作为构造函数
class MyClass {
/* ··· */
}
const inst = new MyClass();除了更简洁的语法外,每种专用函数还支持新特性,使其在工作中比普通函数更出色。
表 16 列出了普通函数和专用函数的功能。
| 函数调用 | 方法调用 | 构造函数调用 | |
|---|---|---|---|
| 普通函数 | (this === undefined) |
✔ |
✔ |
| 箭头函数 | ✔ |
(词法 this) |
✘ |
| 方法 | (this === undefined) |
✔ |
✘ |
| 类 | ✘ |
✘ |
✔ |
重要的是要注意,箭头函数、方法和类仍然被归类为函数
> (() => {}) instanceof Function
true
> ({ method() {} }.method) instanceof Function
true
> (class SomeClass {}) instanceof Function
true将箭头函数添加到 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});
assert.deepEqual(func1(), { a: 1 });如果不这样做,JavaScript 会认为箭头函数有一个块主体(不返回任何内容)
const func2 = () => {a: 1};
assert.deepEqual(func2(), undefined);{a: 1} 被解释为带有 标签 a: 的块和表达式语句 1。如果没有显式的 return 语句,则块主体返回 undefined。
此陷阱是由 语法歧义 引起的:对象字面量和代码块具有相同的语法。我们使用括号来告诉 JavaScript 主体是一个表达式(对象字面量),而不是一个语句(块)。
this 特殊变量
this 是面向对象的特性
我们在这里快速了解一下特殊变量 this,以便理解为什么箭头函数比普通函数更适合作为真实函数。
但此特性仅在面向对象编程中才有意义,并在 §28.5 “方法和特殊变量 this” 中进行了更深入的介绍。因此,如果您现在还没有完全理解它,请不要担心。
在方法内部,特殊变量 this 允许我们访问接收者,即接收方法调用的对象
const obj = {
myMethod() {
assert.equal(this, obj);
}
};
obj.myMethod();普通函数可以是方法,因此也具有隐式参数 this
const obj = {
myMethod: function () {
assert.equal(this, obj);
}
};
obj.myMethod();即使我们将普通函数用作真实函数,this 也是一个隐式参数。然后它的值是 undefined(如果 严格模式处于活动状态,而它几乎总是处于活动状态)
function ordinaryFunc() {
assert.equal(this, undefined);
}
ordinaryFunc();这意味着,用作真实函数的普通函数无法访问周围方法的 this(A 行)。相反,箭头函数没有将 this 作为隐式参数。它们像对待任何其他变量一样对待它,因此可以访问周围方法的 this(B 行)
const jill = {
name: 'Jill',
someMethod() {
function ordinaryFunc() {
assert.throws(
() => this.name, // (A)
/^TypeError: Cannot read properties of undefined \(reading 'name'\)$/);
}
ordinaryFunc();
const arrowFunc = () => {
assert.equal(this.name, 'Jill'); // (B)
};
arrowFunc();
},
};
jill.someMethod();在这段代码中,我们可以观察到两种处理 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;
}
assert.equal(func(), 123);另一个例子
function boolToYesNo(bool) {
if (bool) {
return 'Yes';
} else {
return 'No';
}
}
assert.equal(boolToYesNo(true), 'Yes');
assert.equal(boolToYesNo(false), 'No');如果在函数结束时没有显式返回任何内容,JavaScript 会为您返回 undefined
function noReturn() {
// No explicit return
}
assert.equal(noReturn(), undefined);再次强调,我在本节中仅提及函数,但所有内容也适用于方法。
术语_参数_和术语_参数_基本上指的是同一件事。如果您愿意,可以进行以下区分
_参数_是函数定义的一部分。它们也称为_形式参数_和_形式参数_。
_参数_是函数调用的一部分。它们也称为_实际参数_和_实际参数_。
_回调_或_回调函数_是作为函数或方法调用的参数的函数。
以下是回调的示例
const myArray = ['a', 'b'];
const callback = (x) => console.log(x);
myArray.forEach(callback);
// Output:
// 'a'
// 'b'如果函数调用提供的参数数量与函数定义期望的不同,JavaScript 不会报错
undefined。例如
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]);参数默认值指定在未提供参数时使用的值——例如
function f(x, y=0) {
return [x, y];
}
assert.deepEqual(f(1), [1, 0]);
assert.deepEqual(f(), [undefined, 0]);undefined 也会触发默认值
assert.deepEqual(
f(undefined, undefined),
[undefined, 0]);剩余参数通过在标识符前面加上三个点 (...) 来声明。在函数或方法调用期间,它接收一个包含所有剩余参数的数组。如果最后没有额外的参数,则它是一个空数组——例如
function f(x, ...y) {
return [x, y];
}
assert.deepEqual(
f('a', 'b', 'c'), ['a', ['b', 'c']]
);
assert.deepEqual(
f(), [undefined, []]
);关于如何使用剩余参数有两个限制
每个函数定义不能使用多个剩余参数。
assert.throws(
() => eval('function f(...x, ...y) {}'),
/^SyntaxError: Rest parameter must be last formal parameter$/
);剩余参数必须始终放在最后。因此,我们无法像这样访问最后一个参数
assert.throws(
() => 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};
}
assert.deepEqual(
selectEntries(),
{ 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)
11Array.prototype.push() 中类似地,数组方法 .push() 将其零个或多个参数破坏性地添加到其数组的末尾。JavaScript 没有将数组破坏性地附加到另一个数组的方法。再一次,我们通过扩展得救了
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];
arr1.push(...arr2);
assert.deepEqual(arr1, ['a', 'b', 'c', 'd']); 练习:参数处理
exercises/callables/positional_parameters_test.mjsexercises/callables/named_parameters_test.mjs.call()、.apply()、.bind()函数是对象并且具有方法。在本节中,我们将研究其中三种方法:.call()、.apply() 和 .bind()。
.call()每个函数 someFunc 都有以下方法
someFunc.call(thisValue, arg1, arg2, arg3);此方法调用大致等效于以下函数调用
someFunc(arg1, arg2, arg3);但是,使用 .call(),我们还可以为隐式参数 this指定一个值。换句话说:.call() 使隐式参数 this 显式化。
以下代码演示了 .call() 的使用
function func(x, y) {
return [this, x, y];
}
assert.deepEqual(
func.call('hello', 'a', 'b'),
['hello', 'a', 'b']);正如我们之前所见,如果我们函数调用一个普通函数,它的 this 是 undefined
assert.deepEqual(
func('a', 'b'),
[undefined, 'a', 'b']);因此,前面的函数调用等效于
assert.deepEqual(
func.call(undefined, 'a', 'b'),
[undefined, 'a', 'b']);在箭头函数中,通过 .call()(或其他方式)为 this 提供的值将被忽略。
.apply()每个函数 someFunc 都有以下方法
someFunc.apply(thisValue, [arg1, arg2, arg3]);此方法调用大致等效于以下函数调用(使用扩展)
someFunc(...[arg1, arg2, arg3]);但是,使用 .apply(),我们还可以为隐式参数 this指定一个值。
以下代码演示了 .apply() 的使用
function func(x, y) {
return [this, x, y];
}
const args = ['a', 'b'];
assert.deepEqual(
func.apply('hello', args),
['hello', 'a', 'b']);.bind().bind() 是函数对象的另一种方法。此方法的调用方式如下
const boundFunc = someFunc.bind(thisValue, arg1, arg2);.bind() 返回一个新函数 boundFunc()。调用该函数将使用设置为 thisValue 的 this 以及以下参数调用 someFunc():arg1、arg2,后跟 boundFunc() 的参数。
也就是说,以下两个函数调用是等效的
boundFunc('a', 'b')
someFunc.call(thisValue, arg1, arg2, 'a', 'b').bind() 的替代方法另一种预填充 this 和参数的方法是通过箭头函数
const boundFunc2 = (...args) =>
someFunc.call(thisValue, arg1, arg2, ...args);.bind() 的实现考虑到上一节,.bind() 可以实现为如下真实函数
function bind(func, thisValue, ...boundArgs) {
return (...args) =>
func.call(thisValue, ...boundArgs, ...args);
}对真实函数使用 .bind() 有点违反直觉,因为我们必须为 this 提供一个值。鉴于在函数调用期间它是 undefined,因此通常将其设置为 undefined 或 null。
在以下示例中,我们通过将 add() 的第一个参数绑定到 8 来创建一个带有一个参数的函数 add8()。
function add(x, y) {
return x + y;
}
const add8 = add.bind(undefined, 8);
assert.equal(add8(1), 9); 测验
参见测验应用程序。