Web 前端开发

函数作用域与闭包

2017-11-22  本文已影响0人  黎贝卡beka

函数作用域

要理解闭包,必须从理解函数被调用时都会发生什么入手。

我们知道,每个javascript函数都是一个对象,其中有一些属性我们可以访问到,有一些不可以访问,这些属性仅供JavaScript引擎存取,是隐式属性。[[scope]]就是其中一个。
[[scope]]就是我们所说的作用域,其中存储了执行期上下文的集合。由于这个集合呈链式链接,我们把这种链式链接叫做作用域链。

当函数被定义(创建)时有一个自己所在环境的作用域(GO全局作用域 ,若是在函数内部,就是引用别人的作用域),当函数被执行时,会将自己的独一无二的AO(活动对象,是使用arguments和该函数内部的变量值初始化的活动对象)执行上下文放在前端,形成一个作用域链;当该函数执行完,自己的AO会被干掉,回到被定义时的状态。

另外,变量的查找,就是找所在函数的作用域,首先从作用域的顶端开始查找,找不到的情况下,会查找外部函数的活动对象,依次向下查找,直到到达作为作用域链终点的全局执行环境。

下面看几个查找变量例子,深入理解函数作用域及作用域链。

function a(){
   function b(){
      var b=2223;  
   }
   var a=78;
}
a()
b()
console.log(b)

输出结果: error: b is not defined
当函数a执行完毕后,该函数内部的活动对象AO就会被销毁。所以函数外部是访问不到函数内部的变量的。


function outer(){
   function inner(){
      var b=2223;   
      a=0
   }
   var a=78;
   inner() //①
   console.log(a) 
   console.log(b)
}
outer() 

输出结果: 0 , error: b is not defined

当函数inner在被定义的阶段,就会拥有(引用)函数outer的作用域(包括函数outer自己局部的活动对象AO和全局作用域);当函数inner()被执行的时候,会再创建一个自己的活动对象AO并被推入执行环境作用域链的前端。
inner()函数在被执行的时候,由于变量a在outer()函数中已经存在并被inner()引用,所以inner()函数内部的变量a会修改掉外部函数变量a的值,并且可以不用声明。当inner()函数执行完毕后(执行到①处),inner()函数局部的AO会被销毁,下面就访问不到变量b了,而且这时候变量a的值将是被inner()函数修改过的值。


function a(){
  function b(){
     var b=2223;   
     a=0
  }
  var a=78;
  b=1
  b()
  console.log(b)
}
a()

输出结果: 1


var x=10;
function a(){
    console.log(x);
}
function b(){
    var x=20;
    a();
}
a();//10
b();//还是10;

总之:函数在被定义阶段,会引用着其所在环境的作用域;执行阶段,会创建一个自己独一无二的活动对象AO,并推入执行环境作用域链的前端;函数执行完毕之后,自己的执行上下文AO会被销毁,所以,这时候访问其内部的变量是访问不到的。但是,闭包的情况又有不同。

简述什么是闭包

闭包:有权访问另一个函数作用域中的变量的函数。

从理论的角度上,所有的JavaScript函数都是闭包,因为函数在被定义阶段就会存储一个自己所在环境的作用域,可以访问这个作用域中的所有变量。可以说,闭包是 JS 函数作用域的副产品。理解js作用域,自然就明白了闭包,即使你不知道那是闭包。

从技术实践的角度,以下函数才算闭包:

  1. 定义该函数的执行环境的作用域(执行上下文)即使被销毁 ,它的活动对象(AO)仍然会留在内存中,被该函数引用着。
  2. 引用了函数体外部的变量。

创建闭包常见方式,就是在一个函数A内部创建另一个函数B,然后通过return这个函数B以便在外部使用,这个函数B就是一个闭包。

举个例子:

//也算闭包
var a = 1;
function foo() {
    console.log(a);
}
foo();

//函数内部定义函数
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()()  //这里相当于:
//var foo = checkscope();
//foo();

输出结果:local scope
f()函数在被定义阶段就被保存到了外部,这个时候就相当于外部的函数可以访问另一个函数内部的变量,f()函数会形成一个闭包。

按照函数作用域的概念,当checkscope()执行完毕后,其局部的活动对象AO会被销毁;但是由于checkscope()函数执行完毕后返回一个函数,根据函数在被定义阶段会引用该函数所在执行环境的执行上下文,被返回的函数f()即使被保存到了外部依然引用着checkscope()函数的执行期上下文,直到函数f()执行完毕,checkscope()函数的执行上下文才会被销毁。

也就是说被嵌套的函数f()无论在什么地方执行,都会包含着外部函数(定义该函数)的活动对象。所以,即使f()被保存到外部,也可以访问到另一个函数checkscope()中定义的变量。

无论通过何种手段将内部函数传递到所在的词法作用域以外, 它都会持有对原始定义作用域的引用, 无论在何处执行这个函数都会使用闭包。

由此看来,闭包可能会导致一个问题:导致原有作用域链不释放,造成内存泄漏(内存空间越来越少)。可以通过手动将被引用的函数设为null,来解除对该函数的引用,以便释放内存。

闭包的作用

  1. 实现公有变量。
function a(){
   var num=100;
   function b(){
      num++;
      console.log(num)
   }
   return b;
}

var demo=a();
demo();//101
demo();//102
function test(){
   var num=100;
   function a(){
      num++;
   }
   function b(){
      num--;
   }
   return [a,b]

}

var demo=test()
demo[0]();//101
demo[1]();//100
//函数a和函数b引用的是同一个作用域。
  1. 实现私有变量。

闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。
通过在立即执行函数中return 将方法保存到外部等待调用,内部的变量由于是私有的,外部访问不到,可防止污染全局变量,利于模块化开发。

var foo = ( function() { 
   var secret = 'secret'; 
   // “闭包”内的函数可以访问 secret 变量,而secret变量对于外部却是隐藏的 
   return { 
      get_secret: function () { 
         // 通过定义的接口来访问 secret 
            return secret; 
      }, 
      new_secret: function ( new_secret ) { 
         // 通过定义的接口来修改 secret 
         secret = new_secret; 
      } 
   }; 
} () ); 
foo.get_secret (); // 得到 'secret' 
foo.secret; // undefined,访问不能 
foo.new_secret ('a new secret'); // 通过函数接口,我们访问并修改了secret 变量 
foo.get_secret (); // 得到 'a new secret'
var name='bcd';
var init=(function (){
   var name='abc';
   function callName(){
      console.log(name);
   }

   //其他方法

   return function () {
      callName();
      //其他方法
   }
}())

init () //abc

闭包经典题

function createFunctions(){
  var result = new Array();
  for (var i=0; i < 10; i++){
    result[i] = function(){
      console.log(i);
     };
  }
return result;
}

var fun = createFunctions();
for(var i=0;i<10;i++){
    fun[i]();
}

输出结果:打印十个10
数组每个值都是一个函数,每个函数对createFunctions()形成一个闭包,此时i都是引用createFunctions()中同一个i变量。

function test(){
   var arr=[];
   for(var i=0;i<10;i++){
      (function(j){
         arr[j]=function(){
            console.log(j);
         }
      }(i))
   }
   console.log(i)//10 ,i还是10
   return arr
}
var myArr=test();
for(var i=0;i<10;i++){
   myArr[i]()
}

输出结果:从0到9
这次依然把数组每个值赋为函数,不同的是循环十次立即执行函数,并将当前循环的i作为参数传进立即执行函数,由于参数是按值传递的,这样就把当前循环的i保存下来了。

闭包中的this对象

在闭包中使用this对象可能会导致一些问题,结果往往不是预想的输出结果。
看个例子:

  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());

输出结果:The Window

this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。匿名函数往往具有全局性,这里可以这样理解,没有任何对象调用这个匿名函数,虽然这个匿名函数拥有getNameFunc()的执行上下文。

因为这个匿名函数拥有getNameFunc()的执行上下文,通过把外部函数getNameFunc()作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问到该对象了。

  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());

输出结果:My Object

理解到这里,基本上就搞定了闭包了。

学习资料

JavaScript 里的闭包是什么?应用场景有哪些?

上一篇下一篇

猜你喜欢

热点阅读