Javascript This.作用域.闭包

JavaScript是个有点神奇的语言,不过它的一些独有的特性往往让我们初学者感到费解。ThisJavascript语言的一个关键词。不过它到底是指什么呢?很多人都会认为this指的是当前对象。当然,这样理解是没错的,但是在有些情况下仍然会有些问题。在此,我搜集了一些资料,重新学习并整理一下,希望能借此来更好的理解this在JS中的工作方式和使用方法。

  1.  

    var test = function(){
        alert(this);
    }
    
    test();
    new test();

运行以上代码,你会发现test()和new test()的运行结果是不一样的,test()指向的是Windows对象而new test()才是指向test对象,为什么会有两种不同的运行结果?其实这里就涉及到一个变量作用域的问题,而变量作用域同时又牵涉到闭包(Closure)这个JS特性了,正因为闭包的存在,理解变量作用域就显得非常重要。

关于变量作用域

接着先来介绍一下所谓的变量作用域,概念非常简单,每个变量都有自己的作用域,即变量在这么一个区域中可以被识别,而出了此区域就没有任何作用了。

作用域就两种:全局作用域和局部作用域。
全局变量在JavaScript中处处都有定义,它贯穿在一个全局对象中,因此可以在任何地方使用。而局部变量则只在一个函数中有定义。其中要注意到的一点就是:JavaScript无块级作用域,这有别于C++和Java。因此任何变量在定义它的整个函数体中都能被识别。

  1.  

    var a = 123;
    function fun1() {
        alert(a);
    }
    
    fun1();//123

很简单,函数内部可以直接读取全局变量;

  1.  

    function fun2() {
        var a =123;
    }
    
    alert(a);//error

     

在函数外部当然无法读取函数内的局部变量。

var s = "oo";
function fun3() {
    alert(s);// undefined,他认为此时的s并没有被初始化
    var s ="xx";
    alert(s);// xx因为javascript无块级作用域,s在此被初始化,整个函数中都有定义
}

fun3();

以上代码其实就相当于

  1.  

    function fun3(){
        var s;
        alert(s);
        s = "xx";
        alert(s);
    }
    
    fun3();

     

另外大家要注意的一点就是,在函数内部声明变量的时候,如果不用var命令,这样其实就相当声明了一个全局变量。

关于闭包

了解过变量作用域之后,对于理解闭包就有很大的帮助了。当我们需要得到函数内的局部变量的时候,就需要在函数的内部再定义一个函数。

  1.  

    function fun1(){
        var a = 123;
    
        function fun2(){
            alert(a);//123
        }
    }

fun2可以访问fun1所有的局部变量,因此我们只要把fun2作为返回值,就可以在fun1外部读取到它的内部变量了。

  1.  

    function fun1(){
        var a = 123;
    
        function fun2(){
            alert(a);//123
        }
    
        return fun2;
    }
    
    var result = fun1();
    result();//因为调用fun1返回的是一个函数fun2,所以调用result是要加上()

因此,有了闭包将联系函数内外联系起来,我们就可以从函数外读取到函数内部的变量了,另外由于JavaScript作用域的机制,闭包只能读取到包含函数中任何变量的最后一个值。
这里引用一个例子:

  1.  

    function createFunctions() {
        var result = new Array();
        for(var i = 0; i < 10; i++) {
            result[i] = function(){
                return i;
            };
        }
    
        return result;
    }
    
    var funcs = createFunctions();
    
    for(var i = 0; i < funcs.length; i++) {
        alert(funcs[i]());//每次都显示10
    }

因为每个函数作用域中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i,当createFunctions()函数返回后,变量i的值是10,因此每个函数都引用着保存变量i的同一个变量对象,所以每个函数内部i的值都是10。我们要稍微改写一下函数,让它符合我们的预期要求。

  1.  

    function createFunctions() {
        var result = new Array();
        for( var i = 0; i < 10; i++) {
            result[i] = function(num){
                return num;
            }(i);
        }
    
        return result;
    }
    
    var funcs = createFunctions();
    for( var i = 0; i < funcs.length; i++) {
        alert(funcs[i]);//显式0,1,2,3,4,5,6,7,8,9
    }

这里,我们通过定义一个匿名函数,并立即执行该匿名函数的结果赋给数组,这里的匿名函数只有一个参数num,也就是最终要返回的值。在调用每个匿名函数时,我们传入了变量i,通过变量i的将当前值赋给参数num,这样就获得我们所要的结果。

清楚了作用域和闭包之后,对于this就能很快理解了。先来看下面这段代码:

  1. function test1() {
        this.x = 123;
        alert(this.x);
    }
    
    test1();//123

当前的this就是全局对象,稍作变化,效果一样。

  1. var x = 1;//x表示的是window.x
    function test() {
        this.x= 0;
    }
    
    alert(x);//1
    test();//此时的this就是window,所以执行过test方法之后,this.x= 0 就表示window.x= 0;他修改了原来的x=1
    
    //所以最终的结果为0
    alert(x);//0

再看如下代码:

  1. var oo = new Object;
    oo.x = 1;
    function test2() {
        alert(this.x);
    }
    
    oo.fun = test2;
    oo.fun();//1,因为是oo对象调用的fun方法,所以此时的this.x表示的就是oo.x

这里的this就指当前调用该方法的对象。
对于开头这段代码来说,test()函数是在全局作用域下的(在这里其实就是window对象),所以this的值是当前的window对象。而通过 new test()其实是作为构造函数来调用的,就是通过这个函数来生成一个新的对象,所以这里的this就指的是这个新对象。

  1. var xx = 2;
    function test3() {
        this.xx= 1;
    }
    
    var oo = new test3();
    alert(oo.xx);//1 oo对象引用了test方法,此时test方法中的this就表示的是oo对象
    alert(xx);//2

以上例子可证明this并不是全局对象,因为xx的值并没有改变。最后在网上看到两段例子,对于理解this和闭包有很大的帮助,在此引用过来。

代码一

var name = "The Window";
//此时的name的作用域为window,所以等价于window.name= "The Window";

var object = {
    name:"My Object",
    getNameFunc:function(){
        return function(){
            return this.name;
            //此时的this在匿名函数中,这个匿名函数没有调用的对象,因此它的作用域为window,
            //此时的this.name等价于window.name
            //即等同于第一行的name
        };
    }
};

alert(object.getNameFunc()());//返回的结果为The Window

代码二

var name = "The Window";
var object = {
    name:"My Object",
    getNameFunc:function(){
        var that = this;
        //此时的this属于getNameFunc函数,而getNameFunc函数只能被object调用,
        //即getNameFunc的调用者是object,所以此时的this表示object对象,
        return function(){
            return that.name;
            //由于getNameFunc中的this表示object对象,所以that也表示object对象,
            //所以that.name就等价于object.name 就是"My Object"
        };
    }
};

alert(object.getNameFunc()());//输出结果为My Object

三、闭包的概念

各种专业文献上的“闭包”(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

四、闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。
Js代码

function f1(){
    var n = 999;
    nAdd = function(){
        n += 1
    }

    function f2(){ 
        alert(n);    
    }

    return f2;
}

var result = f1();
result(); // 999
nAdd();
result(); // 1000

 

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

五、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

Js代码

 

function outerFun(){ 
    var a = 0; 
    function innerFun() {  
        a++;  
        alert(a); 
    } 
	
    return innerFun;  //注意这里
}

var obj = outerFun();
obj();  //结果为1
obj();  //结果为2
var obj2 = outerFun();
obj2();  //结果为1
obj2();  //结果为2

 

什么是闭包:

当内部函数 在定义它的作用域 的外部 被引用时,就创建了该内部函数的闭包 ,如果内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存不会被 释放,因为闭包需要它们.

再来看一个例子

Js代码

 

function outerFun(){ 
    var a =0; 
    alert(a);  
}
var a = 4;
outerFun();
alert(a);

 

结果是 0,4 .  因为在函数内部使用了var关键字 维护a的作用域在outFun()内部.

再看下面的代码:

Js代码 

function outerFun(){
    //没有var  
    a = 0; 
    alert(a);  
}

var a = 4;
outerFun();
alert(a);

结果为 0,0 真是奇怪,为什么呢?

 

作用域链是描述一种路径的术语,沿着该路径可以确定变量的值 .当执行a=0时,因为没有使用var关键字,因此赋值操作会沿着作用域链到var a=4;  并改变其值.

 

一、什么是闭包?

官方”的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
相信很少有人能直接看懂这句话,因为他描述的太学术。其实这句话通俗的来说就是:JavaScript中所有的function都是一个闭包。不过一般来说,嵌套的function所产生的闭包更为强大,也是大部分时候我们所谓的“闭包”。看下面这段代码:

 

function a() { 
    var i = 0; 
    function b() {
        alert(++i); 
    } 

    return b;
}

var c = a();
c();

这段代码有两个特点:

 

1、函数b嵌套在函数a内部;

 

2、函数a返回函数b。

引用关系如图:

  这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:

  当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。

  让我们说的更透彻一些。所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标 对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目 标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新 的值,和上次那次调用的是各自独立的。

二、闭包有什么作用?

  简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。这是对闭包作用的非常直白的描述,不专业也不严谨,但大概意思就是这样,理解闭包需要循序渐进的过程。

在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。

  那 么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引 用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)

三、闭包内的微观世界

  如果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。

  1. 当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
  2. 当执行函数a的时候,a会进入相应的执行环境(excution context)。
  3. 在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
  4. 然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
  5. 下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
  6. 最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。

到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。

当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:

如图所示,当在函数b中访问一个变量的时候,搜索顺序是:

  1. 先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。
  2. 如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
  3. 如果整个作用域链上都无法找到,则返回undefined。

小结,本段中提到了两个重要的词语:函数的定义与执行。文中提到函数的作用域是在定义函数时候就已经确定,而不是在执行的时候确定(参看步骤1和3)。用一段代码来说明这个问题:

function f(x) { 
  var g = function () { return x; }
  return g;
}
var h = f(1);
alert(h()); 

这段代码中变量h指向了f中的那个匿名函数(由g返回)。

  • 假设函数h的作用域是在执行alert(h())确定的,那么此时h的作用域链是:h的活动对象->alert的活动对象->window对象。
  • 假设函数h的作用域是在定义时确定的,就是说h指向的那个匿名函数在定义的时候就已经确定了作用域。那么在执行的时候,h的作用域链为:h的活动对象->f的活动对象->window对象。

如果第一种假设成立,那输出值就是undefined;如果第二种假设成立,输出值则为1。

运行结果证明了第2个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了。
 

四、闭包的应用场景
保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。

  1. 在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
  2. 通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)
    私有属性和方法在Constructor外是无法被访问的

    function Constructor(...) {  
      var that = this;  
      var membername = value; 
      function membername(...) {...}
    }

以上3点是闭包最基本的应用场景,很多经典案例都源于此。
 

五、Javascript的垃圾回收机制

在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。

 

六、结语

理解JavaScript的闭包是迈向高级JS程序员的必经之路,理解了其解释和运行机制才能写出更为安全和优雅的代码。

 

 

 

 

 

闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样.

  但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的理解以及js内部解释器的运作方式的描述,都是可以看出你js实际水平的.即使你没答对,也能让考官对你的水平有个评估.那么我先来说说我对js中的闭包的理解.

  闭包是很多语言都具备的特性,在js中,闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等.

  在理解闭包以前.最好能先理解一下作用域链的含义,简单来说,作用域链就是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引,而他内部的规则是,把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止.当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续.如果找到最后也没找到需要的变量,则解释器返回undefined.

  了解了作用域链,我们再来看看js的内存回收机制,一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了.对应的内存空间也就被回收了.下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用.但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的.并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题.如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了.所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来.也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收.

也就是说,有了闭包,嵌套的函数结构才可以运作,这也是符合我们的预期的.然后,闭包还有一些特性,却往往让程序员觉得很难理解.

看看下面一段代码.

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=function(){
            alert(i)
        }
    }
};
foo();
result[0](); // 3
result[1](); // 3
result[2](); // 3

 

这段代码中,程序员希望foo函数中的变量i被内部循环的函数使用,并且能分别获得他们的索引,而实际上,只能获得该变量最后保留的值,也就是说.闭包中所记录的自由变量,只是对这个变量的一个引用,而非变量的值,当这个变量被改变了,闭包里获取到的变量值,也会被改变.

解决的方法之一,是让内部函数在循环创建的时候立即执行,并且捕捉当前的索引值,然后记录在自己的一个本地变量里.然后利用返回函数的方法,重写内部函数,让下一次调用的时候,返回本地变量的值,改进后的代码:

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=(function(j){
            return function(){
                alert(j);
            };
        })(i);
    }
};
foo();
result[0](); // 0
result[1](); // 1
result[2](); // 2

在这里我再解释一下.这里用到了另外2个技术,立即调用的匿名函数和返回函数.也是初学者比较难以理解的部分.
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值