函数是一段JS代码,它只定义一次,但可能被执行或调用任意次。你可能已经从诸如子例程(subroutine)或过程(procedure)这些名字里对函数的概念有所了解。

JS函数是参数化的,函数的定义会包括一个称为形参(parameter)的标识符列表,这些参数在函数体总像局部变量一样工作。JS函数调用会为形参提供实参(argument)的值,函数使用实参的值来计算返回值,成为函数调用表达式的值。

参数分为形参(parameter)和实参(argument)的区别,形参相当于函数中定义的变量,实参是在运行时的函数调用时传入的参数。

除了实参外,每次调用函数还会拥有另一个值,即本次调用的上下文,这就是 this 关键字的值。如果函数挂载在一个对象上,作为对象的一个属性,就称它为对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文(context),也就是该函数的 this 的值。

在JS中,函数即对象,程序可随意操控他们。比如JS可把函数赋值给变量,或者作为参数传递给其他函数。因为函数就是对象,所以可以给它们设置属性,甚至调用他们的方法。

JS的函数可嵌套在它函数中定义,这样它们 就可以访问被定义时所处的作用域中的任何变量。这意味着JS函数构成了一个闭包(closure),它给JS带啦了非常强劲的编程能力。

用于初始化一个新创建的对象的函数称之为构造函数(constructor)

函数定义

函数对任何语言来说都是一个核心的概念,通过函数可以封装任意多条语句,可在任何地方任何时候调用。

函数使用 function 关键来定义,它可以用在函数定义表达式函数声明语句中。

函数声明语句并非真正的语句,ECMAScript规范只是允许它们作为顶级语句。它们可以出现在全局代码里,或内嵌在其他函数中,但它们不能出现在循环、条件判断、try/catch/finally、with语句中。

注意,此限制仅适用于语句声明形式定义的函数,函数定义表达式可以出现在JS代码的任何地方。

函数声明语句

函数提升 函数声明语句“被提前”到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被在它定义之前出现的代码所调用。不过,以表达式定义的函数就另当别论了,为了调用一个函数,必须要能引用它,而要使用一个以表达式方式定义的函数之前,必须把它赋值给一个变量。变量的声明提前了,但给变量赋值是不会提前的。所以,以表达式方式定义的函数在定义前无法调用。

// 函数通过函数名来调用
log('hello');// 函数提升

// ECMAScript中的函数使用`function`关键字来声明,后跟一组参数以及函数体。
function log(msg){
  console.log(msg);
}

大多数函数包含一条 return 语句, return 语句导致函数停止执行,并返回它的表达式的值给调用者。如果 return 语句没有一个与之相关的表达式,则它返回 undefined 值。如果一个函数不包含 return 语句,那它就只执行函数体中的每条语句,并返回 undefined 值给调用者。

// 计算两个笛卡尔坐标之间的距离
function distance(x1,y1, x2,y2){
  var dx = x2 - x1;
  var dy = y2 - y1;
  return Math.sqrt(dx*dx + dy*dy);
}
// 计算阶乘的递归函数
function factorial(n){
  if(n <= 1){
    return 1;
  }
  return n * factorial(n-1);
}

函数定义表达式

以表达式方式定义的函数,函数名称是可选的。一条函数声明语句实际上声明了一个变量,并把一个函数对象赋值给它。

// 函数表达式定义函数后赋值给变量
var square = function(n){
  return n * n;
}

相对而言,定义函数表达式时并没有声明一个变量。函数可以命名,如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量

// 函数表达式可包含函数名,这在递归时很有用。
var fn = function fact(n){
  if(n <= 1){
    return 1;
  }else{
    return n * fact(n-1);
  }
}

通常而言,以表达式方式定义函数时不需要名称,这会让定义它们的代码更为紧凑。函数定义表达式特别适合用来定义那些只会用到一次的函数

// 函数表达式作为参数传递给其他函数
arr.sort(function(a, b){
  return a-b;
});
// 函数表达式可定义后立即执行
var ten_squared = (function(n){
  return n  * n;
}(10));

函数返回值

  • 函数在定义时不必指定是否具有返回值
  • 函数可包含多个 return 语句
  • 函数的 return 语句也可以不带任何返回值
// ECMAScript中的函数在定义时不必指定是否具有返回值
function sum(num1, num2){
  // 实际上,任何函数在任何收都可以通过 return 语句后跟要返回的值来实现返回值。
  return sum1+sum2;// 函数在执行完 return 语句后停止并立即退出
  // 位于 return 语句之后的任何代码永远不会执行
}
var result = sum(1,2);// 3
// 一个函数可包含多个 return 语句
function diff(num1, num2){
  if(num1 < num2){
    return num2-num1;
  }else{
    return num1-num2;
  }
}
// 函数的 return 语句也可以不带任何返回值
// 这种情况下,函数在停止执行后将返回 undefined 值。
// 这种用法用在需要提前停止函数执行而又不需要返回值的情况。
function say(name, msg){
  return ;
  console.log(name,msg);// 永远不会执行
}

推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如果函数有时候返回值,有时候不返回值,会给调试代码带来不便。

在JS的严格模式下(use strict)对函数有一些限制:

  • 不能将函数命名为 evalarguments
  • 不能将函数参数命名命名为 evalarguments
  • 不能出现两个命名参数同名的情况

如果发生以上情况,会导致语法错误,代码无法执行。

嵌套函数

JS中,函数可以嵌套在其他函数中,嵌套函数的有趣之处在于它的变量作用域规则:它们可以访问嵌套它们(或多重嵌套)的函数的参数和变量。

// 求三角形斜边
function hypotenuse(a,b){
  function square(n){
    return n  * n;
  }
  return Math.sqrt(square(a) + square(b));
}

理解函数参数

ECMAScript函数的参数与大多数其他语言中函数的参数有所不同。

ECMAScript函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可传递一个、两个、三个、多个,甚至不传入参数,而JS解析器永远不会有什么怨言。

之所以会这样,原因是ECMAScript中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关系数组中包含哪些参数。实际上,在函数体内可通过 arguments对象来访问这个参数数组,从而获取传递给函数的每个函数。

其实, arguments对象只是与数组类似,它并不是 Array 类的实例,也被称为 “伪数组”。arguments可以使用方括号语法访问它的每个元素,使用length属性来确定传递进来多少个参数。

function log(){
  // 不显式地使用命名参数
  console.log(arguments.length); // arguments对象的length属性可获知参数传递的个数
  console.log(arguments[0], arguments[1]);// 是通过访问伪数组arguments对象的下标来获取传递的参数
}

这个事实说明了ECMAScript函数的一个重要特点:“命名的参数只提供便利但不是必需的”。另外,在命名参数方面,其他语言可能需要实现创建一个函数签名,而将来的调用必须与该签名一致。但在ECMAScript中,没有这些条条框框,JS解析器不会验证命名参数

另一个与参数相关的重要点是,arguments对象可与命名参数一起使用。

function doAdd(num1, num2){
  if(arguments.length==1){
    console.log(num1+1);
  }else{
    console.log(arguments[0] + num2); 
  }
}

关于 arguments 的行为,还有一点比较有意思,那就是 arguments 的值永远与对应命名参数的值保持同步。

function doAdd(num1, num2){
  arguments[1] = 10; // arguments对象中的值会自动反映到对应的命名参数
  console.log(arguments[0] + num2);
}

arguments 对象中的值会自动反映到对应的命名参数。不过,这并不是说读取这两个值会访问相同的内存空间。它们的内存空间是独立的,但它们的值会同步。这种影响是单向的,即修改命名参数不会改变 arguments 中对应的值。

另外还要记住,如果只传入一个参数,那么为 arguments[1] 设置的值不会反应到命名参数中。这是因为 arguments 对象的长度是由传入的参数个数所决定的,不是由定义函数时的命名参数的个数决定给的

关于参数还要记住,没有传递值的命名参数将自动被赋予 undefined 值,这就跟定义变量但又没有初始化一样。

严格模式对如何使用 arguments 对象作出了一些限制:

  • 重写 arguments 的值会导致语法错误,代码将不会执行。
  • arguments 对象属性赋值将变得无效

ECMAScript中的所有参数传递的都是值,不可能通过引用传递参数。

没有重载

ECMAScript函数不能像传统意义上那样实现重载,在其他语言如Java中,可以为一个函数编写两个定义,只要这两个定义的签名(接收的参数的类型和数量)不同即可。

ECMAScript函数没有签名,因为其参数是由包含零或多个值的伪数组来表示的。所以没有函数签名,真正的重载是不可能做到的。

如果在ECMAScript中定义多个名字相同的函数,则该名字只属于后定义的函数。

如前所述,通过检查传入函数中参数的类型和数量并作出不同的反应,可以模仿方法的重载。

小结

  • ECMAScript中的函数与其他语言中的函数有诸多不同之处
  • ECMAScript函数无需指定返回值,因为任何ECMAScript函数都可以在任何时候返回任意值。
  • 未指定返回值的函数返回的是一个特殊的 undefined
  • ECMAScript中没有函数签名的概念,因为其函数参数是以一个包含零个或多个值的伪数组的形式传递的。
  • 可以向ECMAScript函数传递任意数量的参数,通过 arguments 对象来访问这些参数。
  • 由于不存在函数签名的特性,ECMAScript函数不能重载。