理解js中的作用域,作用域链以及初探闭包

source.gif
前言

对于js中的闭包,无论是老司机还是小白,我想,见得不能再多了,然而有时三言两语却很难说得明白,反正在我初学时是这样的,脑子里虽有概念,但是却道不出个所以然来,在面试中经常会被用来吊自己的胃口,考察基础,虽然网上自己也看过不少相关闭包的文章,帖子,但貌似这玩意,越看越复杂,满满逼格高,生涉难懂的专业词汇常常把自己带到沟里去了,越看越迷糊,其实终归结底,用杨绛先生的一句话就是:“你的问题在于代码写得太少,书读得不够多",其实在我看来前者是主要的,是后者的检验, 自知目标搬砖20年(还差19年..),其实,闭包的应用,在我们不经意间就使用了,是无处不在的,尽管知道闭包是个麻烦的玩意,自己也经常吃回头草看看这家伙的,所谓出去混,迟早是要还的,今天,我就对闭包的一点理解作一点点总结,关于闭包,我也一直在学习当中...话说多了,都是故事,直接撸起袖子,开始干吧

正文从这里开始~

理解上下文和作用域

其实上下文与作用域是两个不同的概念,有时我自己也经常混淆,把它们视为是同一个东西,我们知道函数的每次调用都会有与之紧密相连的作用域和上下文,从本质上说,作用域其实是基于函数的,而上下文基于对象的,也就是说作用域是涉及到它所被调用函数中的变量访问,而调用方法和访问属性又存在着不同的调用场景(4种调用场景,函数调用,方法调用,构造器函数调用,call(),apply()间接调用),而上下文始终是this所代表的值,它是拥有控制当前执行代码的对象的引用

变量作用域

在javascript中,作用域是执行代码的上下文(方法调用中this所代表的值),作用域有三种类型:全局作用域(Global scope),局部作用域(Local/Function scope,函数作用域)和eval作用域,在函数内部使用var定义的代码,其作用域都是局部的,且只对该函数的其他表达式是可见的,包括嵌套子函数中的代码,局部变量只能在它被调用的作用域范围内进行读和写的操作,在全局作用域内定义的变量从任何地方都是可以访问的,因为它是作用域链中的最高层中的最后一个,在整个范围内都是可见的,注意在Es6之前是没有块级作用域的,而Es6后是有的,也就是说Esif,while,switch,for语句是有了块级作用域的,可以使用let关键字声明变量,修正了var关键字的缺点,注意let使用规则
看如下代码所示:

              * 全局变量与局部变量
              * 
              * @global variable {variable="itclab"}
              * @function myFun 
              * @local variable {variable="itclanCode",variable=24}
              * @function otherFun
              * @eval作用域 evalfun
              */
              var variable = "itclan";        //全局变量
              console.log("全局variable","=",variable); // 全局variable = itclan
              // 函数表达式
              var myFun = function(){
                 var variable = "itclanCode"; //局部变量
                 console.log("局部variable","=",variable); // 局部variable = itclanCode
                 var otherFun = function(){
                     var variable = 24;       //局部变量
                     console.log("局部variable","=",variable);  // 局部variable = 24
                 }
                 otherFun();
              }
              myFun();
              eval("var evalfun = 20;console.log('evalfun作用域','=',evalfun)");// evalfun作用域 = 20

01.png

注意

  • 函数可以嵌套函数,并可以无限的嵌套下去,也就是可以创建无数的函数作用域和eval作用域,而javascript坏境只是用一个全局作用域
  • 全局作用域(global scope)是作用域链中的最后一层
  • 包含函数的函数,会创建堆栈执行的作用域,这些链接在一起的栈通常被称为作用域链(也就是后面会提到闭包产生的本质原因)
什么是执行坏境

所谓执行坏境,它定义了变量或函数有访问的其他数据的能力,它决定了各自的行为,它的侧重点在于函数的作用域,而并不是所要纠结的上下文,一旦函数一声明定义,就会自动的分配产生了作用域,有着自己的执行坏境,执行坏境可以分为创建与执行两个阶段,在创建阶段,js解析器首先会创建一个变量对象(活动对象),它由定义在执行坏境中的变量,函数声明和参数组成,在这个阶段,系统会自动的产生一个this对象,作用域链会被初始化,随之,this的值也会被确定,第二阶段,也就是代码执行,代码会被解释执行,你会发现,每个执行坏境都有一个与之关联的变量对象,执行坏境中所有定义的变量和函数都保存在这个对象中,注意,我们是无法手动的访问这个对象的,只有js解析器才能够访问它,其实也就是this,尽管很抽象,但是理解它还是蛮重要的

作用域链(词法作用域)

javascript查找与变量相关联的值时,会遵循一定的规则,也就是沿着作用域链从当前函数作用域内逐级的向上查找,直到顶层全局作用域结束,若找到则返回该值,若无则返回undefined,这个链条是基于作用域的层次结构的,一旦当代码在坏境中执行时,会自动的创建一个变量对象的作用域链,其作用域链的用途也就是保证对执行坏境的全局变量和具有访问权限函数内的局部变量定制特殊的规则,由内到外有序的对变量或者函数进行访问,作用域链包含了在坏境栈中的每个执行坏境对应的变量对象,通过作用域链可以决定变量的访问与标识符的解析,如下代码所示:

               * 作用域链变量的访问
               *
               * @global variable {name="随笔川迹"}
               * @function fun1,fun2
               * @local variable {oTherName="哇嘎嘎",AliasName = "川川"}
               * @return {fun2,name,oTherName,AliasName}
               * @return fun2,fun1函数的返回结果值为fun2的值
               *
               *
               */
               var name = "随笔川迹";               // 全局变量
               var fun1 = function(){
                   var oTherName = "哇嘎嘎";        // 局部变量
                   var fun2 = function(){
                       var AliasName = "川川";      // 局部变量
                       AliasName = oTherName;
                       oTherName = AliasName;
                       return {name,oTherName,AliasName};
                   }
                   console.log(fun2());
                   return fun2();
               }
               //console.log(fun2()); // 若在全局作用域调用访问fun()会失败,显示fun2 is not defined
               console.log(fun1(),"name is","=",name)
02作用域链.png

当我们分析这段代码时,首先全局范围全局变量name,函数fun1嵌套fun2函数,fun1,fun2函数内局部变量分别为:oTherName,AliasName当在函数fun2内,并未声明name变量

便在该函数fun2内进行了访问,这是如何找到的?javascript首先在当前fun2函数作用域内查找一个名为name的变量,但是在fun2并未找到,于是它会查找它的父函数fun1的作用域内进行查找,但是发现仍然没有找到,于是在往外进行查找,结果在全局作用域范围内查找了name的值

于是找到了便把该值进行返回,若是在全局作用域内还未找到则会返回undefined,注意在函数fun2作用域内,name,oTherName,AliasName都是可以访问的,而在函数fun1函数作用域内是访问不了oTherName的,因为它脱离了fun1的函数的作用域嘛,我们知道在函数外是无法访问函数里面的的变量的,访问变量由内向外进行查找是可以的,但是反之则就不行,从上图的箭头分析图可知,内部坏境中,是可以通过作用域链访问它所有的外部坏境

但是在外部坏境是无法访问内部坏境中的任何变量和函数,这点很重要,我们在函数嵌套函数,并且进行函数调用时,要格外注意,如果在编程当中出现这种函数is not defined那么就是牵扯到函数作用域的问题了,在函数外是无法访问函数内的变量或者函数的,当然这种问题是可以解决的,也就是后面提到的闭包

其实上面我们的代码中就已经无形用了闭包,匿名函数fun1,fun2就是个闭包,嵌套函数与被嵌套坏境的连接是线性的,有次序的,对于标识符(也就是变量或者函数名查找)是从当前函数作用域开始,沿着作用域链逐级的向上查找,直到最顶端全局变量坏境,若找到该值则返回,若无则返回undefined

注意:理解作用域以及作用域链对理解原型链是很有帮助的,其实他们区别并不是很大,两者都是通过位置体系(上下嵌套关系)和分层体系来查找值的方法,进而可以对变量或者函数进行读和写的操作,如下代码所示:


              var x = 5;
              var fun1 = function(){
                 var y = 10;
                 var fun2 = function(){
                     var z = 20;
                     return z+y+x;
                 }
                 fun2();
                 return fun2();
              }
              console.log("x+y+z的和=",fun1()); //x+y+z的和= 35
javascript没有块级作用域

在Es6之前,如if,for,while,switch逻辑语句是无法创建作用域,也就是它后面的双大括号并没有域的作用,这才得式变量可以相互覆盖,解决办法,你可以使用es6的let关键字声明变量,注意let的使用,如下代码所示


              var str = "itclan";     // 全局变量
              console.log(str);       // itclan
              if(true){               // if逻辑语句
                 str = "itclanCode"; 
                 console.log(str);    // itclanCode
                 for(var i = 0;i<=2;i++){
                    str = i;
                    console.log(str); // 0,1,2
                 }
                   console.log(str);    // 2
              }
              console.log(str);       // 2

因此,代码在执行过程中,从上到下,str是变化的,因为在Es6之前,没有块级作用域,只有全局作用域,函数作用域,eval()作用域
注意
在函数中应用var声明变量,避免作用域的陷阱
javascript会将缺少var的变量声明,即便在函数或者封装在函数中,都会视为全局变量作用域,而非局部作用域,我们是不应该出现这种不要var声明的,这样会造成全局变量的污染,易混淆,如下代码所示

             * 如果不使用var来声明变量,那么,该变量实际上是在全局作用域中定义,而不是局部作用域中定义(它本是在局部作用域中定义)
             * 
             * @descortion:这样很容易产生误解,应当杜绝这么干
             * @在函数内定义的变量应用var,当然要在函数内部创建或更改全局作用域内的属性就另当别论了的
             *
             *
             */
             var fun1Exp = function(){
                 var fun2Exp = function(){
                     name = "污葵"; // 没有使用var,它相相当于window.name
                 }
                 fun2Exp();
             }
             fun1Exp();
             console.log({name});
             // 相反,使用var的情况
             var fun3Exp = function(){
                 var fun4Exp = function(){
                     var age = 20; //使用var,局部变量
                 }
                 fun4Exp();
             }
             fun3Exp();
             console.log(age);  //Uncaught ReferenceError: age is not defined,报错的原因,age在fun4Exp函数作用域中,在函数外是访问不了函数内部的变量的
作用域是在函数定义时就确定的,而非调用时确定

因为函数决定作用域,又因为函数也是对象,也是一种数据类型,一样可以像基本数据类型值一样被作为值来传递,作用域就是根据函数定义时的位置确定的,而与该函数在哪里被调用无关,其实就是词法作用域,作用域链是在调用函数之前创建,也是这样,我们就可以创建闭包,我们常常是这么做的,让函数向全局作用域返回一个嵌套函数,但该函数仍然能够通过作用域访问它父函数的作用域,作用域链是在定义时确定的,并在函数内部传递代码不会改变作用域
如下代码所示:

 
              * 作用域链是在函数定义时位置确定的,而非函数调用位置,在函数内部传递代码不会改变作用域链
              *
              * @funtion expression parentFun
              * @local variable localVal
              * @return parentFun的返回值为一个匿名函数,访问该匿名函数外的变量
              *
              */
              var parentFun = function(){
                  var localVar = "itclan是个有温度的公众号";
                  return function(){  // 返回一个匿名函数
                      console.log(localVar);
                  }
              }
              var nestedFun =  parentFun();//nestedFun引用parentFun函数,把函数parentFun函数的返回值赋值给变量nestedFun
              nestedFun();   // 输出itclan是个有温度的公众号,因为返回的函数可以通过作用域链访问到localVar变量
产生闭包的根本原因是作用域链

在通过上面的了解变量的作用域和作用域链后,相信你理解闭包就不难了,如下代码所示:

              * 闭包是由作用域链引用的
              *
              * @function expression countNum 匿名函数 
              * @local variable count
              * @return 匿名函数
              * 
              */
              var countNum = function(){
                  var count = 0;
                  return function(){  //调用countNum的时候返回嵌套的子函数
                     return ++count;// count在作用域链内定义,父函数里
                  };

              }(); // 匿名函数的立即调用,返回嵌套函数
              // countNum(),上面的匿名函数后若不加括号调用,则返回的结果将是return 后面的函数的整体代码
              console.log(countNum());   // 1
              console.log(countNum());   // 2
              console.log(countNum());   // 3
03产生闭包的原因是作用域链.png

当每次调用countNum函数时,嵌套在该函数内的匿名函数是可以访问父函数(这里指的是countNum的)作用域的,其实这就是所谓的闭包,作用链就是闭包的桥梁,用来连接内部函数与外部函数的关系,从而达到外部函数访问内部函数局部变量或者函数的目的,其中被嵌套函数就可以称为是一个闭包
小结

  • 产生闭包的原因是由作用域链引起的
  • 函数嵌套函数,被嵌套的函数就可以称为闭包
  • 子函数可以使用父函数的变量(访问其他函数内部的局部变量)
  • 让变量始终保存在内存中,避免自动垃圾回收(其实上面的例子中就已经用到了的)
  • 对外提供公有属性和方法

总结:

整篇文章从理解上文和作用域开始,以及什么是执行坏境,其产生闭包的原因是作用域链,并知道在Es6之前是没有块级作用域的概念的,并且作用域是在函数定义时就确定的,而非函数调用确定,在我的理解中编程其实很大一部分就是对数据进行读和写的操作

其中读可以理解对定义变量数据的访问,而写可以理解赋值,引用,变更,改写操作,当然js中不像其他后台语言的存储数据类型那般复杂,基本就是基本数据类型和对象了

理解作用域以及作用域链对理解闭包是相当的重要,对后续的原型链以及继承都是相关联的,其实也不必抓着什么执行坏境和上下文这些相对抽象的概念不放,我们只有在平时的使用当中,稍稍留意就行,在应用中结合理论进行验证,当然闭包的内容远不及此..

以下是本篇提点概要
  • 理解上下文和作用域,作用域是基于函数的,而上下文是基于对象的,虽然说函数也是对象,但是这里更多的是指对象直接量的表示法,上下文始终围绕着this所代表的值,它是拥有控制当前执行代码对象的引用
  • 变量的作用域,在Es6之前没有块级作用域,而Es6有了块级作用域,也就是if,,while,switch,for,若使用let关键字,则具备块级作用域,也就是说定义在双大括号内的变量,在双大括号内的才起作用,一旦离开该范围,就不起作用了
  • 什么是执行坏境,定义了变量或函数有访问的其他数据的能力,它决定了各自的行为,它的侧重点在于函数的作用域,而并不是所要纠结的上下文,分为创建坏境和执行坏境
  • 作用域链(词法作用域),当查找与变量相关联的值时,会遵循一定的规则,也就是沿着作用域链从当前函数作用域内逐级的向上查找,直到顶层全局作用域结束,若找到则返回该值,若无则返回undefined
  • javascript没有块级作用域,往往很多时候使用匿名函数自执行来模拟块级作用域
  • 作用域是在函数定义时就确定的,而非调用时确定,作用域就是根据函数定义时的位置确定的,而与该函数在哪里被调用无关,其实就是词法作用域
  • 产生闭包的根本原因是作用域链,见上小结
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容