JavaScript之函数定义与闭包

来源:互联网 发布:淘宝运动品牌店铺排名 编辑:程序博客网 时间:2024/05/17 14:25

函数表达式

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

函数的定义

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

函数声明

函数声明的语法:

function functionName () {    //函数体}

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

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


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

console.log(sum(2, 3)); //5function 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  functionvar 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]()); //5console.log(arr[1]()); //5console.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]()); //0console.log(arr[1]()); //1console.log(arr[4]()); //4

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

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


1 0
原创粉丝点击