2022 年 5 月 3 日

函数对象,NFE

我们已经知道,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 有两个特殊之处,这就是它的原因

  1. 它允许函数在内部引用自身。
  2. 它在函数外部不可见。

例如,如果未提供 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,因此使用外部变量。在调用时刻,外部 sayHinull

我们可以放入函数表达式的可选名称旨在解决此类问题。

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

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" 是函数本地的。它不是从外部获取的(并且在那里不可见)。该规范保证它将始终引用当前函数。

外部代码仍然有其变量 sayHiwelcome。而 func 是“内部函数名称”,是函数可靠地调用自身的方式。

函数声明没有这样的事情

此处描述的“内部名称”功能仅适用于函数表达式,不适用于函数声明。对于函数声明,没有添加“内部”名称的语法。

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

总结

函数是对象。

这里我们介绍了它们的属性

  • name – 函数名称。通常从函数定义中获取,但如果没有,JavaScript 会尝试从上下文中猜测它(例如赋值)。
  • length – 函数定义中的参数数量。不计算剩余参数。

如果函数被声明为函数表达式(不在主代码流中),并且它带有名称,则称之为命名函数表达式。名称可以在内部使用来引用自身,用于递归调用或类似操作。

此外,函数可以携带其他属性。许多著名的 JavaScript 库都充分利用了此功能。

它们创建“主”函数,并将许多其他“帮助”函数附加到它。例如,jQuery 库创建名为 $ 的函数。lodash 库创建名为 _ 的函数,然后向其添加 _.clone_.keyBy 和其他属性(当您想了解它们时,请参阅文档)。实际上,它们这样做是为了减少对全局空间的污染,以便单个库仅提供一个全局变量。这降低了命名冲突的可能性。

因此,函数可以自行完成有用的工作,也可以在属性中携带大量其他功能。

任务

重要性:5

修改 makeCounter() 的代码,以便计数器还可以减少和设置数字

  • counter() 应返回下一个数字(与之前相同)。
  • counter.set(value) 应将计数器设置为 value
  • counter.decrease() 应将计数器减少 1。

请参阅沙盒代码以了解完整的用法示例。

P.S. 您可以使用闭包或函数属性来保持当前计数。或者编写两个变体。

打开带有测试的沙盒。

该解决方案在局部变量中使用 count,但附加方法直接写入 counter。它们共享相同的外层词法环境,还可以访问当前 count

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

在沙盒中打开带有测试的解决方案。

重要性:2

编写函数 sum,其工作方式如下

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

P.S. 提示:您可能需要为函数设置自定义对象到基元的转换。

打开带有测试的沙盒。

  1. 无论如何,为了让整个过程正常工作,sum 的结果必须是函数。
  2. 该函数必须在调用之间记住当前值。
  3. 根据任务,该函数在用于 == 时必须成为数字。函数是对象,因此转换将按照 对象到原始转换 章节中所述进行,并且我们可以提供自己的返回数字的方法。

现在是代码

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

请注意,sum 函数实际上只工作一次。它返回函数 f

然后,在每次后续调用中,f 将其参数添加到和 currentSum 中,并返回自身。

f 的最后一行没有递归。

递归如下所示

function f(b) {
  currentSum += b;
  return f(); // <-- recursive call
}

而在我们的例子中,我们只返回函数,而不调用它

function f(b) {
  currentSum += b;
  return f; // <-- does not call itself, returns itself
}

这个 f 将在下一个调用中使用,再次返回自身,次数根据需要而定。然后,当用作数字或字符串时,toString 返回 currentSum。我们还可以在此处使用 Symbol.toPrimitivevalueOf 进行转换。

在沙盒中打开带有测试的解决方案。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行,请用 <pre> 标记将其包装起来,对于 10 行以上,请使用沙盒 (plnkrjsbincodepen…)