原文链接:http://javascript.info/function-object,translate with ❤️ by zhangbao.

我们已经知道,JavaScript 里面的函数是值。

JavaScript 中的任何值都是有类型的,那函数是什么类型呢?

在 JavaScript 中,函数是对象。

一种理解函数的比较好的方式是它是可调用的“操作对象”。我们不仅可以调用它们,也可以把它们当成对象看待:添加/删除属性,传递引用。

“name”属性

功能对象包含一些可用的属性。

例如,一个函数的名字可以通过它的“name”属性访问:

  1. function sayHi() {
  2. alert("Hi");
  3. }
  4. alert(sayHi.name); // sayHi

更有趣的是,分配名字的逻辑很智能。在赋值语句中,能将正确的名称分配给函数:

  1. let sayHi = function() {
  2. alert("Hi");
  3. }
  4. alert(sayHi.name); // sayHi (works!)

对于函数默认赋值(函数类型),同样有效:

  1. function f(sayHi = function() {}) {
  2. alert(sayHi.name); // sayHi (works!)
  3. }
  4. f();

在规范中,这个特性被称为“上下文名称”。如果函数没有提供一个名称,那么在赋值中,它是从上下文中算出来的。

对象方法也是有名字的:

  1. let user = {
  2. sayHi() {
  3. // ...
  4. },
  5. sayBye: function() {
  6. // ...
  7. }
  8. }
  9. alert(user.sayHi.name); // sayHi
  10. alert(user.sayBye.name); // sayBye

这里没有魔法。有某些情况下,没有办法找到正确的名字。在这种情况下,name 属性是空的,比如这里:

  1. // function created inside array
  2. let arr = [function() {}];
  3. alert( arr[0].name ); // ''(空字符串)
  4. // the engine has no way to set up the right name, so there is none

然而,在实践中,大多数函数都有一个名称。

“length”属性

还有另一个内置属性“length”,它返回函数参数的数量,例如:

  1. function f1(a) {}
  2. function f2(a, b) {}
  3. function many(a, b, ...more) {}
  4. alert(f1.length); // 1
  5. alert(f2.length); // 2
  6. alert(many.length); // 2

在这里我们可以看到,剩余参数没有被计算在内。

length 属性有时用于在执行其他函数时的函数内省。

例如,在下面的代码里,ask 函数接收一个要问的 question,和任意数量的要调用的 handler 函数。

一旦用户提供了他们的答案,函数就会调用处理程序。我们可以调用两种处理程序:

  • 一个零参数函数,只有当用户给出一个正(true)的答案时才会调用。

  • 一个带有参数的函数,在任何一种情况下都可以调用,并返回一个答案。

我们的想法是,对于正例(最常见的变体),我们有一个简单的、无参数的处理程序语法,但是也能够提供通用的处理程序。

为了正确地调用 hanlders,我们检查 length 属性:

  1. function ask(question, ...handlers) {
  2. let isYes = confirm(question);
  3. for(let handler of handlers) {
  4. if (handler.length == 0) {
  5. if (isYes) handler();
  6. } else {
  7. handler(isYes);
  8. }
  9. }
  10. }
  11. // for positive answer, both handlers are called
  12. // for negative answer, only the second one
  13. ask("Question?", () => alert('You said yes'), result => alert(result));

有一个实际的用例,称之为多肽——根据不同的类型来处理不同的参数,在我们的情况下,取决于 length,这个想法确实在 JavaScript 库中有用处。

自定义属性

我们也可以添加我们自己的属性。

这里我们添加了 counter 属性来跟踪总的调用次数:

  1. function sayHi() {
  2. alert("Hi");
  3. // 我们统计下执行了多少次
  4. sayHi.counter++;
  5. }
  6. sayHi.counter = 0; // 初始值
  7. sayHi(); // Hi
  8. sayHi(); // Hi
  9. alert( `Called ${sayHi.counter} times` ); // Called 2 times

注意:属性不是变量

赋值给函数的属性,像 sayHi.counter = 0 不是说在函数里面定义了英特本地变量 counter。也就是说,属性 counter 和变量 let counter 是两个不相关的东西。

我们可以把一个函数看成一个对象,在里面储存属性,这对它的执行没有影响。变量从不使用函数属性,反之亦然,这些都是平行(不相干的意思)的词。

函数属性有时可以替代闭包。例如,我们可以使用函数属性,将闭包一章里计数器函数的例子进行重写:

  1. function makeCounter() {
  2. // instead of:
  3. // let count = 0
  4. function counter() {
  5. return counter.count++;
  6. };
  7. counter.count = 0;
  8. return counter;
  9. }
  10. let counter = makeCounter();
  11. alert( counter() ); // 0
  12. alert( counter() ); // 1

现在 count 直接作为函数属性存储,而不是在它的外部词法环境中去查找。

它比使用闭包好还是坏?

主要的区别在于,如果计数的值存在于外部变量中,那么外部代码就无法访问它。只有嵌套函数才能修改它。如果它被绑定到一个函数,那么这样的事情是可能的:

  1. function makeCounter() {
  2. function counter() {
  3. return counter.count++;
  4. };
  5. counter.count = 0;
  6. return counter;
  7. }
  8. let counter = makeCounter();
  9. counter.count = 10;
  10. alert( counter() ); // 10

因此,实施的选择取决于我们的目标。

命名函数表达式

命名函数表达式(或称为 NFE),是对有名字的函数表达式的一个术语。

例如,让我们举一个普通的函数表达式的例子:

  1. let sayHi = function(who) {
  2. alert(`Hello, ${who}`);
  3. };

给它加上一个名字:

  1. let sayHi = function func(who) {
  2. alert(`Hello, ${who}`);
  3. };

我们在这里取得了什么成就吗?添加额外 “func” 名的目的是什么呢?

首先让我们注意,这仍然是一个函数表达式。添加“func”的名称后,并没有将其转为函数声明,因为它仍然是作为赋值表达式的一部分创建的。

添加这样的名称也不会破坏任何东西。

该函数仍然可以通过 sayHi() 访问到:

  1. let sayHi = function func(who) {
  2. alert(`Hello, ${who}`);
  3. };
  4. sayHi("John"); // Hello, John

关于这个名字有两个特别的地方:

  1. 允许函数在内部引用自己。

  2. 在函数外部是不可见的。

例如,下面的函数 sayHi 如果没有提供参数 who 的话,在内部会使用 func,带上参数“Guest”调用自己。

  1. let sayHi = function func(who) {
  2. if (who) {
  3. alert(`Hello, ${who}`);
  4. } else {
  5. func("Guest"); // 使用 func 调用自己
  6. }
  7. };
  8. sayHi(); // Hello, Guest
  9. // 这样不 OK
  10. func(); // Error, func is not defined (func 在函数之外是不可见的)

为什么我们使用 func?也许只是使用 sayHi 来进行嵌套调用?

实际上,在大多数情况下我们可以:

  1. let sayHi = function(who) {
  2. if (who) {
  3. alert(`Hello, ${who}`);
  4. } else {
  5. sayHi("Guest");
  6. }
  7. };

这段代码的问题在于,sayHi 的值可能会改变。这个函数可能会传递给另一个变量,代码就会出现错误了:

  1. let sayHi = function(who) {
  2. if (who) {
  3. alert(`Hello, ${who}`);
  4. } else {
  5. sayHi("Guest"); // Error: sayHi is not a function
  6. }
  7. };
  8. let welcome = sayHi;
  9. sayHi = null;
  10. welcome(); // Error, the nested sayHi call doesn't work any more!

这是因为该函数从其外部词汇环境中获得了 sayHi,因为没有本地 sayHi,所以使用了外部变量。在调用的那一刻,外部 sayHi 是 null 了。

我们可以放入函数表达式的可选名称就能解决这类问题。

让我们用它来修复我们的代码:

  1. let sayHi = function func(who) {
  2. if (who) {
  3. alert(`Hello, ${who}`);
  4. } else {
  5. func("Guest"); // 现在一切都 OK 了
  6. }
  7. };
  8. let welcome = sayHi;
  9. sayHi = null;
  10. welcome(); // Hello, Guest (嵌套调用 OK)

现在它起作用了,因为“func”这个名字是函数本地的。它不是从外部获取的(在那里是不可见的)。该规范保证了它总是引用当前函数。

外部代码仍然有变量 sayHi 或 welcome。func 是一个“内部函数名”,这是它为何能在内部调用自身的原因。

注意:函数声明中没有这样的东西

这里描述的“内部名称”特性只适用于函数表达式,而不是函数声明。对于函数声明,语法上没有可能性来添加一个“内部”名称。

有时,当我们需要一个可靠的内部名称时,这就是将函数声明重写为命名函数表达式形式的原因。

总结

函数是对象。

本章,我们覆盖讲述了它的这些属性:

  • name:函数名。不仅存在于函数定义中,而且也存在于赋值和对象属性中。

  • length:在函数定义中参数的个数,剩余参数不计算在内。

如果函数被声明为一个函数表达式(不是在主代码流中),并且它带有名称,那么它就被称为命名函数表达式。名称可以用于内部引用,用于递归调用或诸如此类的事情。

此外,函数还可以携带额外的属性。许多著名的 JavaScript 库都很好地利用了这个特性。

它们创建一个“主要”函数,并附加许多其他“辅助”函数。例如,jQuery 库创建一个名为 $ 的函数。lodash 库创建了一个 函数。然后添加了.clone、_.keyBy 和其他属性(当您想了解更多关于它们的信息时请参阅文档)。实际上,他们这样做是为了减少对全局空间的污染,这样一个库只提供一个全局变量。这减少了命名冲突的可能性。

因此,一个函数可以自己做一个有用的工作,并且还可以在属性中携带一些其他的功能。

(完)