飞道的博客

JavaScript进阶教程(5)-一文让你搞懂作用域链和闭包

463人阅读  评论(0)

目录

1 作用域

2 作用域链

3 预解析

3.1 变量预解析

3.2 函数预解析

4 闭包

4.1 闭包小案例:

4.2 闭包点赞案例

5 闭包的作用

6 闭包导致的一些问题

6.1 第一:使用更多的闭包

6.2 第二种方法:使用了匿名闭包

6.3 第三种方法:使用用ES2015引入的let关键词

6.4 第四种方法:使用 forEach()来遍历

7 性能

8 总结


1 作用域

在JS中变量可以分为局部变量和全局变量,对于变量不熟悉的可以看一下我这篇文章:https://blog.csdn.net/qq_23853743/article/details/106946100
作用域就是变量的使用范围,分为局部作用域和全局作用域,局部变量的使用范围为局部作用域,全局变量的使用范围是全局作用域。在 ECMAScript 2015 引入let 关键字之前,js中没有块级作用域---即在JS中一对花括号({})中定义的变量,依然可以在花括号外面使用。


  
  1. {
  2. var num2 = 100;
  3. }
  4. console.log(num2); // >100

2 作用域链

当内部函数访问外部函数的变量时,采用的是链式查找的方式进行获取的,从里向外层层的搜索,搜索到了就直接使用,搜索到0级作用域的时候,如果还是没有找到这个变量,就报错。这种结构我们称为作用域链。


  
  1. // 作用域链:变量的使用,从里向外,层层的搜索,搜索到了就直接使用
  2. // 搜索到0级作用域的时候,如果还是没有找到这个变量,就会报错
  3. var num = 10; //作用域链 级别:0
  4. function f1() {
  5. var num2 = 20;
  6. function f2() {
  7. var num3 = 30;
  8. console.log(num); // >10
  9. }
  10. f2();
  11. }
  12. f1();

3 预解析

JS代码在浏览器中是由JS引擎进行解析执行的,分为两步,预解析和代码执行。预解析分为 变量预解析(变量提升) 和 函数预解析(函数提升),浏览器JS代码运行之前,会把变量的声明和函数的声明提前(提升)到该作用域的最上面。

3.1 变量预解析

把所有变量的声明提升到当前作用域的最前面,不提升赋值操作。
示例:


  
  1. console.log(num); // 没有报错,返回的是一个undefined
  2. var num = 666;

预解析后:


  
  1. // 预解析后:变量提升
  2. var num;
  3. console.log(num); // 所以返回的是一个undefined
  4. num = 666;

3.2 函数预解析

将所有函数声明提升到当前作用域的最前面。
示例:


  
  1. f1(); // 能够正常调用
  2. function f1() {
  3. console.log( "Albert唱歌太好听了");
  4. }

预解析后:


  
  1. function f1() {
  2. console.log( "Albert唱歌太好听了");
  3. }
  4. f1(); //预解析后,代码是逐行执行的,执行到 f1()后,去调用函数 f1()

4 闭包

在专业书籍上对于闭包的解释为:Javascript的闭包是指一个函数与周围状态(词法环境)的引用捆绑在一起(封闭)的组合,在JavaScript中,每次创建函数时,都会同时创建闭包。闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰,即形成一个不销毁的栈环境。
这句话比较难以理解,对于闭包我的理解是,在函数A中,有一个函数B,在函数B中可以访问函数A中定义的变量或者是数据x,被访问的变量x可以和B函数一同存在。即使A函数已经运行结束,导致创建变量x的环境销毁,B函数中x变量也依然会存在,直到访问变量x的B函数被销毁,此时形成了闭包。如下面代码所示:


  
  1. function A() {
  2. var x = 0;
  3. function B() {
  4. return ++x;
  5. }
  6. return B // 返回B函数
  7. }
  8. var B = A(); // 创建B函数
  9. console.log(B()); // 1
  10. console.log(B()); // 2
  11. console.log(B()); // 3
  12. console.log(B()); // 4
  13. console.log( "%c%s", "color:red", "*******---------*********");
  14. // 创建新的B函数
  15. B = A();
  16. console.log(B()); // 1

4.1 闭包小案例:


  
  1. // 普通的函数
  2. function f1() {
  3. var num = 0;
  4. num++;
  5. return num;
  6. }
  7. console.log(f1());
  8. console.log(f1());
  9. console.log(f1());
  10. console.log( "%c%s", "color:red", "*******---------*********");
  11. // 闭包
  12. function f2() {
  13. var num = 0;
  14. return function() {
  15. num++;
  16. return num;
  17. }
  18. }
  19. var ff = f2();
  20. console.log(ff()); // 1
  21. console.log(ff()); // 2
  22. console.log(ff()); // 3

4.2 闭包点赞案例

演示地址:https://www.albertyy.com/2020/9/like.html



代码:


  
  1. <!DOCTYPE html> <html>
  2. <head>
  3. <meta charset="utf-8">
  4. <title>闭包点赞案例:公众号AlbertYang </title>
  5. <style>
  6. ul {
  7. list-style-type: none;
  8. }
  9. li {
  10. float: left;
  11. margin-left: 10px;
  12. margin-bottom: 20px;
  13. }
  14. img {
  15. height: 300px;
  16. }
  17. input {
  18. margin-left: 30%;
  19. }
  20. </style>
  21. </head>
  22. <body>
  23. <ul>
  24. <li> <img src="1.jpg" alt=""> <br /> <input type="button" value="(1)赞"> </li>
  25. <li> <img src="2.jpg" alt=""> <br /> <input type="button" value="(1)赞"> </li>
  26. <li> <img src="3.jpg" alt=""> <br /> <input type="button" value="(1)赞"> </li>
  27. <li> <img src="4.jpg" alt=""> <br /> <input type="button" value="(1)赞"> </li>
  28. <li> <img src="5.jpg" alt=""> <br /> <input type="button" value="(1)赞"> </li>
  29. <li> <img src="6.jpg" alt=""> <br /> <input type="button" value="(1)赞"> </li>
  30. </ul>
  31. </body>
  32. <script>
  33. // 根据标签名字获取元素
  34. function my$(tagName) {
  35. return document.getElementsByTagName(tagName);
  36. }
  37. // 使用闭包
  38. function getValue() {
  39. var value = 2;
  40. return function() {
  41. // 每一次点击的时候,都应该改变当前点击按钮的value值
  42. this.value = "(" + (value++) + ")赞";
  43. }
  44. }
  45. //获取所有的按钮
  46. var btnObjs = my$( "input");
  47. //循环遍历每个按钮,注册点击事件
  48. for ( var i = 0; i < btnObjs.length; i++) {
  49. //注册事件
  50. btnObjs[i].onclick = getValue();
  51. }
  52. </script> </html>

5 闭包的作用

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。在一些编程语言中,比如 Java,是支持将方法声明为私有的(private),即它们只能被同一个类中的其它方法所调用。而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。下面我们计数器为例,代码如下:


  
  1. var myCounter = function() {
  2. var privateCounter = 0;
  3. function changeBy(val) {
  4. privateCounter += val;
  5. }
  6. return {
  7. increment: function() {
  8. changeBy( 1);
  9. },
  10. decrement: function() {
  11. changeBy( -1);
  12. },
  13. value: function() {
  14. return privateCounter;
  15. }
  16. }
  17. };
  18. var Counter1 = myCounter();
  19. var Counter2 = myCounter();
  20. console.log(Counter1.value()); /* 计数器1现在为 0 */
  21. Counter1.increment();
  22. Counter1.increment();
  23. console.log(Counter1.value()); /* 计数器1现在为 2 */
  24. Counter1.decrement();
  25. console.log(Counter1.value()); /* 计数器1现在为 1 */
  26. console.log(Counter2.value()); /* 计数器2现在为 0 */
  27. Counter2.increment();
  28. console.log(Counter2.value()); /* 计数器2现在为 1 */

在上边的代码中我们创建了一个匿名函数含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问,Counter.increment,Counter.decrement 和Counter.value,这三个公共函数共享同一个环境的闭包,多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。我们把匿名函数储存在一个变量myCounter 中,并用它来创建多个计数器,每次创建都会同时创建闭包,因为每个闭包都有它自己的词法环境,每个闭包都是引用自己词法作用域内的变量 privateCounter ,所以两个计数器 Counter1 和 Counter2 是各自独立的。以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

6 闭包导致的一些问题

在 ECMAScript 2015 引入let 关键字之前,在循环中有一个常见的闭包创建问题。请看以下代码:


  
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>公众号AlbertYang </title>
  6. </head>
  7. <body>
  8. <p id="help">提示信息 </p>
  9. <p>E-mail: <input type="text" id="email" name="email"> </p>
  10. <p>Name: <input type="text" id="name" name="name"> </p>
  11. <p>Age: <input type="text" id="age" name="age"> </p>
  12. </body>
  13. <script>
  14. function showHelp(help) {
  15. document.getElementById( 'help').innerHTML = help;
  16. }
  17. function setupHelp() {
  18. var helpText = [{
  19. 'id': 'email',
  20. 'help': '你的邮件地址'
  21. },
  22. {
  23. 'id': 'name',
  24. 'help': '你的名字'
  25. },
  26. {
  27. 'id': 'age',
  28. 'help': '你的年龄'
  29. }
  30. ];
  31. for ( var i = 0; i < helpText.length; i++) {
  32. var item = helpText[i];
  33. document.getElementById(item.id).onfocus = function() {
  34. showHelp(item.help);
  35. }
  36. }
  37. }
  38. setupHelp();
  39. </script>
  40. </html>

上边代码中,我们在数组 helpText 中定义了三个提示信息,每一个都关联于对应的文档中的input 的 ID。通过循环依次为相应input添加了一个 onfocus  事件处理函数,以便显示帮助信息。运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。
演示地址:https://www.albertyy.com/2020/7/closure1.html
我们想要的正确效果:https://www.albertyy.com/2020/7/closure2.html

这是因为赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。这里因为变量item使用var进行声明,由于变量提升(item可以在函数setupHelp的任何地方使用),所以item具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在onfocus 事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。要解决这个问题,有以下几个方法。

6.1 第一:使用更多的闭包


  
  1. function showHelp(help) {
  2. document.getElementById( 'help').innerHTML = help;
  3. }
  4. function makeHelpCallback(help) {
  5. return function() {
  6. showHelp(help);
  7. };
  8. }
  9. function setupHelp() {
  10. var helpText = [{
  11. 'id': 'email',
  12. 'help': '你的邮件地址'
  13. },
  14. {
  15. 'id': 'name',
  16. 'help': '你的名字'
  17. },
  18. {
  19. 'id': 'age',
  20. 'help': '你的年龄'
  21. }
  22. ];
  23. for ( var i = 0; i &lt; helpText.length; i++) {
  24. var item = helpText[i];
  25. document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  26. }
  27. }
  28. setupHelp();

这段代码可以正常的执行了。这是因为所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,help 指向 helpText 数组中对应的字符串。

6.2 第二种方法:使用了匿名闭包


  
  1. function showHelp(help) {
  2. document.getElementById( 'help').innerHTML = help;
  3. }
  4. function setupHelp() {
  5. var helpText = [{
  6. 'id': 'email',
  7. 'help': '你的邮件地址'
  8. },
  9. {
  10. 'id': 'name',
  11. 'help': '你的名字'
  12. },
  13. {
  14. 'id': 'age',
  15. 'help': '你的年龄'
  16. }
  17. ];
  18. for ( var i = 0; i &lt; helpText.length; i++) {
  19. ( function() {
  20. var item = helpText[i];
  21. document.getElementById(item.id).onfocus = function() {
  22. showHelp(item.help);
  23. }
  24. })(); // 马上把当前循环项的item与事件回调相关联起来
  25. }
  26. }
  27. setupHelp();

6.3 第三种方法:使用用ES2015引入的let关键词


  
  1. function showHelp(help) {
  2. document.getElementById( 'help').innerHTML = help;
  3. }
  4. function setupHelp() {
  5. var helpText = [{
  6. 'id': 'email',
  7. 'help': '你的邮件地址'
  8. },
  9. {
  10. 'id': 'name',
  11. 'help': '你的名字'
  12. },
  13. {
  14. 'id': 'age',
  15. 'help': '你的年龄'
  16. }
  17. ];
  18. for ( var i = 0; i &lt; helpText.length; i++) {
  19. let item = helpText[i]; //使用let代替var
  20. document.getElementById(item.id).onfocus = function() {
  21. showHelp(item.help);
  22. }
  23. }
  24. }
  25. setupHelp();

这个里使用let而不是var,因为let是具有块作用域的变量,即它所声明的变量只在所在的代码块({})内有效,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。

6.4 第四种方法:使用 forEach()来遍历


  
  1. function showHelp(help) {
  2. document.getElementById( 'help').innerHTML = help;
  3. }
  4. function setupHelp() {
  5. var helpText = [{
  6. 'id': 'email',
  7. 'help': '你的邮件地址'
  8. },
  9. {
  10. 'id': 'name',
  11. 'help': '你的名字'
  12. },
  13. {
  14. 'id': 'age',
  15. 'help': '你的年龄'
  16. }
  17. ];
  18. helpText.forEach( function(text) {
  19. document.getElementById(text.id).onfocus = function() {
  20. showHelp(text.help);
  21. }
  22. });
  23. }
  24. setupHelp();

7 性能

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。如果不是某些特定任务需要使用闭包,最好不要使用闭包。例如,在创建新的对象或者类时,方法通常应该放到原型对象中,而不是定义到对象的构造函数中。原因是这将导致每次构造函数被调用时,方法都会被重新赋值一次(也就是说,对于每一个实例对象,geName和 getMessage都是一模一样的内容, 每生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。)。请看以下代码:


  
  1. function MyObject(name, message) {
  2. this.name = name.toString();
  3. this.message = message.toString();
  4. this.getName = function() {
  5. return this.name;
  6. };
  7. this.getMessage = function() {
  8. return this.message;
  9. };
  10. }

在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改如下:


  
  1. function MyObject(name, message) {
  2. this.name = name.toString();
  3. this.message = message.toString();
  4. }
  5. MyObject.prototype = {
  6. getName: function() {
  7. return this.name;
  8. },
  9. getMessage: function() {
  10. return this.message;
  11. }
  12. };

如果我们不想重新定义原型,可修改如下:


  
  1. function MyObject(name, message) {
  2. this.name = name.toString();
  3. this.message = message.toString();
  4. }
  5. MyObject.prototype.getName = function() {
  6. return this.name;
  7. };
  8. MyObject.prototype.getMessage = function() {
  9. return this.message;
  10. };

思考:为了测试你是否理解闭包请看下面两段代码,请思考它们的运行结果是什么?并在留言区给出你的答案。

代码一:


  
  1. var name = "Window";
  2. var object = {
  3. name: "Object",
  4. getNameFunc: function() {
  5. return function() {
  6. return this.name;
  7. };
  8. }
  9. };
  10. console.log(object.getNameFunc()());

代码二:


  
  1. var name = "Window";
  2. var object = {
  3. name: "Object",
  4. getNameFunc: function() {
  5. var that = this;
  6. return function() {
  7. return that.name;
  8. };
  9. }
  10. };
  11. console.log(object.getNameFunc()());

8 总结

内部函数访问外部函数的变量时,采用的是链式查找的方式进行获取的,从里向外层层的搜索,搜索到了就直接使用,搜索到0级作用域的时候,如果还是没有找到这个变量,就报错,这种结构我们称为作用域链。本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁   局部变量是在函数中,函数使用结束后,局部变量就会被自动的释放,但是产生闭包后,里面的局部变量的使用作用域链就会被延长,闭包的作用是缓存数据这是闭包的优点也是缺点,这会导致变量不能及时的释放。如果想要缓存数据,就把这个数据放在外层的函数和里层的函数的中间位置。由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不要滥用闭包。

今天的学习就到这里了,由于本人能力和知识有限,如果有写的不对的地方,还请各位大佬批评指正。如果想继续学习提高,欢迎关注我,每天学习进步一点点,就是领先的开始。如果觉得本文对你有帮助的话,欢迎转发,评论,点赞!!!


转载:https://blog.csdn.net/qq_23853743/article/details/108553218
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场