函数(function)就是一段被封装的代码,允许反复调用。不仅如此,在JavaScript中,函数可以作为表达式参与运算,可以作为闭包存储信息,也可以作为类型构造实例等。JavaScript拥有函数式编程的很多特性,灵活使用函数,可以编写出功能强大、代码简洁、设计优雅的程序。
使用function关键字可以声明函数,具体语法格式如下:
function funName([args]){
statements
}
funName表示函数名,必须是合法的标识符。在函数名之后是由小括号包含的参数列表,参数之间以逗号分隔,参数是可选项,没有数量限制。
【示例】最简单的函数体是一个空函数,不包含任何代码:
function funName(){} //空函数
提示:var和function都是声明语句,它们声明的变量和函数在JavaScript预编译期被解析,这种现象被称为变量提升或函数提升,因此在代码的底部声明变量或函数,在代码的顶部也能够访问。在预编译期,JavaScript引擎会为每个function创建上下文运行环境,定义变量对象,同时把函数内所有私有变量作为属性注册到变量对象上。
使用Function()可以构造函数,具体语法格式如下:
var funName = new Function(p1, p2, ... , pn, body);
Function()的参数类型都是字符串,p1~pn表示所创建函数的参数列表,body表示所创建函数的函数体代码,在函数体内语句之间通过分号进行分隔。
【示例1】构造一个函数:求两个数的和。参数a和b用来接收用户输入的值,然后返回它们的和:
var f = new Function("a", "b", "return a+b"); //通过构造函数创建函数结构
在上面代码中,f就是所创建函数的名称。如果使用function语句可以设计相同结构的函数:
function f(a, b){ //使用function语句定义函数结构
return a + b;
}
【示例2】使用Function()构造函数可以不指定任何参数,表示创建一个空函数:
var f = new Function(); //定义空函数
【示例3】在Function()构造函数参数中,p1~pn表示参数列表,可以分开传递,也可以合并为一个字符串进行传递。下面三行代码定义的参数都是等价的:
var f = new Function("a", "b", "c", "return a+b+c")
var f = new Function("a, b, c", "return a+b+c")
var f = new Function("a,b", "c", "return a+b+c")
提示:使用Function()可以动态创建函数,这样可以把函数体作为一个字符串表达式进行设计,而不是作为一个程序结构,因此使用起来会更灵活。Function()的缺点如下:
因此,Function()构造函数不是很常用,也不推荐使用。
函数直接量也称为函数表达式、匿名函数,没有函数名,仅包含function关键字、参数列表和函数体。具体语法格式如下:
function([args]){
statements
}
【示例1】定义一个函数直接量:
function(a, b){ //函数直接量
return a + b;
}
在上面代码中,函数直接量与使用function语句定义函数结构基本相同,它们的结构都是固定的。但是函数直接量没有指定函数名。
【示例2】匿名函数可以作为一个表达式使用,也称为函数表达式,而不再是函数结构块。下面把匿名函数作为一个表达式,赋值给变量f:
var f = function(a, b){
return a + b;
};
当把函数作为一个表达式赋值给变量之后,变量就可以作为函数被调用:
console.log(f(1,2)); //返回数值3
【示例3】匿名函数可以直接参与表达式运算。下面把函数定义和调用合并在一起编写:
console.log( //把函数作为一个操作数进行调用
(function(a, b){
return a + b;
})(1,2)); //返回数值3
ECMAScript 6新增箭头函数,它是一种特殊结构的函数表达式,语法比function函数表达式更简洁,并且没有自己的this、arguments、super或new.target,不能用作构造函数,不能与new一起使用。语法格式如下:
(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
其中,param1, param2, …, paramN表示参数列表,statements表示函数内的语句块,expression表示函数内仅包含一个表达式,它相当于如下语法:
function (param1, param2, …, paramN) { return expression; }
当只有一个参数时,小括号是可选的:
(singleParam) => { statements } //正确
singleParam => { statements } //正确
没有参数时,需要使用空的小括号表示:
() => { statements }
【示例1】使用箭头函数定义一个求平方的函数:
var fn = x => x * x;
等价于:
var fn = function (x) {
return x * x;
}
【示例2】定义一个比较函数,比较两个参数,返回最大值:
var fn = (x,y) => {
if (x > y) {
return x;
} else {
return y;
}
}
调用函数有4种模式:常规调用、方法调用、动态调用、实例化调用。下面进行详细讲解。
在默认状态下,函数是不会被执行的。使用小括号(())可以执行函数,在小括号中可以包含零个或多个参数,参数之间通过逗号进行分隔。
【示例1】使用小括号调用函数,然后把返回值作为参数,再传递给f()函数,进行第二轮运算,这样可以节省两个临时变量:
function f(x,y){ //定义函数
return x*y; //返回值
}
console.log(f(f(5,6),f(7,8))); //返回1680。重复调用函数
【示例2】如果函数返回值为一个函数,则在调用时可以使用多个小括号反复调用:
function f(x, y){ //定义函数
return function(){ //返回函数类型的数据
return x * y;
}
}
console.log(f(7, 8)()); //返回值56,反复调用函数
在函数体内,使用return语句可以设置函数的返回值,一旦执行return语句,将停止函数的运行,并运算和返回return后面的表达式的值。如果函数不包含return语句,则执行完函数体内所有语句后,返回undefined值。
【示例1】函数的参数没有限制,但是返回值只能是一个,如果要输出多个值,可以通过数组或对象进行设计:
function f(){
var a = [];
a[0] = true;
a[1] = 123;
return a; //返回多个值
}
在上面代码中,函数返回值为数组,该数组包含两个元素,从而实现使用一个return语句,返回多个值的目的。
【示例2】在函数体内可以包含多个return语句,但是仅能执行一个return语句,因此在函数体内可以使用分支结构决定函数返回值:
function f(x, y){
//如果参数为非数字类型,则终止函数执行
if( typeof x != "number" || typeof y != "number") return;
//根据条件返回值
if(x > y) return x - y;
if(x < y) return y - x;
if(x * y <= 0) return x + y;
}
当一个函数被设置为对象的属性值时,称为方法。使用点语法可以调用一个方法。
【示例】创建一个obj对象,它有一个value属性和一个increment方法。increment方法接收一个可选的参数,如果该参数不是数字,则默认使用数字1:
var obj = {
value : 0,
increment : function(inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
}
obj.increment();
console.log(obj.value); //1
obj.increment(2);
console.log(obj.value); //3
使用点语法调用对象obj的方法increment,然后通过increment()函数改写value属性的值。在increment方法中可以使用this访问obj对象,然后使用obj.value方式读写value属性值。
call和apply是Function的原型方法,它们能够将特定函数当作一个方法绑定到指定对象上,并进行调用。具体语法格式如下:
function.call(thisobj, args...)
function.apply(thisobj, [args])
function表示要调用的函数。参数thisobj表示绑定对象,也就是将函数function体内的this动态绑定到thisobj对象上。参数args表示将传递给被函数的参数。
call只能接收多个参数列表,而apply只能接收一个数组或者伪类数组,数组元素将作为参数列表传递给被调用的函数。
【示例1】使用call动态调用函数f,并传入参数值3和4,返回两个值的和:
function f(x,y){ //定义求和函数
return x+y;
}
console.log( f.call(null, 3, 4)); //返回7
在上面示例中,f是一个简单的求和函数,通过call方法把函数f绑定到空对象null身上,以实现动态调用函数f,同时把参数3和4传递给函数f,返回值为7。实际上,f.call(null, 3, 4)等价于null.m(3,4)。
【示例2】示例1使用call调用,也可以使用apply方法调用函数f:
function f(x,y){ //定义求和函数
return x+y;
}
console.log( f.apply(null, [3, 4] )); //返回7
如果把一个数组或伪类数组的所有元素作为参数进行传递,使用apply方法就非常方便。
【示例3】使用apply方法设计一个求最大值的函数:
function max(){ //求最大值函数
var m = Number.NEGATIVE_INFINITY; //声明一个负无穷大的数值
for( var i = 0; i < arguments.length; i ++ ){ //遍历所有实参
if( arguments[i] > m ) //如果实参值大于变量m,
m = arguments[i]; //则把该实参值赋值给m
}
return m; //返回最大值
}
var a = [23, 45, 2, 46, 62, 45, 56, 63]; //声明并初始化数组
var m = max.apply( Object, a ); //动态调用max,绑定为Object的方法
console.log( m ); //返回63
在上面示例中,设计定义一个函数max(),用来计算所有参数中最大值参数。首先,通过apply方法,动态调用max()函数。其次,把它绑定为Object对象的一个方法,并把包含多个值的数组传递给它。最后,返回经过max()计算后的最大数组元素。在上面示例中,设计定义一个函数max(),用来计算所有参数中最大值参数。首先,通过apply方法,动态调用max()函数。其次,把它绑定为Object对象的一个方法,并把包含多个值的数组传递给它。最后,返回经过max()计算后的最大数组元素。
如果使用call方法,就需要把数组内所有元素全部读取出来,再逐一传递给call方法,显然这种做法不是很方便。
【示例4】可以动态调用Math的max()方法计算数组的最大值元素:
var a = [23, 45, 2, 46, 62, 45, 56, 63]; //声明并初始化数组
var m = Math.max.apply( Object, a ); //调用系统函数max
console.log( m ); //返回63
使用new命令可以实例化对象,在创建对象的过程中会运行函数。因此,使用new命令可以间接调用函数。
注意:使用new命令调用函数时,返回的是对象,而不是return的返回值。如果不需要返回值,或者return的返回值是对象,可以选用new间接调用函数。
【示例】使用new调用函数,把传入的参数值显示在控制台:
function f(x,y){ //定义函数
console.log("x = " + x + ", y = " + y );
}
new f(3, 4);
参数是函数对外联系的唯一入口,用户只能通过参数控制函数的运行。
函数的参数包括两种类型:
【示例1】定义JavaScript函数时,可以设置零个或多个参数:
function f(a,b){ //设置形参a和b
return a+b;
}
var x=1,y=2; //声明并初始化变量
console.log(f(x,y)); //调用函数并传递实参
在上面示例中,a、b就是形参,而在调用函数时向函数传递的变量x、y就是实参。
一般情况下,函数的形参和实参数量应该相同,但是JavaScript并没有要求形参和实参必须相同。在特殊情况下,函数的形参和实参数量可以不相同。
【示例2】如果函数实参数量少于形参数量,那么多出来的形参的值默认为undefined:
(function(a,b){ //定义函数,包含两个形参
console.log(typeof a); //返回number
console.log(typeof b); //返回undefined
})(1); //调用函数,传递一个实参
【示例3】如果函数实参数量多于形参数量,那么多出来的实参就不能够通过形参进行访问,函数会忽略多余的实参。下面函数的实参3和4就被忽略了:
(function(a,b){ //定义函数,包含两个形参
console.log(a); //返回1
console.log(b); //返回2
})(1,2,3,4); //调用函数,传入4个实参值
提示:ECMAScript 6开始支持默认参数,以前设置默认参数的方法如下。
function add(a , b) {
b = b || 1; //判断b是否为空,为空就给默认值1
}
现在可以设置:
function add(a , b=1) { //如果参数b为空,则使用默认值1
}
使用arguments对象的length属性可以获取函数的实参个数。arguments对象只能在函数体内可见,因此arguments.length只能在函数体内使用。
使用函数对象的length属性可以获取函数的形参个数,该属性为只读属性,在函数体内、体外都可以使用。
arguments对象表示函数的实参集合,仅能够在函数体内可见,并可以直接访问。
【示例1】函数没有定义形参,但是在函数体内通过arguments对象可以获取调用函数时传入的每一个实参值:
function f(){ //定义没有形参的函数
for(var i = 0; i < arguments.length; i ++ ){ //遍历arguments对象
console.log(arguments[i]); //显示指定下标的实参的值
}
}
f(3, 3, 6); //逐个显示每个传递的实参
注意:arguments对象是一个伪类数组,不能够继承Array的原型方法。可以使用数组下标的形式访问每个实参,如arguments[0]表示第一个实参,下标值从0开始,直到arguments.length-1。其中,length是arguments对象的属性,表示函数包含的实参个数。同时,arguments对象可以允许更新其包含的实参值。
【示例2】使用for循环遍历arguments对象,然后把循环变量的值传入arguments,以便改变实参值:
function f(){
for(var i = 0; i < arguments.length; i ++ ){ //遍历arguments对象
arguments[i] =i; //修改每个实参的值
console.log(arguments[i]); //提示修改的实参值
}
}
f(3, 3, 6); //返回提示0、1、2,而不是3、3、6
【示例3】通过修改length属性值,可以改变函数的实参个数。当length属性值增大时,则增加的实参值为undefined;如果length属性值减小,则会丢弃length长度值之后的实参值:
function f(){
arguments.length = 2 ; //修改arguments对象的length属性值
for(var i = 0; i < arguments.length; i ++ ){
console.log(arguments[i]);
}
}
f(3, 3, 6); //返回提示3、3
callee是arguments对象的属性,它引用当前arguments对象所属的函数。使用该属性可以在函数体内调用函数自身。在匿名函数中,callee属性比较有用,例如,利用它可以设计递归调用。
【示例】使用arguments.callee获取匿名函数,然后通过函数的length属性获取函数形参个数,最后比较实参个数与形参个数,以检测用户传递的参数是否符合要求:
function f(x, y, z){
var a = arguments.length; //获取函数实参的个数
var b = arguments.callee.length; //获取函数形参的个数
if (a != b){ //如果形参和实参个数不相等,则提示错误信息
throw new Error("传递的参数不匹配");
}
else{ //如果形参和实参数目相同,则返回它们的和
return x + y + z;
}
}
console.log(f(3, 4, 5)); //返回值为12
arguments.callee等价于函数名,在上面示例中,arguments.callee等于f。
ECMAScript 6新增剩余参数,它允许将不定数量的参数表示为一个数组。语法格式如下:
function(a, b, ...args) {
//函数体
}
如果函数最后一个形参以…为前缀,则它表示剩余参数,将传递的所有剩余的实参组成一个数组,传递给形参args。
提示:剩余参数与arguments对象之间的区别主要有如下三点:
【示例】利用剩余参数设计一个求和函数:
var fn = (x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
console.log( fn(5, 7, 6, 4, 7));
可以简写为:
var fn = (...rest) => {
var i, sum = 0;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
JavaScript支持全局作用域和局部作用域,局部作用域也称为函数作用域,局部变量在函数体内可见,因此也称为私有变量。
作用域(scope)表示变量的作用范围、可见区域,一般包括词法作用域和执行作用域。
注意:JavaScript支持词法作用域,JavaScript函数只能运行在被预先定义好的词法作用域里,而不是被执行的作用域里。因此,定义作用域实际上就是定义函数。
JavaScript作用域属于静态概念,根据词法结构确定,而不是根据执行确定。作用域链是JavaScript提供的一套解决函数内私有变量的访问机制。JavaScript规定每一个作用域都有一个与之相关联的作用域链。
作用域链用来在函数执行时,求出私有变量的值。该链中包含多个对象,在访问私有变量的过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与私有变量名称相同的属性。如果在作用域链的顶端(全局对象)中仍然没有找到同名的属性,则返回undefined。
【示例】通过多层嵌套的函数设计一个作用域链,在最内层函数中可以逐级访问外层函数的私有变量:
var a = 1; //全局变量
(function(){
var b = 2; //第1层局部变量
(function(){
var c = 3; //第2层局部变量
(function(){
var d = 4; //第3层局部变量
console.log(a+b+c+d); //返回10
})() //直接调用函数
})() //直接调用函数
})() //直接调用函数
在上面代码中,JavaScript引擎首先在最内层活动对象中查询属性a、b、c和d,其中只找到属性d并获得它的值(4),然后沿着作用域链,在上一层活动对象中继续查找属性a、b和c,其中找到属性c获得它的值(3),以此类推,直到找到所有需要的变量值为止,如下图所示:
在函数体内,一般包含以下类型的私有变量:
其中,this和arguments是系统内置标识符,不需要特别声明。这些标识符在函数体内的优先级为this→局部变量→形参→arguments→函数名,其中左侧优先级要大于右侧。
JavaScript函数的作用域是静态的,但是函数的调用是动态的。由于函数可以在不同的运行环境内被执行,因此JavaScript在函数体内内置了this关键字,用来获取当前的运行环境。this是一个指针型变量,它动态地引用当前的运行环境,具体说就是调用函数的对象。
闭包是高阶函数的重要特性,在函数式编程中起着重要作用。本节将介绍闭包的结构和基本用法。
闭包就是一个持续存在的函数上下文运行环境。典型的闭包体是一个嵌套结构的函数。内部函数引用外部函数的私有变量,同时内部函数又被外界引用,当外部函数被调用后,就形成闭包,这个函数也称为闭包函数。
【示例1】下面是一个典型的闭包结构:
function f(x){ //外部函数
return function(y){ //内部函数,通过返回内部函数,实现外部引用
return x + y; //访问外部函数的参数
};
}
var c = f(5); //调用外部函数,获取引用内部函数
console.log(c(6)); //调用内部函数,原外部函数的参数继续存在
【示例2】下面结构形式可以形成闭包,通过全局变量引用内部函数,实现内部函数对外开放:
var c; //声明全局变量
function f(x){ //外部函数
c = function(y){ //内部函数,通过向全局变量开放实现外部引用
return x + y; //访问外部函数的参数
};
}
f(5); //调用外部函数
console.log(c(6)); //使用全局变量c调用内部函数,返回11
【示例3】除了嵌套函数外,如果外部引用函数内部的私有数组或对象也容易形成闭包:
var add; //全局变量,定义访问闭包的通道
function f(){ //外部函数
var a = [1,2,3]; //私有变量,引用型数组
add = function(x){ //测试函数,对外开放
a[0] = x*x; //修改私有数组的元素值
}
return a; //返回私有数组的引用
}
var c = f();
console.log(c[0]); //读取闭包内数组,返回1
add(5); //测试修改数组
console.log(c[0]); //读取闭包内数组,返回25
add(10); //测试修改数组
console.log(c[0]); //读取闭包内数组,返回100
与函数相同,对象和数组是引用型数据。调用函数f,返回私有数组a的引用,即传址给全局变量c,而a是函数f的私有变量。当被调用后,活动对象继续存在,这样就形成闭包。
注意:这种特殊形式的闭包没有实际应用价值,因为它的功能单一,只能作为一个静态的、单向的闭包。而闭包函数可以设计各种复杂的运算表达式,它是函数式编程的基础。
如果返回的是一个简单的值,就无法形成闭包,值传递是直接复制。外部变量c得到的仅是一个值,而不是对函数内部变量的引用,这样当函数调用后,直接注销活动对象:
function f(x){ //外部函数
var a = 1; //私有变量,简单值
return a;
}
var c = f(5);
console.log(c); //仅是一个值,返回1
下面结合示例介绍闭包的简单使用,以加深对闭包的理解。
【示例1】使用闭包实现优雅的打包,定义存储器:
var f = function(){ //外部函数
var a = [] //私有数组初始化
return function(x){ //返回内部函数
a.push(x); //添加元素
return a; //返回私有数组
};
}(); //直接调用函数,生成执行环境
var a = f(1); //添加值
console.log(a); //返回1
var b = f(2); //添加值
console.log(b); //返回1,2
在上面示例中,通过外部函数设计一个闭包,定义一个永久的存储器。当调用外部函数并生成执行环境之后,就可以利用返回的匿名函数,不断地向闭包体内的数组a传入新值,传入的值会一直持续存在。
【示例2】在网页中,事件处理函数很容易形成闭包:
function f(){ //事件处理函数,闭包
var a = 1; //私有变量a,初始化为1
b = function(){ //开放私有函数
console.log( "a = " + a ); //读取a的值
}
c = function(){ //开放私有函数
a ++ ; //递增a的值
}
d = function( ){ //开放私有函数
a --; //递减a 的值
}
}
</script>
<button onclick="f()">生成闭包</button>
<button onclick="b()">查看a的值</button>
<button onclick="c()">递增</button>
<button onclick="d()">递减</button>
在浏览器中浏览时,首先单击“生成闭包”按钮,生成一个闭包。单击“查看a的值”按钮,可以随时查看闭包内私有变量a的值。单击“递增”“递减”按钮时,可以动态修改闭包内变量a的值,演示效果如下图所示: