接下来的部分将解释可调用值的含义。
JavaScript 有两类功能:
- 普通函数 可以扮演几个角色:
- 实际功能(在其他语言中,你只需使用术语“功能”;在 JavaScript 中,我们需要区分角色“真实功能”和可以扮演该角色的实体“普通功能”)
- 方法
- 构造函数
- 专用功能 只能扮演其中一个角色。例如:
- 箭头功能 只能是实际功能。
- 方法 只能是一种方法。
- 类 只能是构造函数。
21.1.普通功能
以下代码显示了三种方法(大致)相同的事情:创建一个普通的函数。
// 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
};
正如我们在 8.变量 中看到的,函数声明被提升,而变量声明(例如通过const
)则没有。我们将在本章后面探讨其后果。函数声明和函数表达式的语法非常相似。上下文决定哪个是哪个。有关这种语法歧义的更多信息,请参阅 4.语法 章节。
21.1.1 函数声明的一部分
让我们通过一个例子来检查函数声明的各个部分:
function add(x, y) {
return x + y;
}
add
是函数声明的 名称。add(x, y)
是函数声明的 头。x
和y
是 参数。- 花括号(
{
和}
)以及它们之间的所有东西都是函数声明的 体。 return
运算符显式返回函数的值。
21.1.2 普通功能的名称
函数表达式的名称只能在函数内部访问,函数可以使用它来引用自身(例如,用于自递归):
const func = function funcExpr() { return funcExpr };
assert.equal(func(), func);
// The name `funcExpr` only exists inside the function:
assert.throws(() => funcExpr, ReferenceError);
相反,函数声明的名称可在当前范围内访问:
function funcDecl() { return funcDecl }
// The name `funcDecl` exists inside the current scope
assert.equal(funcDecl(), funcDecl);
21.1.3 普通功能扮演的角色
请考虑上一节中的以下函数声明:
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);
- 构造函数/类:通过
new
调用。
(顺便说一句,类的名称通常以大写字母开头。)const inst = new add(); assert.equal(inst instanceof add, true);
21.2 专业功能
专业功能是普通功能的专用版本。他们每个人只扮演一个角色:
- 箭头函数 只能是一个真正的函数:
const arrow = () => { return 123 }; assert.equal(arrow(), 123);
- 方法 只能是一种方法:
const obj = { method() { return 'abc' } }; assert.equal(obj.method(), 'abc');
- 类 只能是构造函数:
class MyClass { /* ··· */ } const inst = new MyClass();
除了更好的语法之外,每种专用功能还支持新功能,使其在工作中比普通功能更好。
下表列出了普通和专用功能的功能。
普通功能 | 箭头功能 | 方法 | 类 | |
---|---|---|---|---|
函数调用 | ✔ |
✔ |
✔ |
✘ |
方法调用 | ✔ |
词汇this |
✔ |
✘ |
构造函数调用 | ✔ |
✘ |
✘ |
✔ |
21.2.1 专业功能仍然是功能
值得注意的是,箭头函数,方法和类仍然归类为函数:
> (() => {}) instanceof Function
true
> ({ method() {} }.method) instanceof Function
true
> (class SomeClass {}) instanceof Function
true
21.2.2 建议:更喜欢专门的功能
通常,您应该优先使用专用函数而不是普通函数,尤其是类和方法。但是,箭头功能和普通功能之间的选择不那么明确:
- 箭头函数没有
this
作为隐式参数。如果你使用真正的函数,这几乎总是你想要的,因为它避免了一个重要的this
相关的陷阱(详见 25.单实例对象 的章节)。 - 但是,我喜欢语法上的函数声明(它产生一个普通的函数)。如果你不在其中使用
this
,它大部分相当于const
加箭头功能:function funcDecl(x, y) { return x * y; } const arrowFunc = (x, y) => { return x * y; };
21.2.3 箭头功能
Arrow 函数被添加到 JavaScript 中有两个原因:
- 为创建函数提供更简洁的方法。
- 更容易使用实际函数:不能在普通函数中引用周围范围的
this
。
21.2.3.1 箭头函数的语法
让我们回顾一下匿名函数表达式的语法:
-
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 });
21.2.3.2 箭头功能:词汇this
普通函数既可以是方法,也可以是实际函数。唉,这两个角色是冲突的:
- 由于每个普通函数都可以是一个方法,因此它有自己的
this
。 - 自己的
this
使得无法从普通函数内部访问周围范围的this
。这对于真正的功能来说是不方便的。
以下代码演示了一种常见的解决方法:
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
的行为与任何其他变量一样。
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
x => this.prefix + x);
},
};
总结:
- 在普通函数中,
this
是隐式(动态)参数。 - 箭头函数从其周围的范围获得
this
(词法)。
21.2.3.3 语法陷阱:从箭头函数返回一个对象字面值
如果希望箭头函数的表达式主体是对象字面值,则必须将字面值放在括号中:
const func1 = () => ({a: 1});
assert.deepEqual(func1(), { a: 1 });
如果不这样,JavaScript 认为,箭头函数有一个块体(不返回任何内容):
const func2 = () => {a: 1};
assert.deepEqual(func2(), undefined);
{a: 1}
被解释为标签 a:
和表达式语句1
的块。这个陷阱是由语法模糊引起的:对象字面值和代码块具有相同的语法,我们必须帮助 JavaScript 区分它们。
21.3 吊装功能
函数声明是 悬挂(内部移动到顶部):
assert.equal(foo(), 123); // OK
function foo() { return 123; }
提升允许您在声明之前调用foo()
。变量声明不会被挂起:在以下示例中,您只能在声明后使用bar()
。
assert.throws(
() => bar(), // before declaration
ReferenceError);
const bar = () => { return 123; };
assert.equal(bar(), 123); // after declaration
类声明不会悬挂,也可以:
assert.throws(
() => new MyClass(),
ReferenceError);
class MyClass {}
assert.equal(new MyClass() instanceof MyClass, true);
21.3.1 在没有吊装的情况下提前召唤
注意函数f()
仍然可以在声明之前调用非提升函数g()
- 如果在声明g()
之后调用f()
:
const f = () => g();
const g = () => 123;
// We call f() after g() was declared:
assert.equal(f(), 123);
通常在执行模块的完整主体之后调用模块的功能。因此,您很少需要担心模块中的函数顺序。
21.3.2 吊装的陷阱
如果您在声明之前依赖于提升来调用函数,那么您需要注意它不能访问非提升数据。
hoistedFunc();
const MY_STR = 'abc';
function hoistedFunc() {
assert.throws(
() => MY_STR,
ReferenceError);
}
和以前一样,如果你在最后调用函数hoistedFunc()
,问题就会消失。
21.4 从函数返回值
使用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);
21.5 参数处理
21.5.1 术语:参数(parameters)与参数(arguments)
术语 参数 (parameters) 和术语 参数 (arguments)基本上意思相同。如果您愿意,可以进行以下区分:
- 参数 (parameters) 是函数定义的一部分。它们也被称为 形式参数 (formal parameters )和 形式参数 (formal arguments.)。
- 参数 (arguments) 是函数调用的一部分。它们也被称为 实际参数 actual parameters 和 实际参数 actual arguments.。
21.5.2 术语:回调
回调 或 回调函数 是作为参数传递给另一个函数或方法的函数。此术语经常在 JavaScript 社区中广泛使用。以下是回调的示例:
const myArray = ['a', 'b'];
const callback = (x) => console.log(x);
myArray.forEach(callback);
// Output:
// 'a'
// 'b'
21.5.3 参数太多或不够
如果函数调用提供的参数数量不同于函数定义所预期的数量,则 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]);
21.5.4 参数默认值
参数默认值指定在未提供参数时要使用的值。例如:
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]);
21.5.5 休息参数
通过在标识符前面添加三个点(...
)来声明 rest 参数。在函数或方法调用期间,它接收包含所有剩余参数的数组。如果最后没有额外的参数,那么它是一个空数组。例如:
function f(x, ...y) {
return [x, y];
}
assert.deepEqual(
f('a', 'b', 'c'),
['a', ['b', 'c']]);
assert.deepEqual(
f(),
[undefined, []]);
21.5.5.1 通过 rest 参数强制执行一定数量的参数
您可以使用 rest 参数来强制执行一定数量的参数。例如,以下功能。
function bar(a, b) {
// ···
}
这就是我们强制调用者总是提供两个参数的方法:
function bar(...args) {
if (args.length !== 2) {
throw new Error('Please provide exactly 2 arguments!');
}
const [a, b] = args;
// ···
}
21.5.6 命名参数
当有人调用函数时,调用者提供的参数将分配给被调用者接收的参数。执行映射的两种常用方法是:
- 位置参数:如果参数具有相同的位置,则将参数分配给参数。仅具有位置参数的函数调用如下所示。
selectEntries(3, 20, 2)
- 命名参数:如果参数具有相同的名称,则将参数分配给参数。 JavaScript 没有命名参数,但您可以模拟它们。例如,这是一个只有(模拟)命名参数的函数调用:
selectEntries({start: 3, end: 20, step: 2})
命名参数有几个好处:
- 它们导致更加自我解释的代码,因为每个参数都有一个描述性标签。只需比较
selectEntries()
的两个版本:使用第二个版本,可以更容易地看到发生了什么。 - 参数顺序无关紧要(只要名称正确)。
- 处理多个可选参数更方便:调用者可以轻松提供所有可选参数的任何子集,而不必知道它们省略的那些(使用位置参数,您必须使用
undefined
填写前面的可选参数])。
21.5.7 模拟命名参数
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 destructure property `start` of 'undefined' or 'nu
您可以通过为整个模式提供默认值来解决此问题。此默认值与更简单的参数定义的默认值相同:如果缺少该参数,则使用默认值。
function selectEntries({start=0, end=-1, step=1} = {}) {
return {start, end, step};
}
assert.deepEqual(
selectEntries(),
{ start: 0, end: -1, step: 1 });
21.5.8 将(...
)传播到函数调用中
spread 参数的前缀(...
)与 rest 参数的前缀相同。在调用函数或方法时使用前者。它的操作数必须是可迭代的对象。迭代的值转换为位置参数。例如:
function func(x, y) {
console.log(x);
console.log(y);
}
const someIterable = ['a', 'b'];
func(...someIterable);
// Output:
// 'a'
// 'b'
因此,扩展参数和 rest 参数用于相反的目的:
- 定义函数或方法时使用 Rest 参数。他们在数组中收集参数。
- 调用函数或方法时使用 Spread 参数。他们将可迭代对象转换为参数。
21.5.8.1 示例:传播到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
21.5.8.2 示例:传播到Array.prototype.push()
类似地,Array 方法.push()
破坏性地将其零个或多个参数添加到其 Array 的末尾。 JavaScript 没有破坏性地将数组附加到另一个数组的方法,但我们再次通过传播保存:
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];
arr1.push(...arr2);
assert.deepEqual(arr1, ['a', 'b', 'c', 'd']);
下一节:JavaScript 模块的当前环境非常多样化:ES6 带来了内置模块,但是它们之前的模块系统仍然存在。了解后者有助于理解前者,所以让我们进行调查。