函数也是数据
在Javascript中,函数实际上也是数据。这概念对于我们日后的学习至关重要。也就是说,我们可以把一个函数赋值给一个变量。
var f = function () {
return 1;
}
上面这种定义方式通常被叫做函数标识记法。
function (){return 1;}
是一个函数表达式。表达式可以被命名,成为命名函数表达式所以以下这种情况也是合法的。虽然我们不常常用到(在这里,myFunc是函数的名字,而不是变量)
var f = function myFunc(){
return 1;
}
这样看起来,似乎命名函数表达式和函数声明没有什么区别。但他们其实是不同的。两者的差别表现于它们的上下文。(以后会举例来阐明这些概念)
如果我们对函数变量调用typeof
,操作符返回的字符串将会是"function"
。
所以,Javascript中的函数也是一种数据,只不过这种特殊的数据类型有两个重要的特性:
- 它们所包含的是代码
- 它们是可以执行的(或者说是可以调用的)
由于函数也是赋值给变量的一种数据,所以函数的命名规则与一般变量相同——即函数名不能以数字开头,并且可以由任意的字母、数字、下划线和$组合而成。
匿名函数
我们可以这样定义一个函数;
var f = function (a){
return a;
}
通过这种方式定义的函数常被称为匿名函数(即没有名字的函数),特别是当它不被赋值给变量单独使用的时候。在这种情况下,此类函数有两种优雅的用法:
- 你可以将匿名函数作为参数传递给其他函数,这样接收方函数就能利用我们所传递的函数来完成某些事情。
- 你可以定义某个匿名函数来执行某些一次性任务。
接下来,我们来看两个具体的应用示例。通过其中的细节来进一步了解匿名函数。
回调函数
既然函数可以与任何可以被赋值给变量的数据是相同的,那么它当然可以像其他数据那样被定义、删除、拷贝,以及当成参数传递给其他函数。
在下面的示例中,我们定义了一个函数,这个函数有两个函数类型的参数,然后它们会分别执行这两个参数所指向的函数,并返回它们的返回值之和。
function invokeAdd(a,b) {
return a() + b();
}
下面我们来简单定义一下这两个参与加法运算的函数(使用函数声明模式),它们只是单纯地返回一个固定值:
function one() {
return 1;
}
function two() {
return 2;
}
现在,我们只需将这两个函数传递给目标函数invokeAdd(),就可以得到执行的结果了。
invokeAdd(one, two); // 3
事实上,我们也可以直接用匿名函数(即函数表达式)来代替one()和two(),以作为目标函数的参数,例如:
invokeAdd(
function () { return 1; },
function () { return 2; }
);
当我们将函数A传递给函数B,并又B来执行A时,A就成了一个回调函数(callback function),如果这时A还是一个无名函数,我们就称它为匿名回调函数。
那么,应该什么时候使用回调函数呢?下面我们将通过几个应用实例来示范下回调函数的优势,包括:
- 它可以让我们在不做命名的情况下传递函数(这意味着可以节省变量的使用)
- 我们可以将一个函数调用错做委托给另一个函数(这以为着可以节省一些代码编写工作)
- 它们也有助于提升性能
回调示例
在编程过程中,我们通常需要将一个函数的返回值传递给另一个函数。在下面的例子中,我们定义了两个函数;第一个是multiplyByTwo()
,该函数会通过一个循环将其所接受的三个参数分别乘以2,并一数组的形式返回结果;第二个函数addOne()
只接受一个值,然后将它加1并返回。
function multiplyByTwo(a,b,c){
var i, arr = [];
for(i = 0; i < 3; i++){
arr[i] = arguments[i] * 2;
}
retuen arr;
}
function (a) {
return a + 1;
}
现在我们来测试下这两个函数,结果如下:
multiplyByTwo(1,2,3) // [2,4,6]
addOne(100) // 101
接下来,假设我们又三个元素,我们要实现这三个元素在两个函数之间的传递。这需要定义另一个数组,用于存储来自第一步的结果。我们先从multiplyByTwo()的调用开始。
var myarr = [];
myarr = multiplyByTwo(10,20,30);
然后,用循环来遍历每个元素,并将它们分别传递给addOne().
for(var i = 0; i < 3; i++){
myarr[i] = addOne(myarr[i]);
}
myarr; // [21,41,61]
如你所见,这段代码可以工作,但是显然还有一定的改善空间。特别是我们这里使用了两个循环,如果数据量很大或循环操作很复杂的话,开销一定不小。因此,我们需要将他们合二为一。这就需要对multiplyByTwo()函数做一些改动,使其接受一个回调函数,并在每次迭代操作中调用它。具体如下:
function multipslyByTwo(a,b,c,callback){
var i, arr = [];
for(i = 0; i < 3; i++){
arr[i] = callback(arguments[i] * 2);
}
return arr;
}
函数修改完成之后,之前的工作只需要一次函数调用就够了,我们只需要像下面这样同时将初始值和会调函数传递给它:
myarr = multipslyByTwo(1,2,3,addOne);
// [3,5,7]
同样,我们还可以用匿名函数来代替addOne(),这样做可以节省一个额外的全局变量。
multipslyByTwo(1,2,3,function (a){
return a + 1;
})
// [3,5,7]
而且,使用匿名函数也更抑郁随时根据需求来调整代码。
即时函数
目前我们已经讨论了匿名函数在回调方面的应用。接下来,我们来看匿名函数的另一个应用示例——这种函数可以在顶以后立即调用。比如:
(
function () {
alert('Bob')
}
) ();
这种语法看上去有点吓人,但其实很简单——我们只需要将匿名函数的定义放进一对括号中,然后再紧跟一堆括号即可。其中,第二队括号起到的“立即调用”的作用,同时它也是我们向匿名函数传递参数的地方。
(
function (name) {
alert('Hello' + name + ' ! ');
}
) ('dude');
另外,你也可以将第一对括号闭合于第二对括号之后。这两种做法都有效。
(
function () {
// dosomething
} () )
//vs
(function () {
//dosomething
}) ()
使用即时(自调)匿名函数的好处是不会产生任何全局变量,当然,缺点在于这样的函数是无法重复执行的(除非你将它放在某个循环或者其他函数中),这样也使得即时函数非常适合执行一些一次性的或初始化的任务。
如果需要的话,即时函数也可以有返回值,虽然并不常见。
内部(私有)函数
函数与其他类型的值本质上是一样的,因此,没有什么理由可以阻止我们在一个函数内部定义另一个函数。
function outer(param) {
function inner(theinput) {
return theinput * 2;
}
return 'The result is' + inner(param)
}
当我们调用全局函数outer()
时,本地函数inner()
也会在其内部调用。由于inner()
是本地函数,它在outer()
以外的地方是不可见的,所以我们也能将它称为私有函数。
outer(2) // The result is 4
inner(2) // inner is not defined
使用私有函数的好处有以下几点:
- 有助于我们确保全局名字空间的纯净性(这意味着命名冲突的机会很小)
- 确保私有性——这使我们可以选择只讲一些必要的函数暴露给“外部世界”,而保留属于自己的函数,使他们不为该应用程序的其他部分所用。
返回函数的函数
正如之前所提到的,函数始终都会有一个返回值,即便不是显示返回,它也会隐式返回一个undefined。既然函数能返回一个唯一值,那么这个值也有可能是另一个函数。例如:
function a() {
alert('A!');
return funtion () {
alert('B!');
}
}
在这个例子中,函数a()会在执行它工作(弹出A!)之后返回另一个函数。而所返回的函数又会去执行另外一些事情(弹出B!)。我们只需将返回值赋值给某个变量,然后就可以像使用一般函数那样调用它了。
var newFunc = a();
newFunc();
上面第一行执行的是alert('A!'),第二行才是alert('B!')。
如果你像让返回的函数立即执行,也可以不用将它赋值给变量,直接在该调用后面再加一对括号,效果是一样的:
a() ();
能重写自己的函数
由于一个函数可以返回另一个函数,一次我们可以用新的函数来覆盖旧的。例如在之前的例子中,我们也可以通过a()的返回值来重写a()函数自己:
a = a();
当然这句话依然只执行alert('A!'),但如果我们再次调用a(),他就会执行alert('B!')了。这对于要执行某些一次性初始化工作的函数来说会非常有用。这样一来,该函数可以在第一次被调用后重写在即,从而避免了每一次调用时重复一些不必要的操作。
在上的例子中,我们实在外面来重定义该函数的——即我们将函数返回值赋值给函数本身。但是们也可以让函数从内部重写自己。例如:
function a(){
alert("A!");
a = function () {
alert("B!")
}
}
这样一来,当我们第一次调用该函数时,会有如下情况发生:
- alert('A!')将会被执行(可以视之为一次性的准备操作)。
- 全局变量a将会被重定义,并赋予新的函数。
而如果该函数被调用的话,被执行就是alert('B!')了。
下面我们来看一个组合型的应用示例,其中有些技术我们会在以后的章节中讨论。
var a = (function () {
function someSutup () {
var setup = 'done';
}
function actualWork() {
alert('worky-worky')
}
someSetup();
return actualWork;
} () );
在这个列子中有如下情况:
- 我们使用了私有函数——someSetup()和actualWork().
- 我们也使用了即时函数——函数a()的定义后面有一对括号,因此它会执行自调。
- 当函数第一次被调用时,它会调用someSetup(),并返回函数变量actualWork的引用。请注意,返回值中是不带括号的,因此该结果仅仅是一个函数的引用,并不会产生函数调用。
- 由于这里的执行语句是以var a = ...开头的,因而该自调函数所返回的值会重新赋值给a。
好了,我们可以尝试一下以下的问题:上面的代码在以下情景中分别会alert()什么内容?
- 当它最初被载入时。
- 之后再次调用a()时。
这项技术对于某些浏览器相关的操作会相当有用。因为在不同的浏览器中,实现相同任务的方法可能不同,我们都知道浏览器的特性不可能行为函数调用而发生任何改变,因此,最好的选择就是让函数根据其当前所在的浏览器来从新定义自己。这就是所谓的“浏览器兼容性探测”技术。