关于javascript:JS理解了词法环境就理解了闭包

49次阅读

共计 5913 个字符,预计需要花费 15 分钟才能阅读完成。

学习地址:https://zh.javascript.info/closure(本文的截图都来自此地址,这些图画得都很漂亮。)


闭包是一种编程思维,理解它之前要先理解上面相干的知识点:

  • 变量作用域
  • 嵌套函数

变量作用域

❓ 什么是变量作用域呢?
✅ 变量作用域就是变量起作用的区域。

❓ 这个区域是怎么进去的呢?
✅ 用大括号 {} 就能够划分出一个区域,这个区域叫做代码块。

❓ 划进去的这个区域有什么用呢?
✅ 在代码块内用 letconst申明的变量(用 var 申明的变量在这里就不探讨了,最好不要用),能够在以后这个块内被拜访到,出了块就拜访不到了,也就是限定了变量起作用的区域。

示例代码:

{
  let a = '哈哈'
  
  console.log(a) // 哈哈
}

console.log(a) // Uncaught ReferenceError: a is not defined
if (true) {
  let a = '哈哈'
  
  console.log(a) // 哈哈
}

console.log(a) // Uncaught ReferenceError: a is not defined

嵌套函数

❓ 什么是嵌套函数呢?
✅ 如果一个函数是在另一个函数中创立的,那么这个函数就叫嵌套函数。

❓ 嵌套函数有什么特点呢?
✅ 嵌套函数能够拜访内部变量。

示例代码:

function func1() {
  let a = '张三'
  
  function func2() {return a}
  
  console.log(` 姓名是:${func2()}`) // 姓名是:张三
}

func1()

更有意思的是,能够返回一个嵌套函数:作为一个新对象的属性或作为后果返回。之后能够在其余中央应用。不管在哪里调用,它依然能够拜访雷同的内部变量。

function makeCounter() {
  let count = 0
  
  return function() {return count++}
}

let counter = makeCounter()

console.log(counter()) // 0
console.log(counter()) // 1
console.log(counter()) // 2

那么问题来了:

  • 为什么代码块能限度变量的作用范畴?
  • 限度了变量的作用范畴后,为什么嵌套函数还能拜访到内部变量?

这背地暗藏的大 boss 是谁呢?

这个大 boss 就是 词法环境,是它制订的游戏规则。这个得好好理解一下。

词法环境

简介

在 JavaScript 中,每个运行的函数,代码块 {…} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment)的外部(暗藏)的关联对象。

“词法环境”是一个标准对象(specification object):它只存在于语言标准的“实践”层面,用于形容事物是如何工作的。咱们无奈在代码中获取该对象并间接对其进行操作。

词法环境对象由两局部组成:

  1. 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包含一些其余信息,例如 this 的值)的对象。
  2. 内部词法环境 的援用,与内部代码相关联。

变量与词法环境的关系

一个“变量”只是 环境记录 这个非凡的外部对象的一个属性。“获取或批改变量”意味着“获取或批改词法环境的一个属性”。

比方,下图展现了一个全局词法环境,全局词法环境没有内部援用,所以箭头指向了 null

再看一个长一点的代码:

右侧的矩形演示了执行过程中全局词法环境的变动:

  1. 脚本开始运行,词法环境事后填充了所有申明的变量。

此时变量处于“未初始化(Uninitialized)”状态。这意味着引擎晓得变量,然而在用 let 申明前,不能引用它。简直就像变量不存在一样。

  1. let 修饰符定义了变量,但没有赋值,此时变量是undefined。从这一刻起,就能够应用变量了。
  2. 变量被赋予了一个值。
  3. 变量被批改。

函数与词法环境的关系

函数其实也是一个值,就像变量一样。与变量的不同之处在于函数申明的初始化会被立刻实现。

下图展现了增加一个函数时全局词法环境的初始状态。

当创立了一个词法环境(Lexical Environment)时,函数申明会立刻变为即用型函数(不像 let 那样直到申明处才可用)。这就是为什么咱们甚至能够在申明本身之前调用一个以函数申明(Function Declaration)的形式申明的函数。

外部和内部词法环境

通过下图,来解释一下外部和内部词法环境。

把本人设想成 js 引擎,当初由引擎你来运行下面的代码,请开始你的表演。

  1. 代码开始运行时,创立了一个全局词法环境,它具备 phrase 变量和 say() 函数。
  2. 而后用 let 修饰符给 phrase 变量赋值为 Hello,就相当于批改了全局词法环境的 phrase 属性。
  3. 当执行到 say("John") 这行时,就跟下面的图对应上了(以后执行地位在箭头标记的那一行上)。

say("John") 调用期间,此函数的词法环境由两局部组成:外部一个(用于函数调用)和内部一个(全局)。

  1. 在外部环境中有一个名叫 name 的属性,它是函数的参数。咱们调用了 say("John") ,因而name 的值为 John。
  2. 内部词法环境是全局词法环境。它具备 phrase 变量和函数自身。

当代码要拜访一个变量时:首先会搜寻外部词法环境,而后搜寻外部环境,而后搜寻更内部的环境,以此类推,直到全局词法环境。(这就是变量作用域的游戏规则)

如果在任何中央都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创立一个全局变量)。

变量的搜寻过程如下图所示。

  • 对于 name 变量,当 say 中的 alert 试图拜访 name 时,会立刻在外部环境中找到。
  • 对于 phrase 变量,因为外部环境中没有 phrase 变量,它会顺着对外部词法环境的援用找到它。

返回函数

这一部分可是重点中的重点了,看懂了这块,就能明确闭包的原理了。

创立嵌套函数

有这样一段代码:

function makeCounter() {
  let count = 0;

  return function() {return count++;};
}

let counter = makeCounter();

在每次 makeCounter()调用的开始,都会创立一个新的词法环境,以存储该 makeCounter 运行时的变量,如下图所示:

和之前的代码不同,在执行 makeCounter() 的过程中,创立了一个嵌套函数。咱们并未运行它,只是创立了它。

[[Environment]]属性

所有的函数在“诞生”时,都会记住创立它们的词法环境。所有函数都有名为 [[Environment]] 的暗藏属性,该属性保留了对创立该函数的词法环境的援用。

所以,counter.[[Environment]]有对 {count: 0} 词法环境的援用。这就是函数记住它创立于何处的形式,与函数被在哪儿调用无关 [[Environment]] 援用在函数创立时被设置并永恒保留。

之后,当调用 counter() 时,会为该调用创立一个新的词法环境,并且 其内部词法环境援用获取于counter.[[Environment]],如下图所示:

也就是说:JavaScript 中的函数会主动通过暗藏的 [[Environment]] 属性记住创立它们的地位,所以它们都能够拜访内部变量。

在变量所在的词法环境中更新变量

当初,当 counter()中的代码查找 count 变量时,它首先搜寻本人的词法环境(为空,因为那里没有局部变量),而后是内部 makeCounter()的词法环境,并且在哪里找到就在哪里批改。也就是说,在变量所在的词法环境中更新变量。如下图所示:

如果咱们调用 counter() 屡次,count 变量将在同一地位减少到 23 等。

这就是嵌套函数可能拜访到内部变量的游戏规则。

闭包

终于该聊一聊正题了。

简介

闭包是指一个函数能够记住其内部变量,并能够拜访这些变量。闭包是一种编程思维,依照这种思维来组织的代码,叫做闭包构造。

为什么要应用闭包呢?闭包通过将变量保留(也能够说是“暗藏”,让外界不晓得有这个变量)在内部函数中这种形式,能够防止该变量净化全局变量,也能够防止其余的代码批改该变量,影响程序的运行。

示例计数器:从一般构造到闭包构造

举个例子,来阐明一下如何应用闭包。比方,当初有这样一个需要:页面上有个用于计数的按钮,点击一次,计数器就加 1。

一般代码

先来个初始代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title> 闭包演示 --- 计数器 </title>
  </head>
  <body>
    <button onclick="clickBtn()"> 点击 </button>
    
    <script>
      function clickBtn() {
        let count = 0;
        
        console.log(++count);
      }
    </script>
  </body>
</html>

执行成果,如图所示:

能够看到,屡次点击按钮后,计数器显示的值始终是 1,并没有累加。

为什么会呈现这种景象呢?依据前边的常识,每点击一次按钮,clickBtn()函数就执行一次,每次执行都会生成一个新的词法环境,跟本次函数对应,所以点击了几次按钮,就创立了几个词法环境,词法环境的 count 属性开始时都是 0,执行 ++ 操作后,变成 1。

将变量 count 放到全局词法环境中

当初革新一下代码,把变量 count 放到全局词法环境中,让 clickBtn() 函数的词法环境去全局词法环境中拿 count 的值。代码如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title> 闭包演示 --- 计数器 </title>
  </head>
  <body>
    <button onclick="clickBtn()"> 点击 </button>
    
    <script>
      let count = 0;
      
      function clickBtn() {console.log(++count);
      }
    </script>
  </body>
</html>

执行成果,如图所示:

能够看到,屡次点击按钮后,计数器能失常工作。

全局词法环境中的 count 变量被其余代码批改

下面的代码貌似失常了,但其实有一个隐患。代码如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title> 闭包演示 --- 计数器 </title>
  </head>
  <body>
    <button onclick="clickBtn()"> 点击 </button>
    
    <script>
      let count = 0;
      
      function clickBtn() {console.log(++count);
      }
      
      // ...  一堆其它的代码
      
      setTimeout(function() {count = 100;}, 2000);
    </script>
  </body>
</html>

执行成果,如图所示:

能够看到,当屡次点击按钮时,开始能够失常计数,但在某一时刻,count的值变成了 100,之后再点击按钮,计数器就会在 100 的根底上,进行累加操作。

为什么会有这种景象呢?因为变量 count 保留在了全局词法环境中,那么 clickBtn() 函数就能够从全局词法环境中拿到这个值,并批改(进行 ++ 操作),那你 clickBtn() 函数能从全局词法环境中拿 count 的值,我其余函数(比方 setTimeout 里的回调函数)也能从全局词法环境中去拿,你能改值,我也能改值啊,别忘了之前说过的话:在变量的词法环境中批改变量 。在哪儿找到就在哪儿改,当回调函数批改了值(count = 100;)后,clickBtn() 函数再去全局词法环境中拿 count 的值,就变成 100 了。

所以,如果把 count 变量放到全局词法环境中,它就有被其余函数批改的危险。

用闭包构造组织代码

既然把 count 变量放到全局词法环境中并不是一个好的抉择,那还是把它放到一个函数的词法环境中吧。

咱们推导一下:因为每次点击按钮,都要执行一个函数 clickBtn(),在这个函数里对count 变量进行累加操作,而这个变量又不能放在全局,所以可不可以:

  • 把这个 count 变量放到一个函数里,做为它的局部变量;(限度变量作用域范畴)
  • clickBtn()函数也嵌套在这个函数中,同时也援用了 count 变量;(变量来自函数的内部词法环境)
  • 这个函数执行时,返回了嵌套的 clickBtn() 函数;(提供给外界应用)

这样点击按钮时,执行 clickBtn() 函数,clickBtn()函数又顺着内部词法环境可能找到 count 变量,而保留 count 变量的这个词法环境,只有 clickBtn() 函数能拜访到,其余的函数拜访不到,连拜访都拜访不到,更别提批改了。

代码如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title> 闭包演示 --- 计数器 </title>
  </head>
  <body>
    <button onclick="clickBtn()"> 点击 </button>
    
    <script>
      function addCount() {
        let count = 0;
        
        return function() {console.log(++count);
        }
      }
      
      let clickBtn = addCount();
    </script>
  </body>
</html>

执行成果,如图所示:

利用示例

字段排序

const users = [{
  name: 'Tom',
  age: 5,
  type: 'cat'
}, {
  name: 'Jerry',
  age: 3,
  type: 'mouse'
}, {
  name: 'Speike',
  age: 10,
  type: 'dog'
}];

function byField(field) {return function(a, b) {if (a[field] > b[field]) {return 1;} else if (a[field] < b[field]) {return -1;} else {return 0;}
  }
}

console.log(users.sort(byField('name')));

找到数组中符合条件的数字

const numbers = [1, 3, 6, 7, 2, 11, 5, 12, 9, 4, 6];

function inBetween(num1, num2) {return function(item) {return item >= num1 && item <= num2;}
}

console.log(numbers.filter(inBetween(3, 9)));

当你晓得了事物背地的原理,你再对待这个事物就跟之前不一样了。以前是含糊凌乱,当初是清晰透彻,以前是局外人,当初是引擎,是游戏规则制定者,代码怎么跑,要按我定的规定来。

正文完
 0