首页 > 网站 > WEB开发 > 正文

对js闭包的粗浅理解

2024-04-27 14:06:52
字体:
来源:转载
供稿:网友

对js闭包的粗浅理解

  只能是粗浅的,毕竟js用法太灵活。

  首先抛概念:闭包(closure)是函数对象与变量作用域链在某种形式上的关联,是一种对变量的获取机制。这样写鬼能看懂。

所以要大致搞清三个东西:函数对象(function object)、作用域链(scope chain)以及它们如何关联(combination)

  首先要建立一个印象,在js中,几乎所有的东西可以看作对象,除了null和undefined。比如常用的数组对象、日期对象、正则对象等。

    var num = 123; // Number    var arr = [1,2,3]; // Array    var str = "hello";  // String

  数字原始值可以看作Number对象,字符串原始值可看做String对象,数组原始值可看作Array对象。有的原始值还可直接调方法,如数组、正则,有的不行

    [1,2,3].toString();  // 可以    //w+/.toString();  // 可以    123.toString();  // 报错

  当用一个变量名呈接它时,又能够这样用

    var num = 123;    num.toString();

  所以,函数也可以是一个对象,函数细说起来又要扯一大堆,毕竟是js精华,简单说这里有用的。函数的定义常见的是

    function fun1(val){  }    fun1(5);      var f1 = function(val){  };  // 定义一个函数赋给变量    f1(6);    var f2 = function fun2(){ }; // 或者取一个函数名    f2();

  因为函数也是对象、变量,所以可以赋给一个变量(更准确说是赋给左值),在这个变量后面使用()调用运算符就可以调用这个函数了,如f1()。还有一种方式:定义即调用

    var num = (function(val){ return val * val; }(5)); // num为25

  在定义一个函数体后面加上()和参数(或者没有参数),就是对这个定义的函数进行了调用,直接传入参数5计算并赋值给num,因此num是一个数值变量而不是函数变量。

既然函数是对象,当然也有new关键字的表达式

    var a = new Array(1,2,3);  // 数组的对象创建表达式    var f = new Function("x", "y", "x = 2 *x; return x + y;");  // 函数对象创建表达式        var f1 = function(x, y){  // 函数f的函数体类似f1的定义          x = 2 * x;          return x + y;    };

  函数对象的创建使用了Function关键字,前面的参数均被当做对象创建函数的形参,如这里的x、y,最后一个字符串是函数的函数体,多行函数体仍以;相隔。

很多时候特别是使用jQuery时经常看到函数调用时传递函数,大多数时候直接写匿名函数,也可可传递一个函数变量

    func(index, function(val){           /* 匿名函数 */    });    var f = function(val){ /* 函数变量 */ };   func1(index, f);

  既然函数函数是对象,可以赋给一个变量,自然也可以作为返回值了,而且在js中,函数可以嵌套定义。

    function func(){        return function(x){  // 返回一个函数变量            return x * x;        }    }    var f = func();    f(5);  // 对这个函数进行调用    function func1(){        function nested1(){   }  // 嵌套定义函数        function nested2(){   }    }

  对函数有个大致了解,说说变量作用域问题,有几个原则:

1. 全局变量拥有全局作用域.

2. 局部变量(一般指定义在函数内部)拥有局部作用域(包括其嵌套的函数).

3. 在局部变量若跟全局变量重名,优先使用局部变量.

    val = 'value';  // 变量定义可以不用var关键字    document.write("val=>" + val + "<br/>");    // g是全局变量,在全局作用域中有效,所以在给g初始化之前就可以访问,只是值是undefined    document.write("g=>" + g );    var g = 'google';

  全局变量的定义,相当于是全局对象的属性,一般我们用this指代这个全局对象,比如在浏览器中运行的时候,它指的是window对象,即当前窗体,在全局作用域中,以下三种访问形式等效

    var g = 'google';    document.write("g=>" + g + "<br/>");    document.write("g=>" + this.g + "<br/>");    document.write("g=>" + window.g + "<br/>");

  而在函数定义内部访问变量时,遵循同名优先,函数作用域内部总是优先访问,例如

    var scope = "global";    function func1(){        var scope = "local";        console.log(scope);  // local    };    func1();    function func2(){        scope = 'changed global';  // 如不加var,改变的是全局变量的值    };    func2();    console.log(scope);  // changed global

  比较有意思的地方是,如果在一个函数内部给一个全局变量赋值时没有加var关键字,如func2,它改变的是全局作用域变量的值!而前面说的this,也有个有意思的地方

    var scope = "global";    function func(){        var scope = "local"        console.log(scope); // local        console.log(this.scope);  // global    }    func();

  在局部作用域(这里均指函数内部),如果有同名变量,以this引用的话,结果是全局变量,即,在函数内部(注意不是方法,方法一般指对象的属性方法),this指代的是全局的对象。再看一个

    var scope = "global";    function func(){        "use strict";   // 开启ECMAScript5严格模式        console.log(this);  // undefined        console.log(this.scope);  // 报错:TypeError: scope undefined    }    func();

  在严格模式中,对语法检查更加严格。在一个全局作用域定义的普通函数中,log打印this是undefined,所以this引用scope当然也不存在。

在ECMAScript(为js脚本制定的一个标准)中,强制规定全局作用域定义的变量,是全局对象(比如在浏览器客户端运行时为window,默认的全局中的this关键字也是指它,通常我们说的就是这个)的属性。一般我们把跟某个变量作用域相关的对象为上下文环境(context),比如全局对象this只要涉及环境这类偏底层的东西肯定就是编程语言层面自己规定的。

  但这个全局对象this限于非严格模式的情况。js允许我们在局部变量的环境中(函数)以this引用全局对象,在严格模式下却没法这样干,也许是它把这个对象给隐藏了,至少目前这样写会报错。

  正是js的函数有局部作用域的特殊功能---全局作用域无法访问函数中定义的变量,所以在js中,也用函数规定命名空间,相比其他有的语言使用的是namespace关键字,js目前好像是把namespace作为保留字,你的变量的命令不能跟它重名,但没投入使用。

    (function(){        var name = "Jeff";        var age = 28;        var pos = "development";    }());  // 在函数中定义一堆变量,外边无法访问

  在一个函数中定义一堆变量,当然得调用它才能生效,这堆变量就限于在这个空间内使用了,好像用得也不多。

  一个重要的点,一个函数中规定的变量,在这个函数内部所有地方都可访问,包括嵌套函数。所以可以出现下面这个现象

    function func(){        console.log(num);  // undefined,先于定义访问        var num = 123;        console.log(num);  // 123    }    func();

  这种特性有时被称为声明提前(hoisting),相当于这样

    function func(){        var num;        console.log(num);  // undefined,先于定义访问        var num = 123;        console.log(num);  // 123    }

  然后是嵌套函数,只要在函数内定义的,该函数内均能访问,看例子

    function func(){        var str = "hello";        function nested1(){  // 第一次嵌套            str = "nested1";            function nested2(){ str = "nested2"; }  // 第二次嵌套            nested2();        }        nested1();        console.log(str);    }    func();  // 打印nested2

  除了明确在函数内定义的变量,还有定义函数时的形参,它们在整个函数内也是可访问的。

  现在我们大概了解函数也是对象,以及全局、局部作用域,在全局作用域定义的变量,是对应的全局对象的属性,也就是说有个全局对象关联它,就从这点进入作用域链(scope chain)吧,这是理解闭包的基础。

  在一个局部作用域内,或者说定义的函数,想象它们关联着某个对象,这个对象是随着我们定义这个函数而自动生成的,函数内定义的变量以及函数的形参均是这个对象的属性,所以在这个对象内部总是可以顺利访问到它们,类似于全局变量是全局对象的属性。这种对象关联在定义时就已经决定了,而不是在调用时才形成(这很重要)。

但是我们定义的很多函数都是嵌套的,由外到内每个函数都会有一个对应的自定义对象跟它关联

    var a = "a";    var name = "Michel";    function fun(b){        var c = "c";        var name = "Clark";        function nested(d){            var name = "Bruce";            var e = "e";            /* TODO */        }        /* TODO */    }

  在上面的嵌套函数nested生成一个自定义对象时,fun函数、全局作用域也会生成对象,因此它们可以形成一个对象的列表或者链表,简单的将函数名作为对象名,全局对象用global表示,并且从内嵌函数nested函数出发的话,大概是这样:

    

  列表上的一组对象定义了这段代码作用域中定义过的变量:即它们的属性。第一个对象的属性是当前函数的形参与内定义变量,第二个对象是它的外部函数的形参与内定义变量,一直到最后是全局对象和它的属性---全局定义的变量,也就是说,当前函数永远在这个列表的最前面,这样才可以保证该函数范围内的变量总是具有最访问高优先级。每次访问变量时便会顺着这个列表查找,这被称为变量解析(variable resolution),如果一直找到列表末尾都找不到对象中的这个属性,会抛一个ReferenceError错误。

  每个定义的函数对象都会有类似这样一个列表与之关联,它们之间通过这个作用域链相关联,而函数体内定义的变量均可保存在函数作用域内,这是在函数定义时及确定的,这种特性称之为闭包。 通常直接把函数称作闭包,而且理论上来说所有的js函数都是闭包,因为它们都是对象,闭包这种机制让js有能力来“捕捉”变量。第一个例子:

    var scope = "global";    function func(){        var scope = "local";        function nested(){ return scope; }        return nested();  // 调用并返回scope    }    console.log(func());

  常规的定义和调用,嵌套函数的定义并调用在局部作用域中完成。它打印的是local。再看这个

    var scope = "global";    function func(){        var scope = "local";  // 局部变量        function nested(){ return scope; }        return nested;  // 返回这个嵌套函数变量    }    console.log(func()());  // 在全局作用域中调用局部的嵌套函数

  前面说过,函数也是变量、对象,也可以作为函数返回值,func不直接返回变量,而返回一个内嵌函数,然后在全局作用域调用这个局部内嵌函数,它会返回什么呢?结果仍为local。由于闭包机制,nested函数在定义时就已经决定了,函数体内的scope变量值是local,这是一种绑定关系,不会随着调用环境的改变而改变,它去对象关联列表中查找按优先级分的话,总是func函数对应的scope值。

  闭包的功能很强大,看这个例子(例A

    var integer = (function(){            var i = 0;            return function(){ return i++; };        }());    console.log(integer());  // 0    console.log(integer());  // 1    console.log(integer());  // 2

  如果有点C/C++基础的人,初看这个调用结果,很可能会说这个打印的是0,0,0,反正我是很小白的这样想,每次调用完,临时变量i就会被销毁。但是确实有打印0的情况,看下这个(例B

    function func(){        var i = 0;        function nested(){ return i++; }        return nested();    }    console.log(func());  // 0    console.log(func());  // 0    console.log(func());  // 0

  也就是说,这两个看起来差不多的函数还是有点差别的。

在C/C++中,如果不是全局变量或局部静态变量,只要在局部函数中定义的变量在调用一次完成后就马上被销毁,当然除使用malloc、realloc、new等函数开辟的动态空间除外,这种必须得手动释放,否则容易造成内存泄露。js中是垃圾自动回收机制,某些无用的东西会被自动销毁,在前两个例子中,例A显然没有被销毁,而例B中的变量被销毁了,因为每次调用都是新声明一个i变量。so why?

  在C中,局部变量被临时保存在一个栈中,先调用的先入栈,后调用的后入栈,调用完从栈订弹出,变量内存被销毁,利用的是栈的后进先出特点。而js依靠的是作用域链,这是一个列表或者链表,并不是栈,没有所谓的压入(push)、弹出(pop)操作,如果说定义时就有一个列表的话,每次调用一个函数时,都会创建一个新的、跟它关联的对象,保存着局部变量,然后把这些对象添加至一个列表中形成作用域链,即便调用同一个函数两次,生成也是两个列表。

当一个函数执行完要返回的时候,便把对应对象从列表中删除,对象中的属性也会被销毁,意味着局部函数中的变量将不复存在,在例B中,return nested(),执行完返回一个值,nested函数再无任何作用,被从列表中删掉了。

  如果一个局部函数定义了嵌套函数,并且有一个外部引用指向这个嵌套函数,就不会被当做垃圾回收。什么时候会有一个外部引用指向它(内嵌函数)?当它作为返回值(即返回一个函数变量),或者它作为某个对象属性的值存储起来时。不会被当成垃圾回收,它绑定的对象也不会从对象列表中删掉,这个绑定对象的

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表