原文链接:http://javascript.info/function-object,translate with ❤️ by zhangbao.
我们已经知道,JavaScript 里面的函数是值。
JavaScript 中的任何值都是有类型的,那函数是什么类型呢?
在 JavaScript 中,函数是对象。
一种理解函数的比较好的方式是它是可调用的“操作对象”。我们不仅可以调用它们,也可以把它们当成对象看待:添加/删除属性,传递引用。
“name”属性
功能对象包含一些可用的属性。
例如,一个函数的名字可以通过它的“name”属性访问:
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
更有趣的是,分配名字的逻辑很智能。在赋值语句中,能将正确的名称分配给函数:
let sayHi = function() {
alert("Hi");
}
alert(sayHi.name); // sayHi (works!)
对于函数默认赋值(函数类型),同样有效:
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
这里没有魔法。有某些情况下,没有办法找到正确的名字。在这种情况下,name 属性是空的,比如这里:
// function created inside array
let arr = [function() {}];
alert( arr[0].name ); // ''(空字符串)
// 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
在这里我们可以看到,剩余参数没有被计算在内。
length 属性有时用于在执行其他函数时的函数内省。
例如,在下面的代码里,ask 函数接收一个要问的 question,和任意数量的要调用的 handler 函数。
一旦用户提供了他们的答案,函数就会调用处理程序。我们可以调用两种处理程序:
一个零参数函数,只有当用户给出一个正(true)的答案时才会调用。
一个带有参数的函数,在任何一种情况下都可以调用,并返回一个答案。
我们的想法是,对于正例(最常见的变体),我们有一个简单的、无参数的处理程序语法,但是也能够提供通用的处理程序。
为了正确地调用 hanlders,我们检查 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");
// 我们统计下执行了多少次
sayHi.counter++;
}
sayHi.counter = 0; // 初始值
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 直接作为函数属性存储,而不是在它的外部词法环境中去查找。
它比使用闭包好还是坏?
主要的区别在于,如果计数的值存在于外部变量中,那么外部代码就无法访问它。只有嵌套函数才能修改它。如果它被绑定到一个函数,那么这样的事情是可能的:
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” 名的目的是什么呢?
首先让我们注意,这仍然是一个函数表达式。添加“func”的名称后,并没有将其转为函数声明,因为它仍然是作为赋值表达式的一部分创建的。
添加这样的名称也不会破坏任何东西。
该函数仍然可以通过 sayHi() 访问到:
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
sayHi("John"); // Hello, John
关于这个名字有两个特别的地方:
允许函数在内部引用自己。
在函数外部是不可见的。
例如,下面的函数 sayHi 如果没有提供参数 who 的话,在内部会使用 func,带上参数“Guest”调用自己。
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // 使用 func 调用自己
}
};
sayHi(); // Hello, Guest
// 这样不 OK
func(); // Error, func is not defined (func 在函数之外是不可见的)
为什么我们使用 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"); // 现在一切都 OK 了
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Hello, Guest (嵌套调用 OK)
现在它起作用了,因为“func”这个名字是函数本地的。它不是从外部获取的(在那里是不可见的)。该规范保证了它总是引用当前函数。
外部代码仍然有变量 sayHi 或 welcome。func 是一个“内部函数名”,这是它为何能在内部调用自身的原因。
注意:函数声明中没有这样的东西
这里描述的“内部名称”特性只适用于函数表达式,而不是函数声明。对于函数声明,语法上没有可能性来添加一个“内部”名称。
有时,当我们需要一个可靠的内部名称时,这就是将函数声明重写为命名函数表达式形式的原因。
总结
函数是对象。
本章,我们覆盖讲述了它的这些属性:
name:函数名。不仅存在于函数定义中,而且也存在于赋值和对象属性中。
length:在函数定义中参数的个数,剩余参数不计算在内。
如果函数被声明为一个函数表达式(不是在主代码流中),并且它带有名称,那么它就被称为命名函数表达式。名称可以用于内部引用,用于递归调用或诸如此类的事情。
此外,函数还可以携带额外的属性。许多著名的 JavaScript 库都很好地利用了这个特性。
它们创建一个“主要”函数,并附加许多其他“辅助”函数。例如,jQuery 库创建一个名为 $ 的函数。lodash 库创建了一个 函数。然后添加了.clone、_.keyBy 和其他属性(当您想了解更多关于它们的信息时请参阅文档)。实际上,他们这样做是为了减少对全局空间的污染,这样一个库只提供一个全局变量。这减少了命名冲突的可能性。
因此,一个函数可以自己做一个有用的工作,并且还可以在属性中携带一些其他的功能。
(完)