返回首页 JavaScript 核心技术指南

函数

函数,在 C 语言之类的过程式语言中,是顶级的实体,而在 Java/C++ 之类的面向对象的语言中,则被对象包装起来,一般称为对象的方法。而在 JavaScript 中,函数本身与其他任何的内置对象在低位上是没有任何区别的,也就是说,函数本身也是对象。

总的来说,函数在 JavaScript 中可以:

  • 被赋值给一个变量
  • 被赋值为对象的属性
  • 作为参数被传入别的函数
  • 作为函数的结果被返回
  • 用字面量来创建

函数对象

创建函数

创建 JavaScript 函数的一种不长用的方式(几乎没有人用)是通过 new 操作符来作用于 Function“构造器”:

var funcName = new Function( [argname1, [... argnameN,]] body ); 

参数列表中可以有任意多的参数,然后紧跟着是函数体,比如:

var add = new Function("x", "y", "return(x+y)");  
print(add(2, 4));

将会打印结果:

6

但是,谁会用如此难用的方式来创建一个函数呢?如果函数体比较复杂,那拼接这个 String 要花费很大的力气,所以 JavaScript 提供了一种语法糖,即通过字面量来创建函数:

function add(x, y){  
    return x + y;  
}  

或:

var add = function(x, y){  
    return x + y;  
} 

事实上,这样的语法糖更容易使传统领域的程序员产生误解,function 关键字会调用 Function 来 new 一个对象,并将参数表和函数体准确的传递给 Function 的构造器。 通常来说,在全局作用域(作用域将在下一节详细介绍)内声明一个对象,只不过是对一个属性赋值而已,比如上例中的add函数,事实上只是为全局对象添加了一个属性,属性名为 add,而属性的值是一个对象,即 function(x, y){return x+y;},理解这一点很重要,这条语句在语法上跟:

var str = "This is a string"; 

并无二致。都是给全局对象动态的增加一个新的属性,如此而已。

为了说明函数跟其他的对象一样,都是作为一个独立的对象而存在于 JavaScript 的运行系统,我们不妨看这样一个例子:

function p(){  
    print("invoke p by ()");  
}  

p.id = "func";  
p.type = "function";  

print(p);  
print(p.id+":"+p.type);  
print(p()); 

没有错,p 虽然引用了一个匿名函数(对象),但是同时又可以拥有属性,完全跟其他对象一样,运行结果如下:

function (){
print("invoke p by ()");
}
func:function
invoke p by ()

函数的参数

在 JavaScript 中,函数的参数是比较有意思的,比如,你可以将任意多的参数传递给一个函数,即使这个函数声明时并未制定形式参数,比如:

function adPrint(str, len, option){  
    var s = str || "default";  
    var l = len || s.length;  
    var o = option || "i";  

    s = s.substring(0, l);  
    switch(o){  
       case "u":  
           s = s.toUpperCase();  
           break;  
       case "l":  
           s = s.toLowerCase();  
           break;  
       default:  
           break;  
    }  

    print(s);  
}  

adPrint("Hello, world");  
adPrint("Hello, world", 5);  
adPrint("Hello, world", 5, "l");//lower case  
adPrint("Hello, world", 5, "u");//upper case 

函数 adPrint 在声明时接受三个形式参数:要打印的串,要打印的长度,是否转换为大小写的标记。但是在调用的时候,我们可以按顺序传递给adPrint一个参数,两个参数,或者三个参数(甚至可以传递给它多于 3 个,没有关系),运行结果如下:

Hello, world
Hello
hello
HELLO

事实上,JavaScript 在处理函数的参数时,与其他编译型的语言不一样,解释器传递给函数的是一个类似于数组的内部值,叫 arguments,这个在函数对象生成的时候就被初始化了。比如我们传递给 adPrint 一个参数的情况下,其他两个参数分别为undefined.这样,我们可以才 adPrint 函数内部处理那些 undefined 参数,从而可以向外部公开:我们可以处理任意参数。

我们通过另一个例子来讨论这个神奇的 arguments:

function sum(){  
    var result = 0;  
    for(var i = 0, len = arguments.length; i < len; i++){  
       var current = arguments[i];  
       if(isNaN(current)){  
           throw new Error("not a number exception");  
       }else{  
           result += current;  
       }  
    }  

    return result;  
}  

print(sum(10, 20, 30, 40, 50));  
print(sum(4, 8, 15, 16, 23, 42));//《迷失》上那串神奇的数字  
print(sum("new")); 

函数 sum 没有显式的形参,而我们又可以动态的传递给其任意多的参数,那么,如何在 sum 函数中如何引用这些参数呢?这里就需要用到 arguments 这个伪数组了,运行结果如下:

150
108
Error: not a number exception

函数作用域

作用域的概念在几乎所有的主流语言中都有体现,在 JavaScript 中,则有其特殊性:JavaScript 中的变量作用域为函数体内有效,而无块作用域,我们在 Java 语言中,可以这样定义 for 循环块中的下标变量:

public void method(){  
    for(int i = 0; i < obj1.length; i++){  
       //do something here;  
    }  
    //此时的i为未定义  
    for(int i = 0; i < obj2.length; i++){  
       //do something else;  
    }    
}  

而在 JavaScript 中:

function func(){  
    for(var i = 0; i < array.length; i++){  
       //do something here.  
    }  
    //此时i仍然有值,及I == array.length  
    print(i);//i == array.length;  
}  

JavaScript 的函数是在局部作用域内运行的,在局部作用域内运行的函数体可以访问其外层的(可能是全局作用域)的变量和函数。JavaScript 的作用域为词法作用域,所谓词法作用域是说,其作用域为在定义时(词法分析时)就确定下来的,而并非在执行时确定,如下例:

var str = "global";  
function scopeTest(){  
    print(str);  
    var str = "local";  
    print(str);  
}  

scopeTest(); 

运行结果是什么呢?初学者很可能得出这样的答案:

global
local

而正确的结果应该是:

undefined
local

因为在函数 scopeTest 的定义中,预先访问了未声明的变量 str,然后才对 str 变量进行初始化,所以第一个 print(str)会返回 undifined 错误。那为什么函数这个时候不去访问外部的 str 变量呢?这是因为,在词法分析结束后,构造作用域链的时候,会将函数内定义的 var 变量放入该链,因此 str 在整个函数 scopeTest 内都是可见的(从函数体的第一行到最后一行),由于 str 变量本身是未定义的,程序顺序执行,到第一行就会返回未定义,第二行为 str 赋值,所以第三行的 print(str)将返回”local”。

函数上下文

在 Java 或者 C/C++等语言中,方法(函数)只能依附于对象而存在,不是独立的。而在 JavaScript 中,函数也是一种对象,并非其他任何对象的一部分,理解这一点尤为重要,特别是对理解函数式的 JavaScript 非常有用,在函数式编程语言中,函数被认为是一等的。

函数的上下文是可以变化的,因此,函数内的 this 也是可以变化的,函数可以作为一个对象的方法,也可以同时作为另一个对象的方法,总之,函数本身是独立的。可以通过 Function 对象上的 call 或者 apply 函数来修改函数的上下文:

call 和 apply

call 和 apply 通常用来修改函数的上下文,函数中的 this 指针将被替换为 call 或者 apply 的第一个参数

//定义一个人,名字为jack  
var jack = {  
    name : "jack",  
    age : 26  
}  

//定义另一个人,名字为abruzzi  
var abruzzi = {  
    name : "abruzzi",  
    age : 26  
}  

//定义一个全局的函数对象  
function printName(){  
    return this.name;  
}  

//设置printName的上下文为jack, 此时的this为jack  
print(printName.call(jack));  
//设置printName的上下文为abruzzi,此时的this为abruzzi  
print(printName.call(abruzzi));  

print(printName.apply(jack));  
print(printName.apply(abruzzi));  

只有一个参数的时候call和apply的使用方式是一样的,如果有多个参数:

Js代码  收藏代码
setName.apply(jack, ["Jack Sept."]);  
print(printName.apply(jack));  

setName.call(abruzzi, "John Abruzzi");  
print(printName.call(abruzzi));  

得到的结果为:

Js代码  收藏代码
Jack Sept.  
John Abruzzi  

apply 的第二个参数为一个函数需要的参数组成的一个数组,而 call 则需要跟若干个参数,参数之间以逗号(,)隔开即可。

使用函数

前面已经提到,在 JavaScript 中,函数可以

  • 被赋值给一个变量
  • 被赋值为对象的属性
  • 作为参数被传入别的函数
  • 作为函数的结果被返回

我们就分别来看看这些场景:

赋值给一个变量:

//声明一个函数,接受两个参数,返回其和  
function add(x, y){  
    return x + y;  
}  

var a = 0;  
a = add;//将函数赋值给一个变量  
var b = a(2, 3);//调用这个新的函数a  
print(b);  

这段代码会打印”5”,因为赋值之后,变量 a 引用函数 add,也就是说,a 的值是一个函数对象(一个可执行代码块),因此可以使用 a(2, 3)这样的语句来进行求和操作。

赋值为对象的属性:

var obj = {  
    id : "obj1"  
}  

obj.func = add;//赋值为obj对象的属性  
obj.func(2, 3);//返回5 

事实上,这个例子与上个例子的本质上是一样的,第一个例子中的 a 变量,事实上是全局对象(如果在客户端环境中,表示为 window 对象)的一个属性。而第二个例子则为 obj 对象,由于我们很少直接的引用全局对象,就分开来描述。

作为参数传递:

//高级打印函数的第二个版本  
function adPrint2(str, handler){  
    print(handler(str));  
}  

//将字符串转换为大写形式,并返回  
function up(str){  
    return str.toUpperCase();  
}  

//将字符串转换为小写形式,并返回  
function low(str){  
    return str.toLowerCase();  
}  

adPrint2("Hello, world", up);  
adPrint2("Hello, world", low);  

运行此片段,可以得到这样的结果:

HELLO, WORLD
hello, world

应该注意到,函数 adPrint2 的第二个参数,事实上是一个函数,将这个处理函数作为参数传入,在 adPrint2 的内部,仍然可以调用这个函数,这个特点在很多地方都是有用的,特别是,当我们想要处理一些对象,但是又不确定以何种形式来处理,则完全可以将“处理方式”作为一个抽象的粒度来进行包装(即函数)。

作为函数的返回值:

先来看一个最简单的例子:

function currying(){  
    return function(){  
       print("curring");  
    }  
}

函数 currying 返回一个匿名函数,这个匿名函数会打印”curring”,简单的调用 currying()会得到下面的结果:

function (){  
    print("curring");  
} 

如果要调用 currying 返回的这个匿名函数,需要这样:

currying()();  

第一个括号操作,表示调用 currying 本身,此时返回值为函数,第二个括号操作符调用这个返回值,则会得到这样的结果:

currying
上一篇: 对象与 JSON 下一篇: 数组