理解闭包和作用域

1. 认识闭包

  闭包就是函数访问并操作函数外部的变量,只要被访问的变量存在于函数被声明时的作用域内。即函数可以记住并访问所在的词法作用域。

   一个简单的闭包:

1 function fn() {
2   var a = 2
3   function fa() {
4     console.log(a) // 2
5   }  
6   fa()
7 }
8 fn()

  这段代码主要是基于词法作用域的查找规则,函数fa()可以访问本函数的外部作用域中的变量a,也是一种闭包的使用。

  下面一段代码更清晰地展示了闭包:

1 function fn() {
2   var a = 2
3   function fa() {
4     console.log(a)
5   }
6   return fa
7 }
8 var fb = f()
9 fb() // 2

  在这段代码中fa()可以被正常执行,但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

2.作用域

  作用域是根据名称查找变量的一套规则。当代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域链的用途就是保证对执行环境有权访问的所有变量和函数的有序访问。

 1 function fn() {
 2     var a = 2
 3     var b = a * 2
 4     function fa() {
 5         var c = b * 3
 6         console.log(a, b, c)
 7     }
 8     return fa
 9 }
10 var fb = fn()

  以上这段代码中共有三层嵌套的的作用域。我们可以将其分成几个块

  1、最外层就是整个全局作用域,其中只有两个标识符: fn, fb。

  2、第二层的2-9行包含着fn()所创建的作用域,其中有三个标识符: a、b和fa。

  3、第三层的5-7行包含着fa()所创建的作用域,其中只有一个标识符:c。

作用域的层级结构和包含关系给引擎提供了标识符的信息位置,引擎可以利用这些信息来查找标识符。

作用域的查找会在找到第一个匹配的标识符时停止。在有多层嵌套的作用域中,可以定义同名的标识符,这是因为“遮蔽效应”,内层的标识符遮蔽了外层的。抛开遮蔽效应,作用域的查找始终是从运行时所处的最内部作用域开始,逐层向外进行。

在函数fn()被定义时,发生过程如下:

 函数fn()执行时,发生过程如下:

函数fa()创建时。发生过程如下:

在以上代码中先定义了fn()函数和fb变量,这时只有一个全局的作用域,包含一个fn()方法和一个undefined的fb,在fn()函数运行时,会为函数创建一个执行环境,产生一个fn()的活动对象,然后复制了全局作用域中的变量对象保存在自己的作用域链中,在之后的fa()创建时,先创建一个fa()的活动对象,然后复制了fn()的作用域链保存在自己的作用域链中。

在fa()从fn()中返回后,就会被销毁,将其活动对象和作用域链会被绑定到fb(),这时fb()的作用域链中是包含着fn()的活动对象和全局的活动对象的,因此可以在fb()中访问fn()中定义的所有变量,而且fn()执行完毕后,其活动对象也不会被销毁,因为fb()的作用域链仍然在引用着它的活动对象,换句话说,当fn()执行完后,它的执行环境的作用域链会被销毁,但它的活动对象仍然保留在内存中,直到fb()被销毁时才会一起销毁。

3使用闭包

  1、封装私有变量

  许多编程语言使用私有变量,这些私有变量是不对外公开的私有属性。是一种非常有用的特性,因为当通过其他代码访问这些变量时,我们不希望对象的实现细节对用户造成过度负荷。

 1 function Test() {
 2     var a = 0
 3     this.getA = function () {
 4         return a
 5     }
 6     this.addA = function () {
 7         a++
 8     }
 9 }
10 var test = new Test()
11 test.addA()
12 console.log(test.a) // undefined
13 console.log(test.getA()) // 1

   在上段代码中,创建了一个Test构造器,通过使用new创建一个新的对象实例,函数内的this指向新的实例化对象。在构造器内部定义了一个变量a,用来保存状态,由于作用域规则的限制,因此只能在构造器内部访问该变量,因此添加了一个访问该变量的方法getA(),该方法只能读取私有变量,不能改写。接下来创建一个增量方法addA(),用于控制私有变量的值。在真实的代码中可能是一些业务逻辑的处理,这里只是用于增加a的值。

  使用场景: getter和setter的实现。模拟Java的setter和getter方法,对value进行赋值和查询。

 1 var foo = (function () {
 2     var value = 'oldValue'
 3     return {
 4         getter_value: function () {
 5             return value
 6         },
 7         setter_value: function (new_value) {
 8             value = new_value
 9         }
10     }
11 }())
12 
13 console.log(foo.getter_value()) // 'oldValue'
14 console.log(foo.value)  // undefined
15 foo.setter_value('newValue')
16 console.log(foo.getter_value()) // 'newValue'

通过以上的测试,我们可以通过闭包内部的方法获取私有变量,修改私有变量,但不能直接访问和修改。有效的阻止了对变量不可控的修改。因此通过闭包可以一定程度上的实现私有变量来处理问题。

  2、回调函数

 在定时器的回调函数中使用闭包:

 1 function animate(elementId) {
 2     var elem = document.getElementById(elementId)
 3     var tick = 0
 4     var play = setInterval(() => {
 5         if (tick < 100) {
 6             elem.style.left = tick + 'px'
 7             tick++
 8         } else {
 9             clearInterval(play)
10         }
11     }, 10)
12 }

  代码中使用了一个箭头函数来实现目标元素的动画效果,将该箭头函数作为参数传入计时器,然后通过闭包,该箭头函数通过三个变量(elem、tick、paly)来控制动画过程,这三个变量都在animate()方法的作用域内,若把它们移出到animate()作用域外的全局作用域内,动画仍然能正常工作。但是如果添加多个具有不同id的dom元素,再将新的id传入animate()方法,就会产生混乱,因为每一个动画都有自己的状态,如果把这三个变量放入全局作用域中就无法使用它们来同时跟踪多个动画的不同状态。

  如果没有闭包,在同一时间做许多事情,例如事件绑定,动画甚至服务端请求等,都会变得复杂困难。

  使用场景:有多个dom元素同时绑定动画事件。

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <meta http-equiv="X-UA-Compatible" content="IE=edge">
 6     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7     <title>闭包的使用场景2:</title>
 8 </head>
 9 <body>
10     <div id="box1" style="width: 100px; height: 100px; background-color: red;"></div>
11     <div id="box2" style="width: 100px; height: 100px; background-color: yellow;"></div>
12     <div id="box3" style="width: 100px; height: 100px; background-color: blue;"></div>
13     <script>
14         function animate(elementId) {
15             var elem = document.getElementById(elementId)
16             var tick = 0
17             var timer = setInterval(function() {
18                 if (tick < 500) {
19                     elem.style.marginLeft = tick + 'px'
20                     tick++
21                 } else {
22                     clearInterval(timer)
23                 }
24             }, 10)
25         }
26         animate("box1")
27         animate("box2")
28         animate("box3")
29     </script>
30 </body>
31 </html>

  3、防抖函数的使用

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <meta http-equiv="X-UA-Compatible" content="IE=edge">
 6     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7     <title>闭包的使用场景3</title>
 8 </head>
 9 <body>
10     <button id="btn">提交按钮</button>
11     <script>
12         // fn 防抖的函数
13         // delay 防抖的延期时间
14         function debounce(fn, delay) {
15             let timer = null
16             return function () {
17                 if (timer) {
18                     clearTimeout(timer)
19                     timer = setTimeout(fn, delay)
20                 } else {
21                     timer = setTimeout(fn, delay)
22                 }
23             }
24         }
25         function success() {
26             console.log('成功提交')
27         }
28         const submit = debounce(success, 5000)
29         let btn = document.getElementById("btn")
30         btn.addEventListener('click', submit)
31     </script>
32 </body>
33 </html>

  防抖延期时间为5秒,在5秒内多次点击提交按钮,只会触发一次成功的提交。使用防抖处理可以有效防止多次触发同一请求,防止页面卡顿。

4. 总结

  通过闭包不仅可以减少代码数量和复杂度来添加高级特性,还能实现一些不太可能完成的功能,可以通过闭包封装私有变量有效的阻止了一些对变量不可控的修改,甚至可以将一些代码封装为模块,通过创建模块实例来调用模块内的方法来完成需要的事件处理。而且闭包内的函数不仅可以在闭包创建的时候访问闭包内的变量,而且当闭包内函数执行时还可以更新这些变量的值。闭包不是一个时刻的状态快照,而是一个真实的状态封装,只要闭包在,就可以对变量进行修改。

  但是由于闭包的特性,因此会比其他的函数占用更多的内存,过度使用闭包可能会导致内存泄露。

posted @ 2021-05-08 11:35  leayun  阅读(566)  评论(0)    收藏  举报