Javascript的作用域、作用域链以及闭包详解

Heather ·
更新时间:2024-09-20
· 1276 次阅读

一、javascript中的作用域

①全局变量-函数体外部进行声明

②局部变量-函数体内部进行声明

1)函数级作用域

javascript语言中局部变量不同于C#、Java等高级语言,在这些高级语言内部,采用的块级作用域中会声明新的变量,这些变量不会影响到外部作用域。

而javascript则采用的是函数级作用域,也就是说js创建作用域的单位是函数。

例如:

在C#当中我们写如下代码:

static void Main(string[] args) { for (var x = 1; x < 10; x++) { Console.WriteLine(x.ToString()); } Console.WriteLine(x.ToString()); }

上面代码会出现如下的编译错误:

The name 'x' does not exist in the current context

同样在javascript当中写如下代码:

<script> function main() { for (var x = 1; x < 10; x++) { console.log(x.toString()); } console.log(x.toString()); } main(); </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中作用域的一个特性,例子如下:

function func(){ console.log(x); var x = 1; console.log(x); } func();

输出结果:

[Web浏览器] "undefined"

[Web浏览器] "1"

如上面的结果:有意思的是,为什么第一个输出是“undefined”呢?这就涉及到javascript中的“变量提升”,其实我感觉叫“声明提升”更好,因为这个机制就是把变量的声明提前到函数的前面。并不会把值也同样提升,也就是为什么第一次没有输出“1”的原因。

上面的代码就相当于:

function func(){ var x; console.log(x); var x = 1; console.log(x); } func(); 3)函数内部用不用“var”对程序的影响

这是个值得注意的地方:

var x = 1; function addVar(){ var x = 2; console.log(x); } addVar(); console.log(x);

输出:

[Web浏览器] "2"

[Web浏览器] "1"

当在函数内部去掉var之后,再执行:

var x = 1; function delVar(){ x = 2; console.log(x); } delVar(); console.log(x);

[Web浏览器] "2"

[Web浏览器] "2"

上面的例子说明了,在函数内部如果在声明变量没有使用var,那么声明的变量就是全局变量。

二、javascript的作用域链

先看如下的代码:

var name="Global"; function t(){         var name="t_Local";         function s1(){             var name="s1_Local";                 console.log(name);         }         function s2(){                 console.log(name);         }         s1();         s2(); } 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.闭包第一个作用

还是先看例子:

function s1() { var x = 123; return s2(); } function s2() { return x; } alert(s1());

这里我想弹出x的值,但是却发生了错误 "Uncaught ReferenceError: x is not defined",根据作用域链的知识,了解到因为s2中没有x的定义,并且向上找全局也没有x定义,所以就报错了。也就是说s1和s2不能够共享x的值。

那么问题来了,我想要访问s1当中的x,怎么弄?

修改代码如下:

function s1() {     var x = 123;     return function(){         return x;     }; } var test = s1(); console.log(test());

结果为:

[Web浏览器] "123"

解释:因为function本质也是数据,所以它和x的作用域相同,可以访问x。这样就相当于对外部开放了一个可以访问内部变量的接口。

还可以怎么玩,稍微修改一下代码

var func; function f(){ var x='123'; func=function(){     return x;     }; } f(); alert(func());

定义一个全局的变量,然后在函数内部让其等于一个函数,这样就可以通过这个全局变量来进行访问x了。

综上:闭包是啥?闭包就相当于函数外部和内部的桥梁,通过闭包可以在外部访问函数内部的变量。这也是闭包的第一个作用。

2.闭包的第二个作用

先看代码:

function f1(){     var n=1;     add=function(){         n+=1;     };     function f2(){         console.log(n);         return '输出完成';     }     return f2; } var res=f1(); console.log(res()); add(); console.log(res());

输出结果:

[Web浏览器] "1"

[Web浏览器] "输出完成"

[Web浏览器] "2"

[Web浏览器] "输出完成"

问题为什么第二次输出的结果n变成了2,没有被清除?

我的理解:res是一个全局变量,一直驻留在内存当中,它就相当于f2函数,f2函数又要依赖于f1函数,所以f1也驻留在内存当中,保证不被GC所回收,每一次调用add函数的时候,就相当于把f1中的n重新赋值了,这样n的值就变化了。这也是闭包的第二个作用,可以让变量的值始终保存在内存当中。

3.闭包的应用

①可以做访问控制(相当于C#当中的属性)

//定义两个变量,用于存储取值和存值 var get,set; //定义一个自调用函数,设定set和get方法 (function(){ //设定x的默认值     var x = 0;     set = function(n){         x = n;     }     get = function(){         return x;     } })(); console.log(get()); set(5); console.log(get());

输出结果:

[Web浏览器] "0"

[Web浏览器] "5"

②可以用做迭代器

//定义一个函数,里面使用了闭包 function foo(myArray){     var i=0;     //闭包迭代器     next=function(){         //每次返回数组的当前值,下标+1         return myArray[i++];     } } //调用foo,参数为一个数组 foo(['a','b','c','d']); //每次调用next都会打印数组的一个值 console.log(next()); console.log(next()); console.log(next()); console.log(next());

输出结果:

[Web浏览器] "a"

[Web浏览器] "b"

[Web浏览器] "c"

[Web浏览器] "d"

③闭包在循环中的使用

例1

function f(){ var a=[]; var i; for(i=0;i<3;i++){ a[i]=function(){ return i; }; } return a; } var test=f(); console.log(test[0]()); console.log(test[1]()); 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怎么搞?

function f(){     var a=[];     var i;     for(i=0;i<3;i++){         a[i]=(function(x){             return function(){                 return x;             }         })(i);     }     return a; } var test=f(); console.log(test[0]()); console.log(test[1]()); console.log(test[2]());

结果:

[Web浏览器] "0"

[Web浏览器] "1"

[Web浏览器] "2"



闭包 作用域 JavaScript

需要 登录 后方可回复, 如果你还没有账号请 注册新账号