🚆 JavaScript 函数对象
JavaScript 中函数也是一种对象类型。可以把 函数 理解为可以被调用的 行为对象(Action Object),不仅可以调用它们,还可以当作对象来处理:增 / 删属性、按引用传递等。
# 函数属性
# name
函数名称
函数对象中包含一些实用的属性。
🌰 例子 / 通过属性 name
访问函数的名字:
function sayHi() {
alert("Hi");
}
console.log(sayHi.name)
2
3
4
5
对于函数的名字复制:即便函数创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值。
let sayHi = function() { alert("Hi"); }; console.log(sayHi.name) // sayHi
1
2
3
4
5以默认值方式完成赋值时也会自动赋予正确的函数名字:
function f(sayHi = function() {}) { console.log(sayHi.name); // sayHi }
1
2
3
4
这种函数命名方式为「上下文命名」。如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。
🌰 例子 / 对于对象中的函数也适用 上下文命名:
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
}
console.log(user.sayHi.name)
console.log(user.sayBye.name)
2
3
4
5
6
7
8
9
10
11
12
13
14
🌰 例子 / 无法推测出函数名字的情况,函数是在数组中创建的:
let arr = [function() {}];
console.log(arr[0].name) // ''
2
由于引擎无法设置正确的函数名字,所以没有值。
实际应用中,函数一般是由名字,方便调用。
# length
🌰 例子 / 函数的 length
属性返回函数入参的个数:
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
console.log(f1.length) // 1
console.log(f2.length) // 2
console.log(many.length) // 2
2
3
4
5
6
7
提示
可以在上面的例子看出,Rest 语法的参数不计入参数的个数。
🌰 例子 / 属性 length
有时在操作其它函数的函数中用于做 内省 / 运行时检查(introspection) (opens new window)。
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));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
多态性 (opens new window) —— 根据参数的类型,或者根据在具体情景下的
length
来做不同的处理。这种思想在 JavaScript 的库里有应用。
# 自定义属性
在函数对象中可以添加自己的属性。
🌰 例子:
function sayHi() {
console.log("Hi");
// 计算调用次数
sayHi.counter++;
}
sayHi.counter = 0;
sayHi(); // Hi
sayHi(); // Hi
console.log(`called ${sayHi.counter} times`) // called 2 times
2
3
4
5
6
7
8
9
10
11
注意
函数中的属性不是变量。被赋值给函数的属性,比如 sayHi.counter = 0
,不会 在函数内定义一个局部变量 counter
。换句话说,属性 counter
和变量 let counter
是毫不相关的两个东西。
可以把函数当作对象,在它里面存储属性,但是这对它的执行没有任何影响。变量不是函数属性,反之亦然。它们之间是平行的。
🌰 例子 / 函数对象中的属性可以用来替代 闭包:
function makeCounter() {
// let count = 0
function counter() {
return counter.count++
}
counter.count = 0
return counter
}
let counter = makeCounter()
console.log(counter()) // 0
console.log(counter()) // 1
2
3
4
5
6
7
8
9
10
11
12
13
14
现在
count
被直接存储在函数里,而不是它外部的词法环境。对比 闭包 和 函数属性:两者最大的不同就是如果
count
的值位于外层(函数)变量中,那么外部的代码无法访问到它,只有嵌套的函数可以修改它。而如果它是绑定到函数的,那么就很容易:let counter = makeCounter(); counter.count = 10; console.log(counter()); // 10
1
2
3
4所以选择哪一种方法依照实际情况选择。
# 命名函数表达式
命名函数表达式(NFE,Named Function Expression):指 带有名字 的函数表达式的术语。
🌰 例子:
let sayHi = function func(who) {
console.log(`Hello, ${who}`);
};
2
3
加了
func
名字后仍然是一个 函数表达式,在function
后面加一个名字func
没有使它成为一个 函数声明,因为它仍然是作为赋值表达式中的一部分被创建的。添加这个名字当然也没有打破任何东西。
函数仍然可以通过 sayHi
调用:
sayHi('Simon')
关于名字 func
有两个特殊的地方,这就是添加它的原因:
- 它允许函数在内部引用自己。
- 它在函数外是不可见的。
🌰 例子 / 当函数没有带参数允许调用自身:
let sayHi = function func(who) {
if (who) {
console.log(`Hello, ${who}`);
} else {
func("Guest"); // 使用 func 再次调用函数自身
}
};
sayHi() // Hello, Guest
func() // 在函数外不可见
2
3
4
5
6
7
8
9
10
11
如果仅仅使用
sayHi
的命名嵌套调用:let sayHi = function(who) { if (who) { console.log(`Hello, ${who}`); } else { sayHi("Guest"); } };
1
2
3
4
5
6
7当函数的命名更换后:
let welcome = sayHi sayHi = null welcome() // Error
1
2
3
4会报错,因为该函数从它的外部词法环境获取
sayHi
。没有局部的sayHi
了,所以使用外部变量。而当调用时,外部的sayHi
是null
。所以给函数表达式命名可以避免这个问题:
let sayHi = function func(who) { if (who) { console.log(`Hello, ${who}`); } else { func("Guest"); // 现在一切正常 } }; let welcome = sayHi; sayHi = null; welcome();
1
2
3
4
5
6
7
8
9
10
11
12此时可用了,名字
func
是函数局部域的。它不是从外部获取的(而且它对外部也是不可见的)。规范确保它只会引用当前函数。外部代码仍然有该函数的
sayHi
或welcome
变量。而且func
是一个「内部函数名」,可用于函数在自身内部进行自调用。
注意
对于 函数声明 中没有添加「内部」名的语法。「内部名」特性只针对 函数表达式,而不是函数声明。当需要一个可靠的内部名时,就可以 把函数声明重写成函数表达式 了。
# 总结
- 函数就是一个对象。
- 函数有属性:
name
:函数的名字。通常取自函数定义,但如果函数定义时没设定函数名,JavaScript 会尝试通过函数的上下文猜一个函数名(例如把赋值的变量名取为函数名)。length
:函数定义时的 入参的个数。Rest 参数不参与计数。
- 函数的自定义属性:函数可以带有额外的属性。很多知名的 JavaScript 库都充分利用了这个功能。
- 可以创建创建一个「主」函数,然后给它附加很多其它「辅助」函数。jQuery (opens new window) 库创建了一个名为
$
的函数。lodash (opens new window) 库创建一个_
函数,然后为其添加了_.add
、_.keyBy
以及其它属性。 - 实际上,它们这么做是为了减少对全局空间的污染,这样一个库就只会有一个全局变量。这样就降低了命名冲突的可能性。
- 可以创建创建一个「主」函数,然后给它附加很多其它「辅助」函数。jQuery (opens new window) 库创建了一个名为
- 命名函数表达式 NFE :函数是通过函数表达式的形式被声明的(不是在主代码流里),并且附带了名字。这个名字可以用于该函数内部自我调用,并且外部不可见。
# 实例
# 利用函数属性给函数提供更多的方法
修改
makeCounter()
代码,使得 counter 可以进行减一和设置值的操作:
counter()
应该返回下一个数字。counter.set(value)
应该将count
设置为value
。counter.decrease()
应该把count
减 1。
点击查看
function makeCounter() {
let count = 0
function counter() {
return count++
}
counter.set = (value) => count = value
counter.decrease = () => count--
return counter
}
2
3
4
5
6
7
8
9
10
11
12
在局部变量中使用 count
,而进行加法操作的方法是直接写在 counter
中的。它们共享同一个外部词法环境,并且可以访问当前的 count
。
# 创建自定义对象任意数量括号求和
写一个函数
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
1
2
3
4
5
点击查看
function sum(a){
let currentSum = a
function f(b) {
currentSum += b
return f
}
f.toString() = function () {
return currentSum
}
return f
}
2
3
4
5
6
7
8
9
10
11
12
13
14
sum
函数只工作一次,它返回了函数 f
。然后,接下来的每一次子调用, f
都会把自己的参数加到求和 currentSum
上,然后 f
自身。
注意,在 f
的最后一行没有递归。 只是返回了函数自身,然后再按照需要才会被调用。