该系列文章的内容主要来自: Pro JavaScript with Mootools.作者: Mark Joseph Obceca
在讨论js的函数前,一定要明白js里函数实现的两个特点:
1. js的函数是第一类的函数,也就是说: js的函数可以赋值给变量,作为其他函数的参数,可以作为其他函数的返回值。简单的说:js的函数和js里的其他数据一样,都是对象。这一点和java是不同的。和 c的函数指针,python的函数很像。
2. js定义函数时,允许函数的嵌套,也就是说:定义函数时,在函数体内可以定义新的函数。这一点 和C,和java的函数定义不同,但是 和python的函数定义是相同的。
(js的函数实现,很大程度上更类似与python)
因为js的函数的以上两个特点,使js的闭包得以实现。同时,js的函数里变量的作用域规则也变得比较复杂。
现在正式开始讨论js的函数。
一种函数类型,多种定义方式:
javascript 定义里,只有一种函数类型,但是可以通过不同的方法去定义函数的实现,也就是说函数的定义有多种方式。这些函数定义方式中,大多都通过一种叫: 函数字面量 的语法去定义函数。
函数字面量长的是这个样子滴:
function Identifier(FormalParameters,...){
function body
}
因为函数是对象,函数也可以有自己的方法和属性。这里我们先引入两个基本的属性:
name: function identifier的字符串值
length: 函数定义时,形参的个数
函数定义方式1: 函数声明
函数声明是函数定义方式中最简单的一种。如下代码通过函数声明的方式定义了一个新的函数add:
//a function call add
function add(a,b){
return a+b
};
console.log(typeof add); //function
console.log(add.name); //add
console.log(add.length); //2
console.log(add(20,5)); //25
(关于console.log :类似于alert,但是直接在浏览器的js console显示结果,省去了用alert时没完没了的点击"确定"的操作)
函数声明中, function identifier是必需的,用该标识符在函数定义的当前作用域里生成一个变量,该变量的值就是定义的函数。 (关于作用域:可以从js解释器的角度去理解: js解释器解释执行js代码时:会根据代码的执行,进入不同的"作用域",后文会有详细说明) 在我们的例子中add变量是生成在global作用域中,add变量具有name属性,该属性的值就是函数标识符: add。add变量的length属性是2.因为我们定义该函数时,使用了2个形参。
js的作用域是基于文法的作用域,也就是说,标识符的作用域是基于代码文件中定义的位置,而不是执行时的位置。(这点python也一样如此)
举例如下:
//outer function ,global scope
function outer(){
//inner function, local scope
function inner(){
//...
}
}
//check the outer function
console.log(typeof outer); //'function'
//run outer to create the new function
outer();
//check the inner function:
console.log(typeof inner); // 'undefined'
上例中,outer变量是的作用域是global的, 调用outer 时,outer函数生成一个名字叫inner的变量,以函数声明形式定义的inner函数产生的变量的作用域仅在outer函数定义的内部有效.
因为函数声明生成了一个和函数标识符同名的变量,因此会有覆盖当前作用域中同名变量的情况:
举例如下:
//a variable in the surrent scope
var items =1;
// a function declaration with the same name
function items(){
//...
}
console.log(typeof items); // 'function' ,NOT 'number'
函数定义方式2: 函数表达式
这种定义函数的方式利用了:函数可以存储在变量中 这一事实.
以下,我们同样定义一个add函数,但是使用的是函数表达式的方式。
var add = function(a,b){
return a+b
};
console.log(typeof add); // 'function'
console.log(add.name); // ' ' or 'anonymous'
console.log(add,length);//2
console.log(add(20,5)) ; //25
在上面的例子中,我们把函数字面量定义赋值给了变量add. 上例中函数的length属性和函数声明中的是一样的,但是name属性却不同,这是因为,我们在函数字面量定义中,没有定义函数标识符,一些js的解释器会把name定义为' ',有些会定义成'anonymous'.
函数声明的作用域规则和函数表达式的作用域规则有稍稍的不同,因为函数表达式的作用域由存储函数的变量的作用域决定。让我们牢记一点: js中,var 关键字定义了一个当前作用域的局部变量,省略var关键字会生成一个全局的变量。
举例如下:
// outer function, global scope
var outer = function(){
// inner function, local scope
var localInner = function(){
// ...
};
// inner function, global scope
globalInner = function(){
// ...
};
};
// check the outer function
console.log(typeof outer);// 'function'
// run outer to create the new functions
outer();
// check the new functions
console.log(typeof localInner); // 'undefined'
console.log(typeof globalInner); // 'function'
outer变量被var限定,但是outer定义在global作用域下,global作用域的局部变量就是全局变量。至于定义在outer内部的 localInner和globalInner作用域规则如我们刚刚牢记的: js中,var 关键字定义了一个当前作用域的局部变量,省略var关键字会生成一个全局的变量。
函数定义方式3: 命名的函数表达式
函数表达式经常使用匿名的函数字面量定义,但是也能在函数字面量定义中显式的定义一个函数标识符。这种函数表达式的变体叫做: 命名的函数表达式 。
举例如下:
var add = function add(a, b){
return a + b;
};
console.log(typeof add); // 'function'
console.log(add.name); // 'add'
console.log(add.length); // 2
console.log(add(20, 5)); // 25
这个例子和函数表达式的唯一不同就是我们在定义函数字面量时,给函数定义了一个标识符,意味着add的name属性不再为空,或anonymous。 命名的函数表达式的之所以要有一个名字,就是为了在函数定义内部访问函数自己。为什么我们需要这个功能呢?还是看例子吧:
例子1:
var myFn = function(){
// reference the function
console.log(typeof myFn);
};
myFn(); // 'function'
这个例子很简单,没什么可说的,但是让我们看下一个例子:
例子2:
// global scope
var createFn = function(){
// result function
return function(){
console.log(typeof myFn);
};
};
// different scope
(function(){
// put the result function of `createFn`
// into a local variable
var myFn = createFn();
// check if reference is available
myFn(); // 'undefined'
})();
在global作用域中,我们定义一个createFn的函数,该函数返回一个如同例子1的log功能的函数。然后我们定义了一个单次执行的匿名函数,在该匿名函数的local左右域中定义了一个myFn变量,并把createFn的返回值(一个函数)赋给了myFn。
例子2,和例子1试图实现的功能差不多,只是有两个变化:1,用调用函数的返回一个函数,而不是用函数字面量定义函数。2,变量myFn在另外一个local左右域中。 js的作用域是文法作用域,在createFn中,myFn是不可见的,因此,在单次执行的匿名函数中,myFn的调用结果是:‘undefined’ 而不是’function‘。(关于左右域规则,本系列之二有详述)
通过给我们在createFn中返回的函数定义一个函数标识符,我们即可解决这个问题。
例子3:
// global scope
var createFn = function(){
// result function
return function myFn(){
console.log(typeof myFn);
};
};
// different scope
(function(){
// put the result function of `createFn`
// into a local variable
var myFn = createFn();
// check if reference is available
myFn(); // 'function'
})();
为函数增加一个显式定义的函数标示符,就如同生成一个在该函数内部可用的指向该函数本身的新的变量。命名的函数表达式和函数表达式定义的函数的作用域规则相同:该函数赋予的变量的作用域决定了该函数是local的还是global的。但是命名函数表达式中新加的名字,具有不同的作用域规则:该名字仅在函数定义的内部可见。
如下例:
//a function with different identifier
var myFn = function fnID(){
console.log(typeof fnID);
}
//the variable
console.log(typeof myFn);//'function'
//the identifier
console.log(typeof fnID);//'undefined'
myFn();// 'function'
该例子显示,在global 作用域下,myFn可以用来引用定义的函数,但是fnID不可以。然而在函数内部,是可以通过fnID对函数进行引用。
函数定义方式4: 单次执行函数
我们在生成函数表达式时,接触过匿名函数,匿名函数有更广阔的用途。一种最重要的用途就是用匿名函数定义一个函数,并且马上执行该函数,而不存储对该函数的任何变量引用。 这种定义函数的方式被称作:单次执行函数。
举例如下:
//create a function and invoke it immediately (function(){ var msg = 'hello world'; console.log(msg);//'hello word' })();
在上例子中,我们用括号包裹了一个函数定义字面量,然后用函数调用操作符()去立即执行该函数。该函数没有存储在变量中,也没有对该函数生成任何引用。这一个单次运行,既抛型函数:生成该函数,干活,然后消失。
为了理解单次执行函数的工作原理,我们应该记得:function是对象,而对象就是数值。js的数值可以被立即使用而不用存储在变量中,因此我们也可以生成一个匿名函数然后通过函数调用操作符()去立即执行该函数。
另外,我们应该注意到,前例中,我们用了一对括号去包裹了我们定义的函数,而不是这样:
//create a function and invoke it immediately function(){ var msg = 'hello world'; console.log(msg);//'hello word' }();
若如此,js解释器会报语法错误。因为当解释器遇到如上的代码行,它会解释把上面的代码解释成一个函数声明。解释器看到一个函数声明,就要去找函数标识符,结果找不到,于是报错。
我们需要给函数定义包裹一个括号,告知js 解释器,这不是一个函数声明,而是我们要生成一个函数,并且立即使用。因为我们定义该函数时没有标示符去引用该函数,因此我们需要用一对包裹的括号来做直接引用,然后直接调用。
注意:函数调用操作符()可以出现在包裹函数的括号的里面,也可以出现在外面。象: (function(){...}())也是可以的。但是把函数调用操作符()放包裹的括号外面更常见一些。
单次执行函数非常有用,其中最重要的用途就是保持变量和标示符处在一个本地的,受保护的作用域中,
如下例:
//top level scope var a=1; //localize scope with a single execution function (function(){ //local scope var a=2; })(); console.log(a); //1
我们第一个变量在顶层的作用域中声明,使该变量全局可见。我们在单次执行函数中,有声明了一个本地的变量。把该本地变量的值改变成2,但是顶层变量的值并不受影响。
这种应用很常见,特别是在开发库的时候。因为本地变量处于一个独立的左右域中,这样救避免了标识符的冲突。若你在你的应用中有两个脚本定义了同样的标识符,则两个标识符冲突导致覆盖的几率就很高了,除非两个脚本之一用单次执行函数把左右域本地化。
单次执行函数的另外一个特点是,可以像函数声明那样定义一个函数标识符:
(function myFn(){ console.log(typeof myFn); //'function' })(); console.log(typeof myFn);//'undefined'
看起来象函数声明,其实是单次执行函数。尽管我们为函数定义了一个标识符,但是却不会象函数声明那样,在当前作用域中产生一个对应的变量。这个标识符仅允许你在函数定义内部引用该函数。
和其他函数一样,单次执行函数也可以有参数,和单次执行函数的标识符结合,就可以生成一个立马可运行的迭代函数:
var number=12; var numberFactorial = (function factorial(number){ return (number==0)?1:number*factorial(number-1); })(number); console.log(numberFactorial);// 479001600
函数定义方式5: Function 对象
最后一种函数定义方式Function对象,和其他的定义方式都不相同,因为这种方式,不使用函数字面量去定义函数。这种定义函数方式的基本语法如下:
// a function object new Function('FormalArgument1',"FormalArgument2",...,'FunctionBody');
我们用传递字符串做参数给Function构建器,来生成函数。
举例如下:
var add = new Function('a','b','return a+b;'); console.log(typeof add);//'function' console.log(add.name);//' ' or 'anonymous' console.log(add.length);//2 console.log(add(20,5));//25
Function 对象定义的函数一个最大的特点: 该函数的定义里,js解释器对函数里出现的变量是从global作用域里去解析的。
举例如下:
//global varibal var x=1; //localized scope (function(){ //local x variable var x=5; // a function object var myFn = new Function('console.log(x);'); myFn(); //1, not 5 })();
以上就是函数定义的五种方式。
当然,函数还有参数,返回值等问题可以讨论,但是其他的很多书已经讨论的很清楚,就不再次多说了。
P.S. 对iteye的编辑器不熟悉,导致前面发的很多代码段前面都会出现: <span style= "font-size: small;" >,后面出现</span>的情况,略去即可