Javascript 一席谈之一:一种函数,多种定义方式

该系列文章的内容主要来自: 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>的情况,略去即可

你可能感兴趣的:(JavaScript)