JavaScript之函数定义与闭包

函数表达式

函数表达式是JavaScript中既强大又容易让人困惑的特性。

函数的定义

函数的定义有两种方式:函数声明 和 函数表达式。

函数声明

函数声明的语法:

function functionName () {
    //函数体
}

function是关键字,后面跟着的是函数的名字,这就是指定函数名的方法。谷歌、火狐、苹果、欧朋等浏览器给函数定义了一个非标准的name属性,它的值 等于 跟在function关键字后面的 函数名:

function sum (num1, num2) {
    //函数体
    return num1 + num2;
}

console.log(sum.name); //sum


函数声明有个重要的特性,就是 函数声明提升,即:在执行代码之前会先读取函数声明。这就意味着可以将函数声明放在调用它的语句之后。

console.log(sum(2, 3)); //5

function sum (num1, num2) {
    //函数体
    return num1 + num2;
}


这段代码不会出错,因为在执行之前会先读取函数的声明,这段代码在引擎中可以认为是这样的:

//先读取了函数声明
function sum (num1, num2) {
    //函数体
    return num1 + num2;
}

console.log(sum(2, 3)); //5 接着再调用函数


函数表达式

定义函数的第二种方法就是使用 函数表达式,创建函数表达式最常见的格式是:

var functionName = function (num1, num2, num3) {
    //函数体
};

这种形式就像是变量赋值语句,即创建一个函数并将其赋值给一个变量,这个情况下创建的函数是 匿名函数(也 拉姆达函数),关键字function后面没有标识符,name属性为空字符。


函数表达式与其它表达式一样, 调用前先赋值,或者会出错,如:

console.log(sum(10,3)); //提示:sum is not a  function

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

没有赋值就调用函数表达式是会报错的。如果先赋值,再调用就不会报错了。

var sum = function (num1, num2) {
    return num1 + num2;
};
	  
console.log(sum(10,3)); //13 


理解函数提升,关键是理解 函数声明 与 函数表达式 的区别



能够创建函数再将其赋值给变量,也能够把函数作为其它函数的值返回。就如前面所学的内部属性中一个升序比较函数:

function bj (proName) {
    return function (object1, object2) {
        var value1 = object1[proName];
        var value2 =  object2[proName];

       if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    };
}

函数bj()就返回了一个匿名函数,返回的函数可能会被赋值给一个变量,或者被其它方式调用。但 在bj()内部,它就是一个匿名的函数, 在把函数当作返回值来使用的情况下,都可以使用匿名函数。



递归函数

递归函数就是 通过自身函数名调用自己的函数。一个经典的阶乘函数就是一个递归函数:

function jshs (num) {
    if (num <= 1) {
        return 1;
    } else if (num > 1) {
        return num * jshs(num - 1);
    }
}

如果此时我们将上述代码修改如下:

var jshs1 = jshs;

jshs = null;

console.log(jshs1(5)); //出错

将函数jshs赋值给变量jshs1,再将jshs赋值null,这样指向原始函数的引用只有一个。但在执行代码时,由于必须执行jshs()函数,但jshs()已经不是函数了,就会报错。

解决这样的问题就引用arguments.callee属性。callee是一个指针,它指向拥有arguements属性的函数,也就是指向函数自己本身。

我们将上述代码用arguments.callee修改一下:

function jshs (num) {
    if (num <= 1) {
        return 1;
    } else if (num > 1) {
        return num * arguments.callee(num - 1);
    }
}


var jshs1 = jshs;

jshs = null;

console.log(jshs1(5)); //120

这样就不会报错了,通过arguments.callee代替函数名,不管函数名怎样修改,其都指向函数本身。在写递归的情况下,arguments.callee比函数名更保险。


在严格模式下,用arguments.callee也会报错,这时我们可以用例命名函数来完成这个任务:

var jshs = (function f (num) {
    if (num <= 1) {
        return 1;
    } else if () {
        return num * f(num -1);
    }
});

创建一个名为f的命名函数,将其赋值给jshs,即便赋值给其它变量,函数内部的f函数名也不会变,照样能执行。在严格与非严格模式下都是可行的。




闭包

变量的作用域

要理解闭包,首先必须理解JavaScript特殊的变量作用域。

变量的作用域分为两种:全局作用域 和 局部作用域。

JavaScript语言的特殊之处:就在于 函数内部可以直接读取全局变量

var n = 999;

function f1 () {
    alert(n);
}

f1(); //999


函数外部无法读取函数内的局部变量。
function f1 () {
    var n = 999;
}

alert(n); //error

注意:函数内部声明变量时,一定要用关键字var,如果不用的话,实际上是声明了一个全局变量。


function f1 () {
    n = 999;
}

f1();

console.log(n); //999


如何从外部读取函数内部的局部变量


出于某种原因,我们需要得到函数内部的局部变量,但正常情况下,是无法办到的,只有通过变通方法才能实现。那就是 在函数内部再定义一个函数

function f1 () {
    n = 999;

    function f2 () {
        alert(n); //999
    }
}

上面的代码中,函数f2被包括在函数f1内部,这时f1的所有局部变量对于 f2来说都是可见的。但反过来就不行,f2内部的局部变量对于f1来说是不可见的,这就是"链式作用域"。

子对象会一级一级地向上逐级寻找所有父对象的变量。所以,父对象的所有变量,对于子对象来说都是可见的,反之不可见的。


既然f2可以读取f1的局部变量,那么只要把f2作为f1的返回值,我们不就可以在f1外部读取它内部变量了么。

function f1 () {
    var n = 999;

    function f2 () {
        alert(n); //999
    }
  
    return f2();
}

var a = f1();

console.log(a); //999


其中, 函数f2就是闭包。



何为闭包

所谓闭包,就是 有权访问其它函数内部变量(作用域中的变量)的 函数。创建闭包的方式:就是在函数内部再创建一个函数。

function bj (proName) {
    return function (object1, object2) {

        //内部函数访问外部函数中的变量
        var value1 = object1[proName];
        var value2 =  object2[proName];

       if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    };
}


即使内部函数被返回了,但依然能访问外部函数的变量,之所以能访问外部函数的变量,是因为 内部函数的作用域链 包含了外部函数bj()的作用域
要理解其中的细节的,就必须清楚第一次调用函数会发生什么。


当某个函数被第一次调用时,会 创建一个执行环境及相应的作用域链,并将这作用域链赋值给一个特殊的内部属性[[Sope]],然后 this、argument和其它命名参数的值来初始化函数的活动对象。在作用域链中, 外部函数的活动对象始终处于第二位置,外部的外部函数的活动对象处于第三位置,依次类推,直至作为作用域的终点全局作用域为止。


在函数执行的过程中,为 读取和写入 变量的值,就需要在作用域链中查找变量。如下例子:
function compare (value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value < value2) {
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

先定义了compare()函数,然后又在全局作用域中调用了它。当第一次调用compare()时,会创建一个包含this、argument、value1和valu2的活动对象。全局执行环境的变量对象(包含this、result和compare)在compare()执行环境的作用域链中则处于第二位。


后台的每个执行环境都有一个变量对象。 全局环境的变量对象始终存在,而compare()函数这种 局部环境的变量对象,只存在于函数执行的过程中。在创建compare()函数时,会创建一个 预先包含全局变量对象的作用域链,并将其保存在内部属性[[Scope]]中。当调用compare()函数时,会为函数创建一个执行环境,并复 制[[Scope]]中的对象创建执行环境的作用域链。此后,又有活动对象被创建并推入执行环境的作用域链前端。

作用域链是指向变量对象的指针列表,只引用不实际包含变量对象。

无论什么时候在函数中访问一个变量,都会在作用域链中查找相应名字的变量。当函数执行完毕后,就会从内存中被销毁,内存中只留下全局作用域(全局执行环境中的变量对象)。

function bj (proName) {
    return function (object1, object2) {

        //内部函数访问外部函数中的变量
        var value1 = object1[proName];
        var value2 =  object2[proName];

       if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    };
}

在另一个函数A 内部创建的函数B 会把包含函数(包含此函数的外部函数A)的 活动对象添加到 它的作用域中,因此,函数bj()的活动对象已经在 内部的匿名函数中作用域中了。

匿名函数从bj()函数中被返回后,它的作用域链被初始化为包含bj()函数的活动对象和全局变量对象。这样匿名函数就可以访问bi()函数中的所有变量,更重要的是,bj()函数执行完成后,其活动对象不会被销毁,因为匿名函数的作用域依然在引用这个活动对象。也就是说,当bj()函数返回后,它的作用域链被销毁,但它的活动对象不会被销毁,仍然在内存中,直到匿名函数被销毁后,bj()函数的活动对象才会被销毁。


注:由于闭包会携带包含它的函数的作用域,因此会比其它函数占用更多的内存。所以不能过度地会用闭包,这样会占用大多内存的。



闭包与变量

作用域链的这种机制有一个问题,那就是闭包只能取得包含函数中(外部函数)任何变量的最后一个值。闭包保存的是整个变量对象,而不是某个特殊的变量。如下:
function createFunction () {
    var arr = new Array(); //创建一个局部变量数组。
    
    for (var i = 0; i < 5; i ++) {
        arr[i] = function () { //此函数的作用域链上保存着同一个变量i
            return i;
        };
    }
    
    return arr; //返回数组引用。这个数组的元素是一个函数
}

var arr = createFunction(); //返回长度为5的数组
console.log(arr[0]()); //5
console.log(arr[1]()); //5
console.log(arr[4]()); //5

上例并没有如想像的返回0,1,5等数值。而是每盒函数都返回5。因为每个函数的作用域链上都保存着函数createFunction()的活动对象(活动对象有argument,this,变量arr,变量i),所以每个函数引用的都是同一个变量i。当creatFunction()函数返回后,变量i已经自增到量大值5了,变量i的值为5,此时每个函数都引用着 保存变量i的同一个变量对象(这个变量对象就是createFunction()函数的执行环境中的变量对象,包含了artument,this,变量arr,变量i),所以在每个函数中的变量都是5。这样就没有达到我们预期的效果。

解决这个问题的方法就是: 通过创建另一个匿名函数强制让闭包行为符合我们的预期。如:
function createFunction () {
    var arr = new Array(); //创建一个局部变量数组。
    
    for (var i = 0; i < 5; i ++) {
        arr[i] = function (num) { //将执行匿名函数的结果保存在数组中。
            return function () { //这个闭包函数用于保存不同num的值
                return num;      //这样,返回的不同num的值可以保存在数组中。
            };
        }(i);//定义匿名函数时并调用,变量i的当前值会复制给参数num。
    }
    
    return arr; //返回数组引用。这个数组的元素是一个函数
}

var arr = createFunction(); //返回长度为5的数组
console.log(arr[0]()); //0
console.log(arr[1]()); //1
console.log(arr[4]()); //4

通过创建一个匿名函数,达到了我们预期的值。我们没有把闭包直接赋值给数组,而是创建了一个匿名函数,并把 立即执行这个匿名函数的结果赋值给数组,这个匿名函数有一个参数num,也就是最终要返回的值。在每次调用这个匿名函数时,都会传入一个参数i,由于函数参数是按值传递的,会把每次传入的变量i的当前值复制给参数num。在这个匿名函数的内部又创建了一个闭包函数,用于保存不同的num值,并把这个不同的num值返回给数组中的每个函数。这样,arr数组中的每个函数都有自己的num变量,而不像之前一样,都调用的同一个变量i。

创建并调用匿名函数的方式: var func = function () {//函数体}(i);  创建一个匿名函数并调用它,可以传入参数。那么变量func中保存着的是匿名函数执行的结果。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值