[前端] Javascript的作用域、作用域链以及闭包详解

1848 0
Honkers 2022-10-21 15:40:57 | 显示全部楼层 |阅读模式
一、javascript中的作用域

①全局变量-函数体外部进行声明
②局部变量-函数体内部进行声明
1)函数级作用域

javascript语言中局部变量不同于C#、Java等高级语言,在这些高级语言内部,采用的块级作用域中会声明新的变量,这些变量不会影响到外部作用域。
而javascript则采用的是函数级作用域,也就是说js创建作用域的单位是函数。
例如:
在C#当中我们写如下代码:
  1. static void Main(string[] args)
  2. {
  3.      for (var x = 1; x < 10; x++)
  4.      {
  5.            Console.WriteLine(x.ToString());
  6.      }
  7.      Console.WriteLine(x.ToString());
  8. }
复制代码
上面代码会出现如下的编译错误:
The name 'x' does not exist in the current context
同样在javascript当中写如下代码:
  1. <script>
  2.     function main() {
  3.         for (var x = 1; x < 10; x++) {
  4.              console.log(x.toString());
  5.          }
  6.          console.log(x.toString());
  7.      }
  8.      main();
  9. </script>
复制代码
输出结果如下:
[Web浏览器] "1"
[Web浏览器] "2"
[Web浏览器] "3"
[Web浏览器] "4"
[Web浏览器] "5"
[Web浏览器] "6"
[Web浏览器] "7"
[Web浏览器] "8"
[Web浏览器] "9"
[Web浏览器] "10"
这就说明了,“块级作用域”和“函数级作用域”的区别,块级作用域当离开作用域后,外部就不能用了,就像上面的C#例子,"x"离开for循环后就不能用了,但是javascript中不一样,它还可以进行访问。
2)变量提升

再看javascript中作用域的一个特性,例子如下:
  1. function func(){
  2.     console.log(x);
  3.     var x = 1;
  4.     console.log(x);
  5. }        
  6. func();
复制代码
输出结果:
[Web浏览器] "undefined"
[Web浏览器] "1"
如上面的结果:有意思的是,为什么第一个输出是“undefined”呢?这就涉及到javascript中的“变量提升”,其实我感觉叫“声明提升”更好,因为这个机制就是把变量的声明提前到函数的前面。并不会把值也同样提升,也就是为什么第一次没有输出“1”的原因。
上面的代码就相当于:
  1. function func(){
  2.     var x;
  3.     console.log(x);
  4.     var x = 1;
  5.     console.log(x);
  6. }        
  7. func();
复制代码
3)函数内部用不用“var”对程序的影响

这是个值得注意的地方:
  1. var x = 1;
  2. function addVar(){
  3.     var x = 2;
  4.     console.log(x);            
  5. }
  6. addVar();
  7. console.log(x);
复制代码
输出:
[Web浏览器] "2"
[Web浏览器] "1"
当在函数内部去掉var之后,再执行:
  1. var x = 1;
  2. function delVar(){
  3.        x = 2;
  4.        console.log(x);            
  5.   }
  6. delVar();
  7. console.log(x);
复制代码
[Web浏览器] "2"
[Web浏览器] "2"
上面的例子说明了,在函数内部如果在声明变量没有使用var,那么声明的变量就是全局变量。
二、javascript的作用域链

先看如下的代码:
  1. var name="Global";
  2. function t(){
  3. var name="t_Local";
  4. function s1(){
  5. var name="s1_Local";
  6. console.log(name);
  7. }
  8. function s2(){
  9. console.log(name);
  10. }
  11. s1();
  12. s2();
  13. }
  14. t();
复制代码
输出结果:
[Web浏览器] "s1_Local"
[Web浏览器] "t_Local"
那么就有几个问题:
    1.为什么第二次输出的不是s1_Local?2.为什么不是Global?
解决这个两个问题就在于作用域链…
下面就解析一下这个过程,


例如当上面程序创建完成的时候,会形成上图中的链状结构(假想的),所以每次调用s1()函数的时候,console.log(name);先会在他自己的作用域中寻找name这个变量,这里它找到name=“s1_Local”,所以就输出了s1_Local,而每次调用s2()的时候,它和s1()函数过程一样,只不过在自身的作用域没有找到,所以向上层查找,在t()函数中找到了,于是就输出了"t_Local"。
同样如果我们可以验证一下,如果把t中的name删除,可以看看输出是不是“Global”
结果如下:
[Web浏览器] "s1_Local"
[Web浏览器] "Global"
当然具体每一个作用域直接是如何连接的,请参考
https://www.jb51.net/article/28610.htm
其中也说明了为什么JS当中应该尽量减少with关键字的使用。
三、闭包

了解了作用域和作用域链的概念之后,再去理解闭包就相对容易了。
1.闭包第一个作用

还是先看例子:
  1. function s1() {
  2.     var x = 123;
  3.     return s2();
  4. }
  5. function s2() {
  6.     return x;
  7. }
  8. alert(s1());
复制代码
这里我想弹出x的值,但是却发生了错误 "Uncaught ReferenceError: x is not defined",根据作用域链的知识,了解到因为s2中没有x的定义,并且向上找全局也没有x定义,所以就报错了。也就是说s1和s2不能够共享x的值。
那么问题来了,我想要访问s1当中的x,怎么弄?
修改代码如下:
  1. function s1() {
  2. var x = 123;
  3. return function(){
  4. return x;
  5. };
  6. }
  7. var test = s1();
  8. console.log(test());
复制代码
结果为:
[Web浏览器] "123"


解释:因为function本质也是数据,所以它和x的作用域相同,可以访问x。这样就相当于对外部开放了一个可以访问内部变量的接口。
还可以怎么玩,稍微修改一下代码
  1. var func;
  2. function f(){
  3. var x='123';
  4. func=function(){
  5. return x;
  6. };
  7. }
  8. f();
  9. alert(func());
复制代码
定义一个全局的变量,然后在函数内部让其等于一个函数,这样就可以通过这个全局变量来进行访问x了。
综上:闭包是啥?闭包就相当于函数外部和内部的桥梁,通过闭包可以在外部访问函数内部的变量。这也是闭包的第一个作用。
2.闭包的第二个作用

先看代码:
  1. function f1(){
  2. var n=1;
  3. add=function(){
  4. n+=1;
  5. };
  6. function f2(){
  7. console.log(n);
  8. return '输出完成';
  9. }
  10. return f2;
  11. }
  12. var res=f1();
  13. console.log(res());
  14. add();
  15. console.log(res());
复制代码
输出结果:
[Web浏览器] "1"
[Web浏览器] "输出完成"
[Web浏览器] "2"
[Web浏览器] "输出完成"
问题为什么第二次输出的结果n变成了2,没有被清除?
我的理解:res是一个全局变量,一直驻留在内存当中,它就相当于f2函数,f2函数又要依赖于f1函数,所以f1也驻留在内存当中,保证不被GC所回收,每一次调用add函数的时候,就相当于把f1中的n重新赋值了,这样n的值就变化了。这也是闭包的第二个作用,可以让变量的值始终保存在内存当中。
3.闭包的应用

①可以做访问控制(相当于C#当中的属性)
  1. //定义两个变量,用于存储取值和存值
  2. var get,set;
  3. //定义一个自调用函数,设定set和get方法
  4. (function(){
  5.     //设定x的默认值
  6. var x = 0;
  7. set = function(n){
  8. x = n;
  9. }
  10. get = function(){
  11. return x;
  12. }
  13. })();
  14. console.log(get());
  15. set(5);
  16. console.log(get());
复制代码
输出结果:
[Web浏览器] "0"
[Web浏览器] "5"
②可以用做迭代器
  1. //定义一个函数,里面使用了闭包
  2. function foo(myArray){
  3. var i=0;
  4. //闭包迭代器
  5. next=function(){
  6. //每次返回数组的当前值,下标+1
  7. return myArray[i++];
  8. }
  9. }
  10. //调用foo,参数为一个数组
  11. foo(['a','b','c','d']);
  12. //每次调用next都会打印数组的一个值
  13. console.log(next());
  14. console.log(next());
  15. console.log(next());
  16. console.log(next());
复制代码
输出结果:
[Web浏览器] "a"
[Web浏览器] "b"
[Web浏览器] "c"
[Web浏览器] "d"
③闭包在循环中的使用
例1
  1. function f(){
  2.     var a=[];
  3.     var i;
  4.     for(i=0;i<3;i++){
  5.         a[i]=function(){
  6.             return i;
  7.         };
  8.     }
  9.     return a;
  10. }
  11. var test=f();
  12. console.log(test[0]());
  13. console.log(test[1]());
  14. console.log(test[2]());
复制代码
输出结果:
[Web浏览器] "3"
[Web浏览器] "3"
[Web浏览器] "3"
为什么结果不是0、1、2?
这里我们使用了闭包,每次循环都指向了同一个局部变量i,但是闭包不会记录每一次循环的值,只保存了变量的引用地址,所以当我们在调用test[0]()、test[1]()、test[2]()的时候都返回的是for循环最后的值,也就是3的时候跳出了循环。
例2:我想输出0,1,2怎么搞?
  1. function f(){
  2. var a=[];
  3. var i;
  4. for(i=0;i<3;i++){
  5. a[i]=(function(x){
  6. return function(){
  7. return x;
  8. }
  9. })(i);
  10. }
  11. return a;
  12. }
  13. var test=f();
  14. console.log(test[0]());
  15. console.log(test[1]());
  16. console.log(test[2]());
复制代码
结果:
[Web浏览器] "0"
[Web浏览器] "1"
[Web浏览器] "2"
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持中国红客联盟。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

荣誉红客

关注
  • 4010
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行