5-闭包和作用域

作用域概念

1. 作用域:函数内的可用变量

- 函数的传参
- 函数内的局部变量
- 父函数作用域的变量
- 全局变量

2. 作用域链:

- 解析变量时,js引擎先查看嵌套子函数内的局部变量和参数,如果能找到就检索该值。
- 否则沿着作用域链向上查找,父函数作用域,一直到全局变量,直到变量被解析。
- 如果js引擎以达到全局作用域,仍无法解析,则该变量为未定义,undefined。
- 顺序:局部变量 => 父函数中变量 => 父函数的父函数的变量 => ··· => 全局变量

3. 变量阴影:

- 在函数内的变量与函数外的变量重复时,函数内的变量会暂时遮蔽外部作用域中的变量,称为变量阴影。
- 局部变量优先于更宽作用域内与其同名的变量

4. 自由变量:

嵌套函数可以捕获并未作为参数传入,也不在局部函数定义的变量,成为自由变量。

闭包

1. 简单理解闭包

简单理解闭包的概念,函数保留对其作用域(链)访问的过程被称为闭包。
或者可以这样理解:函数本身,及函数声明位置的代码(其上一级的作用域)。
闭包特性:函数至少会在作用域链上遮蔽另一个上下文:全局作用域。这也是为什么全局变量可以被任何函数访问到的原因。
但只有在嵌套函数(在一个函数中定义另一个函数)中,闭包的功能才会发挥出来。

需要特别指出的是:
函数会保持对其父作用域的引用。如果能可以访问该函数的引用,作用域就会保持不变
// 两个例子
// 例1
var outValue = "ninja";
function outFunction(){
    console.log(outValue); //ninja
}
// 函数可以访问到全局作用域的变量,是我们一直知道的。
// 但是原因是因为闭包,闭包允许函数访问并操作函数外部的变量,只要变量或函数存在于声明函数时的作用域内。
// 也可以理解为变量outValue存在于函数父作用域内。

// 例2
var outValue = "sam";
var later;
function outer(){
    var innerValue = 'ninja';
    function inner(){
        console.log(outValue);   // sam 全局作用域
        console.log(innerValue); // ninja 父作用域
    } 
    later = inner;
}
outer(); // inner函数赋给了later
later(); // 因为inner与innerValue在同一作用域下|
//原理流程是:
//当外部函数中声明内部函数式,不仅声明了内部函数,还创建了一个闭包:
//该闭包不仅包含了函数的声明,还包含了在函数声明时该外部函数的作用域(所有变量)。
//当最终执行内部函数时,尽管声明时的作用域已经消失了,但是通过闭包,仍然能够访问原始作用域。
//每个通过闭包访问变量的函数都有一个作用域链,作用域链包含闭包的全部信息。

2. 使用闭包

  • 封装私有变量
    function Ninja(){
        var count = 0; //私有变量,注意不是this
        this.getCount = function () {
             return count; //通过this将方法暴露出来
        }
        this.counts = function () {
            return  count++; //先运算再++ 因为在函数内部声明函数,形成闭包。可以访问同作用域下的变量/父级作用域变量
        }
    }

    var ninjia = new Ninja();
    console.log(ninjia)
  • 回调函数中使用闭包
    function animateIt(elementId){
        var elem = document.getElementById(elementId);
        var tick = 0;
        var timer = setInterval(function(){  //闭包
    
            if(tick<100){
                elem.style.marginLeft = elem.style.marginTop = tick + 'px';
                tick++;
            }else{
                clearInterval(timer);
                console.log(tick===100) //true
                console.log(elem) // <div id="box">div>
                console.log(timer) //1
            }
        },10)
    }
    animateIt('box')

3. 通过执行上下文来跟踪代码

js引擎时如何跟踪函数的执行并回到函数的位置呢?

js代码有两种类型:全局代码和函数代码。
js引擎执行代码时,每一条语句都处于特定的执行上下文中。
之前说过,js执行代码,从上往下执行全局代码,执行到函数调用时再返回到函数定义中,执行完成再回到函数中。

既然有两种类型的代码,就有两种执行上下文。全局执行上下文和函数执行上下文。
二者最主要的区别时:全局执行上下文只有一个,函数执行上下文是在每次调用时,都会创建一个新的函数执行上下文。js引擎使用执行上下文跟踪函数的执行。

注意:函数上下文是this。函数执行上下文是内部的js概念,js引擎使用执行上下文来跟踪函数的执行。
js单线程的执行模式:在某个特定时刻只能执行特定的代码。一旦发生调用,当前执行上下文必须停止,
并创建新的函数执行上下文来执行函数。当函数执行完成后,将函数执行上下文销毁,并重新回到生调用的执行上下文中。
所以需要跟踪执行上下文-正在执行的上下文以及正在等待的上下文。最简单的跟踪方法是执行上下文栈(或称为调用栈)-后进先出
比如
先执行全局代码,调用函数时进入函数执行上下文,嵌套函数上下文,退出嵌套函数上下文,回到函数上下文然后退出,最后回到全局上下文。

4. 使用此法环境跟踪变量的作用域

词法环境:js引擎内部用来跟踪标识符和特定变量之间的映射关系。
也是js作用域的内部实现机制,人们常称为作用域。
词法环境主要基于代码嵌套,内部代码结构可以访问外部代码结构中定义的变量。

词法环境如何跟踪这些变量的呢?
在特定的执行上下文中,我们的程序不仅直接访问词法环境中定义的局部变量,而且还会访问外部环境定义的变量。是因为-->
无论何时创建函数,都会创建词法环境,包含外部环境的引用(当前函数所在的环境引用)。存在[[Environment]]的内部属性上,这是函数被创建时的所在环境。
调用函数时,会创建新的执行上下文,也会创建新的词法环境,这个词法环境会关联创建时的外部环境。
var a = 1;
function bar(){  //创建时bar有全局词法环境的引用
   var a = 2;
   function foo() { //创建时foo有bar词法环境的引用
    console.log(a)
   };
   foo()  //foo调用时,会关联到foo创建时的词法环境,所以2
};
bar(); //2

var a = 1;
function foo() { //创建时 foo有全局引用
    console.log(a)
};
function bar(){   // bar有全局引用
    var a = 2;
    foo(); // foo调用时,关联到foo创建时的环境,即全局环境,所以1
};
bar(); //1

5. 理解js的变量类型

  • 变量可变性

    • const 不需要重新赋值的特殊变量/指向一个固定值
    • 声明const变量并初始化,以后不允许对const变量重新赋值const a=[]
    • 当const变量值为对象/数组时,可以为其添加修改属性值,但不能重写const变量。
    • 使用const定义全局变量,全局静态变量通常用大写。const GLOGAL_NINJA='yoshi'
  • 定义变量的关键字和词法环境

    • 使用关键字var
      • 该变量是在距离最近的函数内部或全局词法环境中定义的。var没有块级作用域的概念。也就是说,for循环中的var变量在for循环外,函数内,都能访问。
    • 使用let与const定义具有块级作用域的变量
      • let和const是最近的词法环境中定义变量。
      • 【可以在块级】、【循环内】、【函数或者全局环境】。
  • 在词法环境中注册标识符

    • 注册标识符的过程
    js引擎在执行代码时分了两个阶段,
    第一个阶段访问注册词法环境声明的变量和函数;
    第二个阶段,执行代码,具体执行略;
    
    第一阶段过程:
    - 判断是否是函数,创建形参和默认参数。否,略过;
    - 判断全局或函数,扫描当前代码(非块级作用域)进行函数声明,不扫描函数表达式和箭头函数和构造函数。
    找到函数声明后创建函数,并在当前环境内绑定标识符,若标识符已存在则重写;
    - 扫描当前代码的变量声明。找到函数外、块级外var let const声明的变量,注册标识符并初始化为undefined。若标识符已存在,将保留其值。
    
  • 变量提升和函数声明提升

    • 即在代码执行前现在词法环境里注册,
      但是变量不会赋值。函数也只对函数声明有效。
  • 似有变量的使用及警告

        function Nin(){
            var num=0;
            this.getNum=function(){
                console.log('getNum',num)
                return num;
            }
            this.setNum=function(){
                 num++;
                 console.log('setNum',num)
            }
        }
        var num1=new Nin()
        console.log(num1.getNum())
        console.log(num1.setNum())
        //通过闭包的方式实现了私有化,但是
        // const bb=num1.getNum()
        // bb能拿到似有变量的值,所以私有化只是模拟私有化
    

6. 研究闭包原理

    function Ninja(){
        var count = 0; //私有变量,注意不是this
        this.getCount = function () {
             return count; //通过this将方法暴露出来
        }
        this.counts = function () {
            return  count++; //先运算再++ 因为在函数内部声明函数,形成闭包。
            //可以访问同作用域下的变量/父级作用域变量
        }
    }

    var ninjia = new Ninja();
    console.log(ninjia)
    /*无论何时创建函数,都会保持词法环境的引用(通过内置[[Environment]]),
    在本例中,Ninja构造函数内部,创建的两个函数getFeints于feint均有Ninja环境的引用,
    因为Ninja环境是这两个函数创建时所处的环境。
    getFeints与feint函数是新创建的ninja的对象方法,
    因此可以在Ninjs构造函数外部访问这两个函数。
    这样实际上就创建了包含feints变量的闭包。「闭包的原理」
    */

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容