008 - 变量的作用域链和闭包

当我们了解函数之后,接下来说一下,和函数有关的变量作用域以及闭包问题

执行上下文

这一部分 : 参考网上 “深入理解javascript之执行上下文(execution context)” 。该博文对于 执行上下文对象的描述较为详细,不过略显啰嗦,简单整理,突出一下 什么是 执行上下文,在JavaScript引擎中如何设计的,函数调用的时候,上下文对象的创建过程

  • 什么是执行上下文

    调用一个函数的时候就会创建一个函数的环境,我们可以称之为执行环境,或者叫执行上下文, 每一个函数都会有 “执行上下文”

    “执行上下文” 定义了变量或者函数有权访问其他数据, 每个“执行上下文” 中有与之关联的 ”变量对象“, 在这个变量对象中,保存着上下文中定义的所有变量和函数。

    类似Java中的方法栈,当执行一个方法时,该方法的变量对象就会推到作用域链的前端

    function f1(){ 
      var a = 12;  
      var f = function(){
        	var b = 20;
      } 
    }
    //a 和 f 就在 f1 的变量对象中,
    //b 在函数f的变量对象
    
  • Js中方法调用

    在Js中,按照代码执行环境分 ,可以分为下边三种
    全局代码 、函数内局部代码、eval()中文本

    JavaScript解释器在浏览器中是单线程 ,在同一时间只会有一个事件执行,其他事件在此时会处于 排队状态 , 将其理解为执行栈

    比如 :
    1. 浏览器首次加载js代码,默认进入全局执行环境,执行全局代码
    2. 全局代码中,如果调用了函数,会把这个函数的执行上下文压入执行栈,代码顺序流会进入这个被调用的函数,进入局部代码环境
    3. 如果在局部代码环境中,再次调用函数,会重复第二步操作,将新调用的函数压入执行栈,顺序流进入新的被调用的函数
    4. 一旦 ,当前执行环境结束了,它就会弹出栈的顶部,把控制权交给栈中的下一个函数。

    以下边代码为例,说明代码执行顺序 :

    (function foo(i) {
    	if (i === 3) {
    		return;
    	}
    	else {
    		foo(++i);
    	}
    }(0));
    

    这段代码 简单地调用了自己三次,由1递增i的值。每次函数foo被调用,一个新的执行环境就会被调用。一旦一个环境完成了执行,它就会被弹出执行栈并且把控制权返回给当前执行环境的下个执行环境直到再次到达全局执行环境,这个过程示意图如下 :
    在这里插入图片描述

  • 执行上下文的建立过程

    每当调用一个函数时,一个新的执行上下文就会被创建出来。然而,在javascript引擎内部,这个上下文的创建过程具体分为两个阶段:

    阶段一 : 建立
    发生在调用函数后,执行函数内具体代码之前。主要工作是: 建立变量、函数、arguments对象、参数 ; 建立作用域链 ; 确定this的值(是哪个对象)
    阶段二 :
    变量赋值、函数引用、执行其他代码

  • 执行上下文对象

    实际上可以把执行上下文看做一个对象,包含是下边三个属性

    (executionContextObj = {
      variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
      scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
      this: {}
    }
    
  • 创建上下文对象的过程

    函数被调用,具体代码执行之前(就是上边阶段一:建立阶段),创建上下文对象(executionContextObj

    1. 建立阶段具体过程 :

      建立阶段发生在 : 调用函数后,执行具体函数体内代码之前

      1. 建立 variableObject 对象 :
        1. 建立arguments对象,检查当前上下文中参数,建立该对象下的属性以及属性值
        2. 检查当前上下文中的函数声明
          每找到一个函数声明,就在variableObject 下边用函数名建立一个属性,属性值指向该函数在内存中的地址的一个引用
          如果上述函数名已经存在与variableObject下,那么对应属性值会被新的引用刷新
      2. 初始化作用域链 (scopeChain)
      3. 确定上下文中 this的指向对象
    2. 代码执行阶段
      执行函数体中的代码,一行行的运行代码,给variableObject中的变量属性赋值

  • 创建上下文的代码示例

    下边代码中创建了一个函数,并在全局环境下调用函数

         function foo(i) {
             var a = 'hello';
             var b = function privateB() {
         
             };
             function c() {
         
             }
         }
         
         foo(22);
    
    1. 调用foo(22)时,创建上下文对象
           fooExecutionContext = {
             variableObject: {
                 arguments: {
                     0: 22,
                     length: 1
                 },
                 i: 22,
                 c: pointer to function c()
                 a: undefined,
                 b: undefined
             },
             scopeChain: { ... },
             this: { ... }
         }
    

    可以清晰的看出,调用函数 建立阶段 :创建了上下文对象,并且对 arguments对象、传参、还有一些函数的声明 进行了赋值。

    剩下的 比如函数体内的局部参数只有声明,值是undifined的 , 这些是在函数代码执行阶段进行赋值 (注意 : 此时b是undifined , 执行代码阶段才赋值一个引用指向 一个函数) ,如下所示

        
    
         fooExecutionContext = {
             variableObject: {
                 arguments: {
                     0: 22,
                     length: 1
                 },
                 i: 22,
                 c: pointer to function c()
                 a: 'hello',
                 b: pointer to function privateB()
             },
             scopeChain: { ... },
             this: { ... }
         }
    

上边核心内容总结 :

  • 执行上下文对象是 : 调用函数时,创建的函数执行环境。全局代码是全局环境,调用函数时,转入具体的函数执行上下文,将其推入执行栈顶部,执行完成,出栈
  • 执行上下文对象 : 从Javascript引擎中看 是一个 如下对象
    (executionContextObj = {
      variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
      scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
      this: {}
    }
    
  • 函数调用时,执行上下文创建过程 主要分两步 , 第一步 :未调用具体代码之前准备工作 , 主要把arguments对象、传参、这个函数内一些函数的声明进行赋值, 初始化作用域链,确定this ; 第二步 : 执行函数体内具体代码,对一些局部变量进行赋值

接下来: 展开说,具体说一下,JavaScript中函数变量作用域链,为什么出现变量作用域提升

作用域链

上边提到,创建上下文对象,会初始化作用域链, 接下来 说一下 什么是作用域链,它的目的是什么

  • 什么是作用域链

    举例来说 : 在函数 outer()中定义了另一个函数 inner(),那么,在 inner()中可以访问的变量既来自它自身的作用域,也可以来自其“父级”作用域,形成了一条作用域链( scope chain)

    通俗的讲 , javaScript中变量的作用域是函数级 , 每个函数作用域都是封闭的,即外部是访问不到函数域中的变量。 作用域链就是处理变量访问相关的, 比如查找一个变量, 先在当前函数中找,找不到再去找上级函数,一直到全局 。

    这就是作用域链,一个对象列表或者链表,Js变量解析时(就是找一个变量的值)从作用域链的第一个对象开始查找,如果对象中有这个变量的值,就会使用,没有的话就去找链中下一个对象 (上边说的父级函数的”变量对象“),如果作用域链中没有任何一个对象有这个变量的值,就会报出一个引用错误

    比如 :下边代码中,在console.log(b) 执行时,去作用域链中找,代码执行环境是全局代码,作用域链中只有一个全局变量对象,而且此对象中没有变量b,所以报错 ”ReferenceError: b is not defined“

     var a = 1;
    
    function f() {
      var b = 1;
      return a;
    }
    f();
    //console.log(b); //ReferenceError: b is not defined
    
  • 注意 :javaScript中存在一个问题 , 不用var会成为全局变量

    function f1(){a = 1} ; f1(); console.log(a) //输出1
    
  • 作用域链的创建

    当定义一个函数时,实际上保存一个作用域链。

    当调用这个函数时,它创建一个新的对象来储存它的参数或者局部变量 (变量对象),并且将这个对象添加到作用域链上

    嵌套函数来说,每次调用外部函数时,内部函数又重新定义一遍。所以,每次调用外部函数时,作用域链都是不同的

  • 变量作用域使用规则
    如下代码,变量作用域链 是 f3() -> f2 () ->f1()->window , 也就是在代码1处,是从f1()->window ,然而并没有找到b,所以会报错,找不到b

     function f1(){
     	var a = 1;
     	function f2(){
     		var b = 2;
     		function f3(){
     			var c = 2;
    		}
    		console.log(b);
    		console.log(c);
    		f3();
    	}
    	console.log(a);
    	console.log(b); // 代码 1
    	f2();
     }
    
闭包
  • 什么是闭包

    因为Javascript中变量访问是利用作用域实现的,从上边的例子中,我们已经看到了,函数外无法使用函数内的变量,而 闭包就是”有权访问另一个函数作用域中的变量的函数“

  • 闭包的特点

    正常形式下,函数执行结束后,这个函数的作用域的”变量对象“就会销毁。利用闭包的写法,可以创建了一个不销毁的作用域
    让局部变量一直保存在内存中;获取函数内部的局部变量

  • 判断是否为闭包

    var a = 2;
    var b = 0;
    function abc(c){
        var b = 1;
        return a+b+c;     
    }
    abc(3) 
    // 这并不属于闭包 :理由是没有保持局部变量
    
  • 闭包的写法

    1. 直接返回局部函数,,然后就可以拿到函数内部的值

      // 1. 直接返回局部函数
      function f1(){
        var local_variable = '1243';
        return function (){
          return local_variable;
        }
      }
      var innerf = f1();
      var inner_local_val = innerf();
      
    2. 函数体内局部函数赋值给函数外部变量

      var globalf ;
      function f2(){
        var local1 ='222';
        globalf = function(){
          return local1;
        }
      }
      f2();
      var inner_local_val = globalf();
      

    上边的写法,本质上是一样的,比如 想要返回函数f()内的局部变量a , 就在f()在写一个函数,让这个函数返回这个变量a , 然后设法在函数f() 保存一个变量指向f()的函数的引用 。 第一种,是直接返回函数,可以通过调用函数拿到这个内部函数引用 ; 第二种 , 用一个函数外变量,在函数内赋值成新函数的引用

  • 闭包会导致作用域被保持

    上文提到闭包会让作用域被保持,如下代码,当f1()函数执行结束, 这也证明了闭包会使作用域保持,

    function f1(param){
      var N = function (){
        return param
      }
      param++;
      return N;
    }
    var inner = f1(123);
    console.log(inner())//输出结果是 124
    
  • 所以 : 绑定函数 如果用到当时的值,请采用传参立刻调用的方式

    如下代码中,目的是让数组中填充成 1,2,3,4,下边是错误代码,结果是1,2,3,4

    // 错误代码 : 函数绑定的是当时作用域,而不是传入的值
    function bindArray (){
      var arr = []
      for(var i = 0 ; i < 4;i++){
        arr[i] = function (){
          return i + 1 ;
        }
      }
      return arr;
    }
    

    正确的写法,应该 不再直接创建一个返回 i 的函数 , 而是将 i 传递给了另一个即时函数、

    function bindArray(){
        var arr = []
        for(var i = 0; i< 4;i++){
          arr[i] = (function(x){
            return function(){
              return x
            }
          }(i));
        }
        return arr
      }	  
    

    或者是 ,思路2 :采用将i本地化的方式

      function bindArray(){
        var bind = function (x){
          return function (){
      			return x
          }
        }
        var arr;
        for(var i = 0 ;i < 4 ;i++){
          arr[i] = bind(i)
      }
        return arr;
    }
    

在循环体内, 采用最开始的代码,每个arr绑定的都是作用域,这样的后果是每个绑定的都是4。 而上边的代码中,利用闭包就可以解决上边问题

接下来 用闭包实现一个 getter 和setter , 实现一个迭代器作为测试

  • 利用闭包定义 getter和setter

    //即时函数
    var getValue,setValue;
    (function(){
     var secret = 0;
     getValue = function(){return secret}
     setValue = function(v){
       if(typeof v === 'number'){
         secret = v;
       }
     }
    }())
    setValue(123)
    console.log(getValue())
    
  • 闭包实现迭代器

    侧面证明闭包会保持作用域

    // 下边会输出数组 1 2 3 ...  实现了一个类似js的迭代器
    // 侧面证明了,闭包会使得函数作用域保持,
    function setUp(x){
      var i = 0;
      return function(){
        return x[i++]
      }
    }
    var next = setUp([1,2,3,4,5,6])
    console.log(next())
    console.log(next())
    console.log(next())
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值