我们已经知道,JavaScript 中的函数是一个值。
JavaScript 中的每个值都有一个类型。函数是什么类型?
在 JavaScript 中,函数是对象。
想象函数是一种可调用的“动作对象”是一个很好的方式。我们不仅可以调用它们,还可以将它们视为对象:添加/删除属性、按引用传递等。
“name”属性
函数对象包含一些可用的属性。
例如,函数的名称可作为“name”属性访问
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
有点儿有趣的是,名称分配逻辑很智能。它甚至会为没有名称的函数分配正确的名称,然后立即分配
let sayHi = function() {
alert("Hi");
};
alert(sayHi.name); // sayHi (there's a name!)
如果分配是通过默认值完成的,它也能正常工作
function f(sayHi = function() {}) {
alert(sayHi.name); // sayHi (works!)
}
f();
在规范中,此功能称为“上下文名称”。如果函数未提供名称,则在分配中会从上下文中找出名称。
对象方法也有名称
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
}
alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye
不过没有魔法。在某些情况下,无法找出正确的名称。在这种情况下,名称属性为空,如下所示
// function created inside array
let arr = [function() {}];
alert( arr[0].name ); // <empty string>
// the engine has no way to set up the right name, so there is none
然而,在实践中,大多数函数都有名称。
“length”属性
还有另一个内置属性“length”,它返回函数参数的数量,例如
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2
在这里,我们可以看到不计算 rest 参数。
length
属性有时用于对操作其他函数的函数进行自省。
例如,在下面的代码中,ask
函数接受一个要询问的question
和任意数量的要调用的handler
函数。
一旦用户提供他们的答案,该函数就会调用处理程序。我们可以传递两种类型的处理程序
- 零参数函数,仅在用户给出肯定答案时调用。
- 带参数的函数,无论哪种情况都会调用并返回答案。
要正确调用handler
,我们检查handler.length
属性。
我们的想法是,对于肯定的情况(最常见的变体),我们有一个简单的无参数处理程序语法,但也可以支持通用处理程序
function ask(question, ...handlers) {
let isYes = confirm(question);
for(let handler of handlers) {
if (handler.length == 0) {
if (isYes) handler();
} else {
handler(isYes);
}
}
}
// for positive answer, both handlers are called
// for negative answer, only the second one
ask("Question?", () => alert('You said yes'), result => alert(result));
这是所谓的多态性的一个特殊情况——根据参数的类型或在我们的情况下根据length
不同地处理参数。这个想法在 JavaScript 库中确实有用。
自定义属性
我们还可以添加我们自己的属性。
在这里,我们添加counter
属性来跟踪总调用次数
function sayHi() {
alert("Hi");
// let's count how many times we run
sayHi.counter++;
}
sayHi.counter = 0; // initial value
sayHi(); // Hi
sayHi(); // Hi
alert( `Called ${sayHi.counter} times` ); // Called 2 times
分配给函数的属性(如 sayHi.counter = 0
)不会在其中定义一个局部变量counter
。换句话说,属性counter
和变量let counter
是两个不相关的东西。
我们可以将函数视为一个对象,在其中存储属性,但这对其执行没有任何影响。变量不是函数属性,反之亦然。这些只是平行世界。
有时函数属性可以替换闭包。例如,我们可以重写章节 变量作用域、闭包 中的计数器函数示例,以使用函数属性
function makeCounter() {
// instead of:
// let count = 0
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
count
现在直接存储在函数中,而不是存储在外层词法环境中。
这比使用闭包更好还是更糟?
主要区别在于,如果count
的值存在于外部变量中,则外部代码无法访问它。只有嵌套函数可以修改它。如果它绑定到函数,那么这种事情是可能的
function makeCounter() {
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
counter.count = 10;
alert( counter() ); // 10
因此,实现的选择取决于我们的目标。
命名函数表达式
命名函数表达式,或 NFE,是一个术语,用于表示具有名称的函数表达式。
例如,我们采用一个普通的函数表达式
let sayHi = function(who) {
alert(`Hello, ${who}`);
};
并为其添加一个名称
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
我们在这里实现了什么?那个额外的 "func"
名称有什么目的?
首先让我们注意,我们仍然有一个函数表达式。在 function
后面添加名称 "func"
并未使其成为函数声明,因为它仍然作为一个赋值表达式的组成部分而创建。
添加这样的名称也不会破坏任何内容。
该函数仍然可用作 sayHi()
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
sayHi("John"); // Hello, John
名称 func
有两个特殊之处,这就是它的原因
- 它允许函数在内部引用自身。
- 它在函数外部不可见。
例如,如果未提供 who
,则下面的函数 sayHi
将再次使用 "Guest"
调用自身
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // use func to re-call itself
}
};
sayHi(); // Hello, Guest
// But this won't work:
func(); // Error, func is not defined (not visible outside of the function)
我们为什么使用 func
?也许只对嵌套调用使用 sayHi
?
事实上,在大多数情况下,我们都可以
let sayHi = function(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
sayHi("Guest");
}
};
该代码的问题在于 sayHi
可能会在外层代码中发生变化。如果函数被分配给另一个变量,则代码将开始出现错误
let sayHi = function(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
sayHi("Guest"); // Error: sayHi is not a function
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Error, the nested sayHi call doesn't work any more!
这是因为该函数从其外部词法环境中获取 sayHi
。没有本地 sayHi
,因此使用外部变量。在调用时刻,外部 sayHi
为 null
。
我们可以放入函数表达式的可选名称旨在解决此类问题。
我们用它来修复我们的代码
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // Now all fine
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Hello, Guest (nested call works)
现在它可以正常工作,因为名称 "func"
是函数本地的。它不是从外部获取的(并且在那里不可见)。该规范保证它将始终引用当前函数。
外部代码仍然有其变量 sayHi
或 welcome
。而 func
是“内部函数名称”,是函数可靠地调用自身的方式。
此处描述的“内部名称”功能仅适用于函数表达式,不适用于函数声明。对于函数声明,没有添加“内部”名称的语法。
有时,当我们需要一个可靠的内部名称时,这就是将函数声明重写为命名函数表达式形式的原因。
总结
函数是对象。
这里我们介绍了它们的属性
name
– 函数名称。通常从函数定义中获取,但如果没有,JavaScript 会尝试从上下文中猜测它(例如赋值)。length
– 函数定义中的参数数量。不计算剩余参数。
如果函数被声明为函数表达式(不在主代码流中),并且它带有名称,则称之为命名函数表达式。名称可以在内部使用来引用自身,用于递归调用或类似操作。
此外,函数可以携带其他属性。许多著名的 JavaScript 库都充分利用了此功能。
它们创建“主”函数,并将许多其他“帮助”函数附加到它。例如,jQuery 库创建名为 $
的函数。lodash 库创建名为 _
的函数,然后向其添加 _.clone
、_.keyBy
和其他属性(当您想了解它们时,请参阅文档)。实际上,它们这样做是为了减少对全局空间的污染,以便单个库仅提供一个全局变量。这降低了命名冲突的可能性。
因此,函数可以自行完成有用的工作,也可以在属性中携带大量其他功能。
评论
<code>
标记,对于多行,请用<pre>
标记将其包装起来,对于 10 行以上,请使用沙盒 (plnkr、jsbin、codepen…)