目录

🚆 JavaScript 函数对象

JavaScript 中函数也是一种对象类型。可以把 函数 理解为可以被调用的 行为对象(Action Object),不仅可以调用它们,还可以当作对象来处理:增 / 删属性、按引用传递等。

# 函数属性

# name 函数名称

函数对象中包含一些实用的属性。

🌰 例子 / 通过属性 name 访问函数的名字:

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

console.log(sayHi.name)
1
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

🌰 例子 / 无法推测出函数名字的情况,函数是在数组中创建的:

let arr = [function() {}];
console.log(arr[0].name) // ''
1
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
1
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));
1
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
1
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
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}`);
};
1
2
3

加了 func 名字后仍然是一个 函数表达式,在 function 后面加一个名字 func 没有使它成为一个 函数声明,因为它仍然是作为赋值表达式中的一部分被创建的。添加这个名字当然也没有打破任何东西。

函数仍然可以通过 sayHi 调用:

sayHi('Simon')
1

关于名字 func 有两个特殊的地方,这就是添加它的原因:

  1. 它允许函数在内部引用自己
  2. 它在函数外是不可见的

🌰 例子 / 当函数没有带参数允许调用自身:

let sayHi = function func(who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    func("Guest"); // 使用 func 再次调用函数自身
  }
};

sayHi() // Hello, Guest

func() // 在函数外不可见
1
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 了,所以使用外部变量。而当调用时,外部的 sayHinull

所以给函数表达式命名可以避免这个问题:

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 是函数局部域的。它不是从外部获取的(而且它对外部也是不可见的)。规范确保它只会引用当前函数。

外部代码仍然有该函数的 sayHiwelcome 变量。而且 func 是一个「内部函数名」,可用于函数在自身内部进行自调用。

注意

对于 函数声明 中没有添加「内部」名的语法。「内部名」特性只针对 函数表达式,而不是函数声明。当需要一个可靠的内部名时,就可以 把函数声明重写成函数表达式 了。

# 总结

  • 函数就是一个对象。
  • 函数有属性:
    • name函数的名字。通常取自函数定义,但如果函数定义时没设定函数名,JavaScript 会尝试通过函数的上下文猜一个函数名(例如把赋值的变量名取为函数名)。
    • length :函数定义时的 入参的个数。Rest 参数不参与计数。
  • 函数的自定义属性:函数可以带有额外的属性。很多知名的 JavaScript 库都充分利用了这个功能。
    • 可以创建创建一个「主」函数,然后给它附加很多其它「辅助」函数。jQuery (opens new window) 库创建了一个名为 $ 的函数。lodash (opens new window) 库创建一个 _ 函数,然后为其添加了 _.add_.keyBy 以及其它属性。
    • 实际上,它们这么做是为了减少对全局空间的污染,这样一个库就只会有一个全局变量。这样就降低了命名冲突的可能性。
  • 命名函数表达式 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
}
1
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

sum 函数只工作一次,它返回了函数 f 。然后,接下来的每一次子调用, f 都会把自己的参数加到求和 currentSum 上,然后 f 自身。

注意,f 的最后一行没有递归。 只是返回了函数自身,然后再按照需要才会被调用。

📢 上次更新: 2022/09/02, 10:18:16