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

学习地址: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)));

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

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据