Skip to content

Latest commit

 

History

History
198 lines (163 loc) · 10.4 KB

05.精通函数:闭包和作用域.md

File metadata and controls

198 lines (163 loc) · 10.4 KB

05.精通函数:闭包和作用域

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

理解闭包

var outerValue = 'samurai';
var later; //  声明一个空变量,稍后在后面的代码中使用
function outerFunction() {
  var innerValue = 'ninja'; // 在函数内部声明一个值,该值在作用域局限于函数内部,在函数外部不允许访问
  function innerFunction() {
    // 在 outerFunction 函数中声明一个内部函数,声明该内部函数时,innerValue 是在内部函数的作用域内的
    console.log(outerValue === 'samurai', 'I can see the samurai.');
    console.log(innerValue === 'ninja', 'I can see the ninja.');
  }
  later = innerFunction; //  将内部函数 innerFunction 的引用存储在变量 later 上,因为 later 在全局作用域内,所以我们可以对它进行调用
}
outerFunction(); // 调用 outerFunction 函数,创建内部函数 innerFunction,并 将内部函数赋值给变量later
later(); // 通过 later 调用内部函数。我们不能直接调用内部函数,因为它的作用域(和 innerValue 一起) 被限制在外部函数 outerFunction 之内

每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息

使用闭包

封装私有变量

使用闭包模拟私有变量

function Ninja() {
  var feints = 0;
  this.getFeints = function() {
    return feints;
  };
  this.feint = function() {
    feints++;
  };
}
var ninja1 = new Ninja();
ninja1.feint();
console.log(ninja1.feints === undefined); // true
var ninja2 = new Ninja();
console.log(ninja2.getFeints() === 0); // true

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

function skulk(ninja) {
  report(ninja + 'skulking');
}
function report(message) {
  console.log(message);
}
skulk('Kuma');
  1. 每个 JavaScript 程序只创建一个全局执行上下文,并从全局执行上下文开始执行 (在单页应用中每个页面只有一个全局执行上下文)。当执行全局代码时,全局执行上下文处于活跃状态。
  2. 首先在全局代码中定义两个函数: skulkreport,然后调用 skulk("Kuma")。由于在同一个特定时刻只能执行特定代码,所以 JavaScript 引擎停止执行全局代码,开始执行带有 Kuma 参数的 skulk 函数。创建新的函数执行上下文,并置入执行上下文栈的顶部。
  3. skulk 函数进而调用 report 函数。又一次因为在同一个特定时刻只能执行特定代码,所以,暂停 skulk 执行上下文,创建新的 Kuma 作为参数的 report 函数的执行上下文,并置入执行上下文栈的顶部。
  4. report 通过内置函数 console.log 打印出消息后,report 函数执行完成,代码又回到了 skulk 函数。report 执行上下文从执行上下文栈顶部弹出,skulk 函数执行上下文重新激活,skulk 函数继续执行。
  5. skulk 函数执行完成后也发生类似的事情: skulk 函数执行上下文从栈顶端弹出,重新激活一直在等待的全局执行上下文并恢复执行。JavaScript 的全局代码恢复执行。

使用词法环境跟踪变量的作用域

词法环境(lexical environment)是 JavaScript 引擎内部用来跟踪标识符与特定变量之间的映射关系。

var ninja = 'Hattori';
console.log(ninja);

console.log 语句访问 ninja 变量时,会进行词法环境的查询。

词法环境是 JavaScript 作用域的内部实现机制,人们通常称为作用域(scopes)。在 JavaScript 的 ES6 初版中,词法环境只能与函数关联。变量只存在于函数作用域中。这也带来了一些混淆。因为 JavaScript 是一门类 C 的语言,从其他类 C 语言(如 C++、 C#、Java 等)转向 JavaScript 的开发者通常会预期一些初级概念,例如块级作用域。在 ES6 中最终修复了块级作用域问题。

无论何时创建函数,都会创建一个与之相关联的词法环境,并存储在名为 [[Environment]] 的内部属性上(也就是说无法直接访问或操作)。两个中括号用于标志内部属性,这些环境是在函数创建时决定的

理解 JavaScript 的变量类型

在 JavaScript 中,我们可以通过 3 个关键字定义变量:

按变量可变性:varlet为一类,const 为一类。 与词法环境的关系:可以将 var 分为一组,letconst 分为一组。

  • var
    • 使用关键字 var 时,该变量是在距离最近的函数内部或是在全局词法环境中定义的。(注意:忽略块级作用域)
  • let
    • letconst 更加 直接。letconst 直接在最近的词法环境中定义变量(可以是在块级作用域内、循环内、函数内或全局环境内)。我们可以使用 letconst 定义块级别、函数级别、全局级别的变量。
  • const
    • 不需要重新赋值的特殊变量。
    • 指向一个固定的值,例如球队人数的最大值,可通过 const 变量 MAX_RONIN_COUNT 来表示,而不是仅仅通过数字 234 来表示。这使得代码更加易于理解和维护。虽然在代码里没有直接使用数字 234,但是通过语义化 的变量名 MAX_RONIN_COUNT 来表示,MAX_RONIN_COUNT 的值只能指定一次。

在词法环境中注册标识符

JavaScript 代码的执行事实上是分两个阶段进行的。

  • 一旦创建了新的词法环境,就会执行第一阶段。在第一阶段,没有执行代码,但是 JavaScript 引擎会访问并注册在当前词法环境中所声明的变量和函数。
  • JavaScript 在第一阶段完成之后开始执行第二阶段,具体如何执行取决于变量的类型(let、var、const 和函数声明)以及环境类型(全局环境、函数环境或块级作用域)。
    • 如果是创建一个函数环境,那么创建形参及函数参数的默认值。如果是非函数环境,将跳过此步骤。
    • 如果是创建全局或函数环境,就扫描当前代码进行函数声明(不会扫描其他函数的函数体),但是不会执行函数表达式或箭头函数。对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符的值将被重写。如果是块级作用域,将跳过此步骤。
    • 扫描当前代码进行变量声明。在函数或全局环境中,查找所有当前函数以及其他函数之外通过 var 声明的变量,并查找所有通过 letconst 定义的变量。在块级环境中,仅查找当前块中通过 letconst 定义的变量。对于所查找到的变量,若该标识符不存在,进行注册并将其初始化为 undefined。若该标识符已经存在,将保留其值。

在函数声明之前调用函数

console.log(typeof fun === 'function'); // true
console.log(typeof myFunExp === 'undefined'); // true
console.log(typeof myArrow === 'undefined'); // true
function fun() {}
var myFunExpr = function() {};
var myArrow = x => x;

函数重载

console.log(typeof fun === 'function'); // true
var fun = 3;
console.log(typeof fun === 'number'); // true
function fun() {}
console.log(typeof fun === 'number'); // true

研究闭包的工作原理

<div id="box1">First Box</div>
<div id="box2">Second Box</div>
<script>
  function animateIt(elementId) {
    var elem = document.getElementById(elementId);
    var tick = 0;
    var timer = setInterval(function(){
      if (tick < 100) {
        elem.style.left = elem.style.top = tick + "px";
        tick++;
      } else {
       clearInterval(timer);
       console.log(tick === 100);
       console.log(elem);
       console.log(timer);
      }
    }, 10);
}
animateIt("box1");
animateIt("box2");
</script>

每次调用 animateIt 函数时,均会创建新的词法环境,该词法环境保存了动画所需的重要变量(elementIdelem动画元素tick计数次数timer动画计数器的 ID)。只要至少有一个通过闭包访问这些变量的函数存在,这个环境就会一直保持。

<div id="box1">First Box</div>
<div id="box2">Second Box</div>
<script>
  function animateIt(elementId) {
    var elem = document.getElementById(elementId);
    var tick = 0;
    var timer = setInterval(function(){
      if (tick < 100) {
        var position = tick + 'px';
        elem.style.left = position;
        elem.style.top = position;
        tick++;
      } else {
       clearInterval(timer);
       console.log(tick === 100);
       console.log(elem);
       console.log(timer);
      }
    }, 10);
}
animateIt("box1");
animateIt("box2");
</script>

在本例中,浏览器会一直保持 setInterval 的回调函数,直到调用 clearInterval 方法。随后,当一个计 时器到期,浏览器会调用对应的回调函数,通过回调函数的闭包访问创建闭包时的变量。这样避免了手动匹配回调函数的麻烦,并激活变量,极大地简化代码。

小结

  • 通过闭包可以访问创建闭包时所处环境中的全部变量。闭包为函数创建时所处 的作用域中的函数和变量,创建“安全气泡”。通过这种的方式,即使创建函数 时所处的作用域已经消失,但是函数仍然能够获得执行时所需的全部内容。
  • 我们可以使用闭包的这些高级功能:
    • 通过构造函数内的变量以及构造方法来模拟对象的私有属性。
    • 处理回调函数,简化代码。
  • JavaScript 引擎通过执行上下文栈(调用栈)跟踪函数的执行。每次调用函数时,都会创建新的函数执行上下文,并推入调用栈顶端。当函数执行完成后,对应的执行上下文将从调用栈中推出。
  • JavaScript 引擎通过词法环境跟踪标识符(俗称作用域)。
  • 在 JavaScript 中,我们可以定义全局级别、函数级别甚至块级别的变量。可以使用关键字 var、let 与 const 定义变量:
    • 关键字 var 定义距离最近的函数级变量或全局变量。
    • 关键字 let 与 const 定义距离最近级别的变量,包括块级变量。块级变量在 ES6 之前版本的 JavaScript 中是无法实现的。此外,通过关键字 const 允许定义只能赋值一次的变量。
  • 闭包是JavaScript作用域规则的副作用。当函数创建时所在的作用域消失后,仍然能够调用函数。