说起来ECMAScript中什么最有意思,用原书(《JavaScript高级程序设计》)作者的话说——莫过于函数了,有意思的根源在于函数实际上是对象。每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也就是一个指向函数对象的指针,不会与某个函数绑定。

函数的定义方式有三种:

  1. 函数声明
  2. 函数表达式
  3. 使用Function构造函数

函数声明的方式是比较常见的一种,如下面例子所示:

function sum(num1, num2){
  return num1 + num2;
}

这与下面使用函数表达式定义函数的方式几乎相差无几:

var sum = function(num1, num2){
  return num1 + num2;
};

上面的函数表达式语法定义了变量sum,并将其初始化为一个函数。有读者可能会注意到,function关键字后面没有函数名,这是因为在使用函数表达式定义函数的时候,没有必要使用函数名,通过变量sum即可以引用函数。另外,还要注意函数未尾有个分号,就像声明其它变量一样。

使用Function构造函数定义函数时,Function构造函数可以接收任意数量的参数,但最后一个参数始终都被看作是函数体,而前面的参数则枚举出了新函数的参数。来看下面的例子:

var sum = new Function('num1', 'num2', 'return num1 + num2');

从技术上讲,这也是一个函数表达式。但是,我们不推荐使用这种方式定义函数,因为这种函数定义方式会导致解析两次代码(第一次是解析常规ECMAScript代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过,这种方式对于理解“函数是对象,函数名是指针”来说倒是非常直观的。

由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字,如下例所示:

function sum(num1, num2){
  return num1 + num2;
}
console.log(sum(10, 10));// 20

var anotherSum = sum;
console.log(anotherSum(10, 10));// 20

sum = null;
console.log(anotherSum(10, 10));// 20

上面的代码首先定义了一个名为sum的函数,用于求两个数的和。然后,又声明了变量anotherSum,赋值为sum,此时anotherSumsum就指向了同一个函数,因此anotherSum()也正常返回了结果。即使切断sum与函数对象的引用关系,也不会影响anotherSum

函数名作为指针,也可以理解为什么ECMAScript中没有函数重载(函数名相同,参数列表不同)的概念。来看下面的示例:

function add(num){
  return 100 + num;
}
function add(num, num2){
  return 200 + num;
}
console.log(add(100));// 300

按照函数重载的概念,两个add函数的参数列表不同,当传入一个参数时应该调用第一个add,当传入两个参数时应该调用第二个add。但如上例所示,即使传入一个参数依然是调用第二个add,结果为300,这是为什么呢?

答案在函数名是指针,所以第二个add覆盖了第一个add,也许用函数表达式的写法更容易理解:

var add = function(num){
  return 100 + num;
};
var add = function(num, num2){
  return 200 + num;
};

如上例所示,重新声明add会覆盖第一个add。而JavaScript中的函数调用会根据实际传入的参数个数按顺序匹配参数列表,比如这里传入了一个100,那么add中的num就为100num2undefined,如果像这样调用add(100, 20),那么num2则为20

ECMAScript 2015引入了let关键字可以避免变量覆盖的问题,如下所示:

var add = function(num){
  return 100 + num;
};
//Uncaught SyntaxError: Identifier 'add' has already been declared
let add = function(num, num2){
  return 200 + num;
};

解析的时候已经报错,提示add已经声明,所以使用let会更安全一些。