JavaScript学习-精通函数

闭包和作用域

Posted by MakeSail on November 3, 2019

主题

本文包括一下内容:

  • 使用闭包简化代码
  • 使用执行上下文跟踪JavaScript程序的执行
  • 使用词法环境跟踪变量的作用域
  • 理解变量的类型
  • 探讨闭包的工作原理

理解闭包

闭包允许函数访问并操作函数外部的变量。只要变量或者函数存在于声明函数时的作用域内,闭包既可使函数能够访问这些变量或函数。

一个例子

var outerValue = 'Samurai';
var later;


function outerFunction(){
        var innerValue = 'ninja';

        function innerFunction(){
            console.log(outerValue === "Samurai","I can see ther Samuari");
            console.log(innerValue === 'ninja',"I can see the ninja");
        }

        later = innerFunction;
}

outerFunction();

later();

当在外部函数中声明内部函数时,不仅定义了函数的声明,而且还创建了一个闭包。该闭包不仅包含了函数的声明,还包含了在函数声明时该作用域中的所有变量。

谨记每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息。因此,虽然闭包是非常有用的,但不能过度使用。使用闭包时候,所有的信息都会存储在内存中,知道JavaScript引擎确保这些信息不再使用

## 使用闭包

封装私有变量

通过使用闭包,可以通过方法对函数的状态进行维护,而不允许用户直接访问--这是因为闭包内部的变量可以通过闭包内的方法进行访问,构造器外部的代码则不能访问闭包内部的变量。

回调函数

处理回调函数是另一种常见的使用闭包的场景。回调函数值得是需要在将来不确定的某一个时刻异步调用的函数。通常,在这种回调函数中,我们需要经常频繁的访问外部数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #box1 {
            position:relative;
        }
    </style>
</head>
<body>
    <div>
        <div id="box2">2 Box</div>
        <div id="box1">First Box</div>
    </div>
    <script>
        function animateIt(elementId){
            let elem = document.getElementById(elementId);
            let tick = 0;
            let timer = setInterval(function(){
               if (tick < 100){
                   elem.style.left = elem.style.top = tick + "px";
                   console.log(tick === 100,"Tick accessed via  a closure.");
                   console.log(elem,"Element also accessed via a closure");
                   console.log(timer,"Timer reference also obtained via a closure");
                   tick++;
               } else {
                   clearInterval(timer);
                   console.log(tick === 100,"Tick accessed via  a closure.");
                   console.log(elem,"Element also accessed via a closure");
                   console.log(timer,"Timer reference also obtained via a closure");
               }
            },100);
        }
        animateIt("box1");
    </script>
</body>
</html>

上述代码中,我们使用了一个独立的匿名函数来完成目标元素的动画效果,该匿名函数作为计时器的一个参数传入计时器。通过闭包,该匿名函数通过三个变量控制动画过程:elem、tick和timer。这三个变量用于维持动画的过程。

理解JavaScript的变量类型

在JavaScript中,我们可以通过3个关键字定义变量:var、let和const。这三个关键字有两点不同:可变性,与作用域的关系

变量可变性

const定义的变量都不可变,也就是说通过const声明的变量的值只能设置一次。

通过var或则let声明的变量的值可以变更任意次数。

const变量

用于两种目的:

  • 不需要重新赋值的特殊变量
  • 指向一个固定的值

变量的关键字与作用域

使用var

当使用关键字var时,该变量是在距离最近的函数内部或是在全局词法环境中定义的。(注意:忽略块级作用域)。

var globalNinja = "Yoshi"; // 使用关键字var定义全局变量
// 变量globalNinja是在全局环境中定义的(距离最近的函数或者全局词法环境)
function reportActivity(){
    // reportActivity函数创建的函数环境,包含变量functionActivity、i和forMessage,这三个变量均通过关键字var定义的,与他们距离最近的是reportActivity函数
    var functionActivity = "junping"; // 使用关键字var定义函数内部的局部变量
    for(var i = 1;i<3;i++){
        // var 忽略块级作用域
        var forMessage = globalNinja + " " + functionActivity; // 使用关键字var在for循环中定义两个变量
        console.log("forMessage=>"+forMessage);
        console.log("i=",i);
    }
    console.log("i="+i,"","forMessage=",forMessage);
}

reportActivity();

这源于通过var声明的变量实际上总是在距离最近的函数内或是全局词法环境中注册的,不关注块级作用域。

可以分析得出,变量forMessage与i虽然是被包含在for循环中,但实际是在reportActivity环境中注册的(距离最近的函数环境)

使用let与const定义具有块级作用域的变量

var是在距离最近的函数或者全局词法环境中定义变量,与var不同的是,let和const更加直接。let和const直接在最近的词法环境中定义变量(可以是在块级作用域内、循环内、函数内或者全局环境内)。我们可以使用let和const定义跨级别、函数级别、全局级别的变量。

在词法环境中注册标识符

一旦创建了新的 环境,就会执行第一阶段。在第一阶段,没有执行代码,但是JS引擎会访问并注册在当前词法环境中所声明的变量和函数。JS在第一阶段完成后开始执行第二阶段,具体如何执行取决于变量的类型,以及环境类型。

具体的处理过程如下:

  1. 如果是创建一个函数环境,那么创建形参以及函数参数的默认值。如果是非函数环境,将跳过此步骤 。
  2. 如果是创建全局或者函数环境,就扫描当前代码进行函数声明,但是不会扫描函数表达式或者箭头函数。对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符的值将被重写。如果是块级作用域,将跳过此步骤。
  3. 扫描当前代码进行变量声明。在函数或全局环境中,找到当前函数以及其他函数之外通过var声明的变量,并找到所有在其他函数或者代码块中通过let或者const定义的变量。在块级环境中,仅仅查找当前块中通过let或者const定义的变量。对于所查找到的变量,若该标识符不存在,进行注册并将其初始化为underfined。