函数对象,NFE

93 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情 我们已经知道,在 JavaScript 中,函数也是一个值。

而 JavaScript 中的每个值都有一种类型,那么函数是什么类型呢?

在 JavaScript 中,函数的类型是对象。

一个容易理解的方式是把函数想象成可被调用的“行为对象(action object)”。我们不仅可以调用它们,还能把它们当作对象来处理:增/删属性,按引用传递等。

属性 “name”

函数对象包含一些便于使用的属性。

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

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

更有趣的是,名称赋值的逻辑很智能。即使函数被创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值:

let sayHi = function() {
  alert("Hi");
};

alert(sayHi.name); // sayHi(有名字!)

当以默认值的方式完成了赋值时,它也有效:

function f(sayHi = function() {}) {
  alert(sayHi.name); // sayHi(生效了!)
}

f();

规范中把这种特性叫做「上下文命名」。如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。

对象方法也有名字:

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

这没有什么神奇的。有时会出现无法推测名字的情况。此时,属性 name 会是空,像这样:

// 函数是在数组中创建的
let arr = [function() {}];

alert( arr[0].name ); // <空字符串>
// 引擎无法设置正确的名字,所以没有值

而实际上,大多数函数都是有名字的。

属性 “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 有时在操作其它函数的函数中用于做 内省/运行时检查(introspection)

比如,下面的代码中函数 ask 接受一个询问答案的参数 question 和可能包含任意数量 handler 的参数 ...handlers

当用户提供了自己的答案后,函数会调用那些 handlers。我们可以传入两种 handlers

  • 一种是无参函数,它仅在用户给出肯定回答时被调用。
  • 一种是有参函数,它在两种情况都会被调用,并且返回一个答案。

为了正确地调用 handler,我们需要检查 handler.length 属性。

我们的想法是,我们用一个简单的无参数的 handler 语法来处理积极的回答(最常见的变体),但也要能够提供通用的 handler:

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// 对于肯定的回答,两个 handler 都会被调用
// 对于否定的回答,只有第二个 handler 被调用
ask("Question?", () => alert('You said yes'), result => alert(result));

这就是所谓的 多态性 的一个例子 —— 根据参数的类型,或者根据在我们的具体情景下的 length 来做不同的处理。这种思想在 JavaScript 的库里有应用。

自定义属性

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

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

function sayHi() {
  alert("Hi");

// 计算调用次数
  sayHi.counter++;
}
sayHi.counter = 0; // 初始值

sayHi(); // Hi
sayHi(); // Hi

alert( `Called ${sayHi.counter} times` ); // Called 2 times

属性不是变量