深入理解JavaScript中的函数

1、 定义函数

w3school对函数的定义为:函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块,代码块包裹在花括号中,前面使用了关键词 function。实际上,函数也是对象,每一个函数都是Function类型的实例,因此也具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的地址,并不会和某个函数绑定。常用定义函数有两种:

第一种:使用函数申明语法定义:

function sum(num1, num2){
        alert(num1+num2);
    }

第二种:函数表达式:

var sum = function (num1, num2){
        alert(num1+num2);
    };

第二种方法首先定义一个变量,并将其初始化为一个函数,因此末尾需要分号。

最后一种方法利用了Function构造函数,Function构造函数可以接收任意多个参数,最后一个参数为函数体,由于这种定义法会导致解析两次代码,因此并不推荐。这种函数的定义语法如下:

var sum = new Function("num1", "num2","alert(num1+num2)");

那么函数申明和函数表达式有什么区别呢?我们先看下面一个例子:

    sum1(1,2); //3
    sum2(3,4); //error, sum2 is not a function
    function sum1(num1, num2){
        alert(num1+num2);
    }

    var sum2 = function(num1, num2){
        alert(num1+num2);
    };

运行上述代码可以发现通过函数申明的sum1运行正确,而通过函数表达式定义的sum2却出错了。这是因为解析器在加载数据时,会率先读取函数申明,并使其在任何代码执行前可用,因此sum1执行正确;但是函数表达式则必须等到解析器执行到它所在的代码行才会被解释执行。
为了验证,我们再写一个例子:

    sum1(1,2);
     var sum2 = function (num1, num2){
        return(num1+num2);
    };

    function sum1(num1, num2){
        alert(sum2(num1, num2));// error, sum2 is not a function }

这个程序运行的结果仍然是出错,显示“sum2 is not a function”。这是因为解析器在代码执行之前就会解析函数申明,在解析sum1时,由于sum1中调用了sum2,但是sum2是函数表达式,只能等到解析器执行到该行时才会执行,因此会报错。

另外,不用Java等语言有重载的概念,只要两个函数定义接收的参数类型或数量不同,则可实现重载。ECMAscript中没有重载,我们可以把函数名想象成指针,这样,后面申明的函数名就会覆盖前面的同函数名。

2、 函数中的arguments对象

arguments是函数内部的对象,是一个类数组对象,里面存放在传入的所有的参数。ECMAscript函数不在意传入参数的类型以及个数,传入参数的个数可以跟你定义参数接收参数的个数可以不一样。最终,在函数内部都是通过arguments对象去访问参数数组的。

function add(num1, num2){
        alert(arguments[0]+arguments[1]+arguments[2]) ; // 6
        alert(num1+num2+arguments[2]); //6
        arguments[1]=10;
        alert(num2); //10
        }
add(1,2,3);

arguments是一个类数组对象,因此可以像数组一样访问其中的参数,例如第一个元素是arguments[0],第二个参数是arguments[1],以此类推。当然,arguments也能与命名参数一起使用。需要注意的是,arguments的值会自动反映到对应的命名函数上,例如只要修改了arguments[1]的值,这个值也会同步到num2上。

另外可以通过length属性来获得传入参数的个数(注意不是函数定义的参数个数)。

function add(num1, num2){
    alert(arguments.length); //3
    }
    add(1,2,3);
    alert(add.length); //2

arguments.length是获得arguments对象的长度,表示的是最终传入的参数个数;而add.length表示的是add()函数希望接收的命名参数的个数,也就是定义函数时接收的命名参数的个数。

arguments还有一个callee属性,该属性是一个指向拥有这个arguments对象的函数的指针。其主要用途是解除函数体内的函数名与外部函数的耦合问题。看一个经典用递归求阶乘的方法:

function factorial(num){
        if (num<=1){
            return 1;
        }else{
            return num * factorial(num-1);
        }
    }
    alert(factorial(5)); //120
    var f1 = factorial;
    factorial = function(){
        return 0;
    };
    alert(f1(5)); //0
    alert(factorial(5)); //0

这种方法,在factorial()函数调用了与factorial同名的函数,如果将factorial赋值给f1,然后将一个简单的返回0的函数赋值给factorial。那么函数体内的函数调用factorial()将会指向改变后的factorial(),而我们需要它在函数体内调用f1,这样就会出错。用callee属性可以解决此问题:

function factorial(num){
        if (num<=1){
            return 1;
        }else{
            return num * arguments.callee(num-1);
        }
    }
    alert(factorial(5)); //120
    var f1 = factorial;
    factorial = function(){
        return 0;
    };
    alert(f1(5)); //120
    alert(factorial(5)); //0

arguments.callee指向拥有arguments对象的函数,一开始是factorial();然后将factorial赋值给f1,这时arguments.callee指向f1,这样不管函数名如何变,都能得到正确的结果。

这里顺带介绍一个与callee长得特别像的属性:caller,这个是在ECMAScript 5中规范化的一个函数对象的属性,这个属性中保存着调用当前函数的函数的引用,如果在全局作用域中调用当前函数,caller的值为null。

    function outer(){
        inner();
        }
    function inner(){
        console.log(arguments.callee.caller);
        }
    outer();

以上代码在控制台会打印出outer()函数的源代码。

3、 函数中的apply()和call()方法

前面说过,在JavaScript中,函数也是对象,因此都有方法,每个函数都包含两个方法apply()和call(),这两个方法的用途都是在指定的作用域中调用函数。

apply()方法接收两个参数:第一个参数是在其中运行函数的作用域,第二个参数是参数数组,该数组可以Array的实例,也可以是arguments对象:

function sayHi(str1,str2){
    return str1+","+str2;
    }

function callSayHi1(str1, str2){
    alert(sayHi.apply(this, arguments));  //传入arguments
    }

function callSayHi2(str1, str2){
    alert(sayHi.apply(this, [str1, str2])); //传入Array实例
    }

callSayHi1("hello","sean"); //hello,sean
callSayHi2("hello","jack"); //hello,jack

call()方法和apply()的作用相同,这两个方法的区别在于接收参数的方式不同。call()方法第一个参数也是运行函数的作用域,后面的都是参数直接传递给函数:

function sayHi(str1,str2){
    return str1+","+str2;
    }

function callSayHi(str1, str2){
    alert(sayHi.call(this, str1, str2));
    }

callSayHi("hello","sean"); // hello,sean

apply()和call()方法最大的作用在于能改变函数运行的作用域

    var str1 = "hello";
    var str2 = "sean";
    var obj = {
        str1: "你好",
        str2: "jack"
        };

    function sayHi(){
        alert(this.str1+","+this.str2);
    }

    sayHi.apply(this); //hello,sean
    sayHi.apply(obj); //你好,jack

函数是作为全局函数定义的,sayHi.apply(this)调用时,this指向的是window,所以this.str1和this.str2其实是window.str1和window.str2。当执行sayHi.apply(obj)时,函数内的this指向了obj,这时,this.str1和this.str2其实是obj.str1和obj.str2。

ECMAScript 5中还定义了一个bind()方法,这个方法会创建一个函数的实例,其this值会绑定到传给bind()函数的值。:

    var str1 = "hello";
    var str2 = "sean";
    var obj = {
        str1: "你好",
        str2: "jack"
    };

    function sayHi(){
        alert(this.str1+","+this.str2);
    }

    var sayHi2 = sayHi.bind(obj);
    sayHi2(); //你好,jack

sayHi()函数调用bind()方法,并传入obj对象,创建了sayHi2()函数,sayHi2()函数中的this值等于obj,因此即使是在全局作用域中调用该函数,也会得到“你好,jack”。

你可能感兴趣的:(JavaScript,函数)