函数作用域与闭包

函数作用域

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

我们知道,每个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 里的闭包是什么?应用场景有哪些?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容