函数的实参和形参、作为值的函数

函数的实参和形参

JavaScript中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。实际上,JavaScript函数调用甚至不检查传入形参的个数。

  • 可选形参

当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。因此在调用函数时形参是否可选以及是否可以省略应当保持较好的适应性。为了做到这一点,应当给省略的参数赋一个合理的默认值,来看这个例子:

//将对象o中可枚举的属性名追加至数组a中,并返回这个数组a
//如果省略a,则创建一个新数组并返回这个新数组
function getPropertyNames(o,/*optional*/a){
    if(a===undefined)a=[];//如果未定义,则使用新数组
    for(var property in o)a.push(property);
    return a;
}

//这个函数调用可以传入1个或2个实参
var a=getPropertyNames(o);//将o的属性存储到一个新数组中
getPropertyNames(p,a);//将p的属性追加至数组a中

如果在第一行代码中不使用if语句,可以使用“||”运算符,这是一种习惯用法:

a=a||[];

回忆一下,“||”运算符,如果第一个实参是真值的话就返回第一个实参;否则返回第二个实参。在这个场景下,如果作为第二个实参传入任意对象,那么函数就会使用这个对象。如果省略掉第二个实参(或者传递null以及其他任何假值),那么就新创建一个空数组,并赋值给a。

需要注意的是,当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。那些调用你的函数的程序员是没办法省略第一个实参并传入第二个实参的,它必须将undefined作为第一个实参显式传入。同样注意在函数定义中使用注释/*optional*/来强调形参是可选的。

  • 可变长的实参列表:实参对象

当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有办法直接获得未命名值的引用。参数对象解决了这个问题。在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传入函数的实参值,而不用非要通过名字来得到实参。

假设定义了函数f,它的实参只有一个x。如果调用这个函数时传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]来得到。第二个实参只能通过arguments[1]来得到。此外,和真正的数组一样,arguments也包含一个length属性,用以标识其所包含元素的个数。因此,如果调用函数f()时传入两个参数,arguments.length的值就是2。

实参对象在很多地方都非常有用,下面的例子展示了使用它来验证实参的个数,从而调用正确的逻辑,因为JavaScript本身不会这么做:

function f(x,y,z)
{
//首先,验证传入实参的个数是否正确
    if(arguments.length!=3){
        throw new Error("function f called with"+arguments.length+
            "arguments,but it expects 3 arguments.");
    }
//再执行函数的其他逻辑...
}

需要注意的是,通常不必像这样检查实参个数。大多数情况下JavaScript的默认行为是可以满足需要的:省略的实参都将是undefined,多出的参数会自动省略。

实参对象有一个重要的用处,就是让函数可以操作任意数量的实参。下面的函数就可以接收任意数量的实参,并返回传入实参的最大值(内置函数Max.max()的功能与之类似):

function max(/*...*/){
    var max=Number.NEGATIVE_INFINITY;//遍历实参,查找并记住最大值
    for(var i=0;i<arguments.length;i++)
    if(arguments[i]>max)max=arguments[i];//返回最大值
    return max;
}
var largest=max(1,10,100,2,3,1000,4,5,10000,6);//=>10000

类似这种函数可以接收任意个数的实参,这种函数也称为“不定实参函数”(varargs function)。

注意,不定实参函数的实参个数不能为零,arguments[]对象最适合的应用场景是在这样一类函数中,这类函数包含固定个数的命名和必需参数,以及随后个数不定的可选实参。

记住,arguments并不是真正的数组,它是一个实参对象。每个实参对象都包含以数字为索引的一组元素以及length属性,但它毕竟不是真正的数组。可以这样理解,它是一个对象,只是碰巧具有以数字为索引的属性。

数组对象包含一个非同寻常的特性。在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参的别名,实参对象中以数字索引,并且形参名称可以认为是相同变量的不同命名。通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值,下面这个例子清楚地说明了这一点:

function f(x){
    console.log(x);//输出实参的初始值
    arguments[0]=null;//修改实参数组的元素同样会修改x的值
    console.log(x);//输出"null"
}

如果实参对象是一个普遍数组的话,第二条console.log(x)语句的结果绝对不会是null,在这个例子中,arguments[0]和x指代同一个值,修改其中一个的值会影响到另一个。

在ECMAScript 5中移除了实参对象的这个特殊特性。在严格模式下还有一点(和非严格模式下相比的)不同,在非严格模式中,函数里的arguments仅仅是一个标识符,在严格模式中,它变成了一个保留字。严格模式中的函数无法使用arguments作为形参名或局部变量名,也不能给arguments赋值。

除了数组元素,实参对象还定义了callee和caller属性。在ECMAScript 5严格模式中,对这两个属性的读写操作都会产生一个类型错误。而在非严格模式下,ECMAScript标准规范规定callee属性指代当前正在执行的函数。caller是非标准的,但大多数浏览器都实现了这个属性,它指代调用当前正在执行的函数的函数。通过caller属性可以访问调用栈。callee属性在某些时候会非常有用,比如在匿名函数中通过callee来递归地调用自身。

function f(x){
    console.log(x);//输出实参的初始值
    arguments[0]=null;//修改实参数组的元素同样会修改x的值
    console.log(x);//输出"null"
}
  • 将对象属性用做实参

当一个函数包含超过三个形参时,对于程序员来说,要记住调用函数中实参的正确顺序实在让人头疼。每次调用这个函数时都要不厌其烦地查阅文档,为了不让程序员每次都翻阅手册这么麻烦,最好通过名/值对的形式来传入参数,这样参数的顺序就无关紧要了。为了实现这种风格的方法调用,定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值对是真正需要的实参数据。下面的代码就展示了这种风格的函数调用,这种写法允许在函数中设置省略参数的默认值:

//将原始数组的length元素复制至目标数组
//开始复制原始数组的from_start元素
//并且将其复制至目标数组的to_start中
//要记住实参的顺序并不容易

function arraycopy(/*array*/from,/*index*/from_start,/*array*/to,/*index*/to_start,
                   /*integer*/length)
{
//逻辑代码
}
//这个版本的实现效率稍微有些低,但你不必再去记住实参的顺序
//并且from_start和to_start都默认为0
function easycopy(args){
    arraycopy(args.from,
        args.from_start||0,//注意这里设置了默认值
        args.to,
        args.to_start||0,args.length);
}

//来看如何调用easycopy()
var a=[1,2,3,4],b=[];
easycopy({from:a,to:b,length:4});
  • 实参类型

JavaScript方法的形参并未声明类型,在形参传入函数体之前也未做任何类型检查。可以采用语义化的单词来给函数实参命名,或者像刚才的示例代码中的arraycopy()方法一样给实参补充注释,以此使代码自文档化,对于可选的实参来说,可以在注释中补充一下“这个实参是可选的”。当一个方法可以接收任意数量的实参时,可以使用省略号:

function max(/*number...*/){/*代码区*/}
JavaScript在必要的时候会进行类型转换。因此如果函数期望接收一个字符串实参,而调用函数时传入其他类型的值,所传入的值会在函数体内将其用做字符串的地方转换为字符串类型。所有的原始类型都可以转换为字符串,所有的对象都包含toString()方法(尽管不一定有用),所以这种场景下是不会有任何错误的。

然而事情不总是这样,回头看一下刚才提到的arraycopy()方法。这个方法期望它的第一个实参是一个数组。当传入一个非数组的值作为第一个实参时(通常会传入类数组对象),尽管看起来是没问题的,实际上会出错。除非所写的函数是只用到一两次的“用完即丢”函数,你应当添加类似的实参类型检查逻辑,因为宁愿程序在传入非法值时报错,也不愿非法值导致程序在执行时报错,相比而言,逻辑执行时的报错消息不清晰且更难处理。下面这个例子中的函数就做了这种类型检查。注意这里使用了isArrayLike()函数:

//返回数组(或类数组对象)a的元素的累加和
//数组a中必须为数字、null和undefined的元素都将忽略
function sum(a){
    if(isArrayLike(a)){
        var total=0;
        for(var i=0;i<a.length;i++){//遍历所有元素
            var element=a[i];
            if(element==null)continue;//跳过null和undefined
            if(isFinite(element))total+=element;
            else throw new Error("sum():elements must be finite numbers");
        }
        return total;
    }
    else throw new Error("sum():argument must be array-like");
}

这里的sum()方法进行了非常严格的实参检查,当传入非法的值时会给出容易看懂的错误提示信息。但当涉及类数组对象和真正的数组(不考虑数组元素是否是null还是undefined),这种做法带来的灵活性其实并不大。

JavaScript是一种非常灵活的弱类型语言,有时适合编写实参类型和实参个数的不确定性的函数。接下来的flexisum()方法就是这样(可能走向了一个极端)。比如,它可以接收任意数量的实参,并可以递归地处理实参是数组的情况,这样的话,它就可以用做不定实参函数或者实参是数组的函数。此外,这个方法尽可能的在抛出异常之前将非数字转换为数字:

function flexisum(a){
    var total=0;
    for(var i=0;i<arguments.length;i++){
        var element=arguments[i],n;
        if(element==null)continue;//忽略null和undefined实参
        if(isArray(element))//如果实参是数组
            n=flexisum.apply(this,element);//递归地计算累加和
        else if(typeof element==="function")//否则,如果是函数...
            n=Number(element());//调用它并做类型转换
        else
            n=Number(element);//否则直接做类型转换
        if(isNaN(n))//如果无法转换为数字,则抛出异常
            throw Error("flexisum():can't convert"+element+"to number");
        total+=n;//否则,将n累加至total
    }
    return total;
}

作为值的函数

函数可以定义,也可以调用,这是函数最重要的特性。函数定义和调用是JavaScript的词法特性,对于其他大多数编程语言来说亦是如此。然而在JavaScript中,函数不仅是一种语法,也是值,也就是说,可以将函数赋值给变量,存储在对象的属性或数组的元素中,作为参数传入另外一个函数等。

为了便于理解JavaScript中的函数是如何用做数据的以及JavaScript语法,来看一下这样一个函数定义:

function square(x){return x*x;}
这个定义创建一个新的函数对象,并将其赋值给变量square。函数的名字实际上是看不见的,它(square)仅仅是变量的名字,这个变量指代函数对象。函数还可以赋值给其他的变量,并且仍可以正常工作:

var s=square;//现在s和square指代同一个函数
square(4);//=>16
s(4);//=>16

 除了可以将函数赋值给变量,同样可以将函数赋值给对象的属性。当函数作为对象的属性调用时,函数就称为方法:

var o={square:function(x){return x*x;}};//对象直接量
var y=o.square(16);//y等于256

函数甚至不需要带名字,当把它们赋值给数组元素时:

var a=[function(x){return x*x;},20];//数组直接量
a[0](a[1]);//=>400

最后一句代码看起来很奇怪,但的确是合法的函数调用表达式!

下面展示了将函数用做值时的一些例子,这段代码可能会难读一些,但注释解释了代码的具体含义:

将函数用做值

//在这里定义一些简单的函数
function add(x,y){return x+y;}
function subtract(x,y){return x-y;}
function multiply(x,y){return x*y;}
function divide(x,y){return x/y;}//这里的函数以上面的某个函数作为参数
//并给它传入两个操作数然后调用它

function operate(operator,operand1,operand2){
    return operator(operand1,operand2);
}

//这行代码所示的函数调用实际上计算了(2+3)+(4*5)的值
var i=operate(add,operate(add,2,3),operate(multiply,4,5));//我们为这个例子重复实现一个简单的函数
//这次实现使用函数直接量,这些函数直接量定义在一个对象直接量中
var operators={
    add:function(x,y){return x+y;},
    subtract:function(x,y){return x-y;},
    multiply:function(x,y){return x*y;},
    divide:function(x,y){return x/y;},
    pow:Math.pow//使用预定义的函数
};//这个函数接收一个名字作为运算符,在对象中查找这个运算符
//然后将它作用于所提供的操作数
//注意这里调用运算符函数的语法

function operate2(operation,operand1,operand2){
    if(typeof operators[operation]==="function")
        return operators[operation](operand1,operand2);
    else throw"unknown operator";
}

//这样来计算("hello"+""+"world")的值
var j=operate2("add","hello",operate2("add","","world"));//使用预定义的函数Math.pow()
var k=operate2("pow",10,2);

 这里是将函数用做值的另外一个例子,考虑一下Array.sort()方法。这个方法用来对数组元素进行排序。因为排序的规则有很多(基于数值大小、字母表顺序、日期大小、从小到大、从大到小等),sort()方法可以接收一个函数作为参数,用来处理具体的排序操作。这个函数的作用非常简单,对于任意两个值都返回一个值,以指定它们在排序后的数组中的先后顺序。这个函数参数使得Array.sort()具有更完美的通用性和无限可扩展性,它可以对任何类型的数据进行任意排序。

  • 自定义函数属性

JavaScript中的函数并不是原始值,而是一种特殊的对象,也就是说,函数可以拥有属性。当函数需要一个“静态”变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性,而不是定义全局变量,显然定义全局变量会让命名空间变得更加杂乱无章。比如,假设你想写一个返回一个唯一整数的函数,不管在哪里调用函数都会返回这个整数。而函数不能两次返回同一个值,为了做到这一点,函数必须能够跟踪它每次返回的值,而且这些值的信息需要在不同的函数调过程中持久化。可以将这些信息存放到全局变量中,但这并不是必需的,因为这个信息仅仅是函数本身用到的。最好将这个信息保存到函数对象的一个属性中,下面这个例子就实现了这样一个函数,每次调用函数都会返回一个唯一的整数:
 

//初始化函数对象的计数器属性
//由于函数声明被提前了,因此这里是可以在函数声明
//之前给它的成员赋值的
uniqueInteger.counter=0;//每次调用这个函数都会返回一个不同的整数
//它使用一个属性来记住下一次将要返回的值
function uniqueInteger(){
    return uniqueInteger.counter++;//先返回计数器的值,然后计数器自增1
}

来看另外一个例子,下面这个函数factorial()使用了自身的属性(将自身当做数组来对待)来缓存上一次的计算结果:
 

//计算阶乘,并将结果缓存至函数的属性中
function factorial(n){
    if(isFinite(n)&&n>0&&n==Math.round(n)){//有限的正整数
        if(!(n in factorial))//如果没有缓存结果
            factorial[n]=n*factorial(n-1);//计算结果并缓存之
        return factorial[n];//返回缓存结果
    }
else return NaN;//如果输入有误
}
factorial[1]=1;//初始化缓存以保存这种基本情况

作为命名空间的函数

JavaScript中的函数作用域的概念:在函数中声明的变量在整个函数体内都是可见的(包括在嵌套的函数中),在函数的外部是不可见的。不在任何函数内声明的变量是全局变量,在整个JavaScript程序中都是可见的。在JavaScript中是无法声明只在一个代码块内可见的变量的,基于这个原因,我们常常简单地定义一个函数用做临时的命名空间,在这个命名空间内定义的变量都不会污染到全局命名空间。

比如,假设你写了一段JavaScript模块代码,这段代码将要用在不同的JavaScript程序中(对于客户端JavaScript来讲通常是用在各种各样的网页中)。和大多数代码一样,假定这段代码定义了一个用以存储中间计算结果的变量。这样问题就来了,当模块代码放到不同的程序中运行时,你无法得知这个变量是否已经创建了,如果已经存在这个变量,那么将会和代码发生冲突。解决办法当然是将代码放入一个函数内,然后调用这个函数。这样全局变量就变成了函数内的局部变量:

function mymodule(){//模块代码
//这个模块所使用的所有变量都是局部变量
//而不是污染全局命名空间
}
mymodule();//不要忘了还要调用这个函数

这段代码仅仅定义了一个单独的全局变量:名叫"mymodule"的函数。这样还是太麻烦,可以直接定义一个匿名函数,并在单个表达式中调用它:

(function(){//mymodule()函数重写为匿名的函数表达式
//模块代码
}());//结束函数定义并立即调用它

这种定义匿名函数并立即在单个表达式中调用它的写法非常常见,已经成为一种惯用法了。注意上面代码的圆括号的用法,function之前的左圆括号是必需的,因为如果不写这个左圆括号,JavaScript解释器会试图将关键字function解析为函数声明语句。使用圆括号JavaScript解释器才会正确地将其解析为函数定义表达式。使用圆括号是习惯用法,尽管有些时候没有必要也不应当省略。这里定义的函数会立即调用。 

猜你喜欢

转载自blog.csdn.net/wuyufa1994/article/details/85526657