JavaScript从作用域到闭包

时间:2022-09-26 10:13:13

目录

作用域

  全局作用域和局部作用域

  块作用域与函数作用域

  作用域中的声明提前

作用域链

函数声明与赋值

  声明式函数、赋值式函数与匿名函数

  代码块  

  自执行函数

闭包 


作用域(scope)

全局作用域和局部作用域

通常来讲这块是全局变量与局部变量的区分。 参考引文:JavaScript 开发进阶:理解 JavaScript 作用域和作用域链

全局作用域:最外层函数和在最外层函数外面定义的变量拥有全局作用域。

  1)最外层函数和在最外层函数外面定义的变量拥有全局作用域

  2)所有末定义直接赋值的变量自动声明为拥有全局作用域,即没有用var声明的变量都是全局变量,而且是顶层对象的属性。

  3)所有window对象的属性拥有全局作用域

局部作用域:和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以在一些地方也会看到有人把这种作用域称为函数作用域。

代码部分请参照引文。

块作用域与函数作用域

函数作用域是相对块作用域来进行解释的,其和局部作用域是一个意思。参考引文:JavaScript的作用域和块级作用域概念理解

块作用域:任何一对花括号{}中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是无效的,我们称之为块级作用域。

函数作用域:在函数中的参数和变量在函数外部是无法访问的。JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。下文会解释。

 //C语言
#include <stdio.h>
void main()
{
int i=;
i--;
if(i)
{
int j=;
}
printf("%d/n",j);
}

运行这段代码,会出现“use an undefined variable:j”的错误。可以看到,C语言拥有块级作用域,因为j是在if的语句块中定义的,因此,它在块外是无法访问的。

 function test(){
for(var i=0;i<3;i++){};
alert(i);
}
test();

运行这段代码,弹出"3",可见,在块外,块中定义的变量i仍然是可以访问的。也就是说,JS并不支持块级作用域,它只支持函数作用域,而且在一个函数中的任何位置定义的变量在该函数中的任何地方都是可见的。

作用域中的声明提前

var scope="global";  //全局变量
function t(){
console.log(scope);
var scope="local" ;//局部变量
console.log(scope);
}
t();

(console.log()是控制台的调试工具,chrome叫检查,有的浏览器叫审查元素,alert()弹窗会破坏页面效果)

第一句输出的是: "undefined",而不是 "global"

第二讲输出的是:"local"

第二个不用说,就是局部变量输出"local"。第一个之所以也是"local",是因为Js中的声明提前,尽管在第4行才进行局部变量的声明与赋值,但其实是将第4行的声明提前了,放在了函数体顶部,然后在第4行进行局部变量的赋值。可以理解为下面这样。

var scope="global";//全局变量
function t(){
var scope;//局部变量声明
console.log(scope);
scope="local";//局部变量赋值
console.log(scope);
}
t();

具体细节可以查阅犀牛书(《JavaScript权威指南》)中的详细介绍。

作用域链(Scope Chain)

当代码在一个环境中执行时,会创建变量对象的的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是一个函数,则将其活动对象作为变量对象。参考引文:Js作用域与作用域链详解,浅析作用域链–JS基础核心之一

num="one";
var a = 1;
function t(){ //t函数的局部作用域,可以访问到a,b变量,但是访问不到c变量
var num="two";
var b = 2;
function A(){ //A函数局部作用域,可以访问到a,b,c变量
var num="three"; //局部变量与外部变量重名以局部变量为主
var c = 3;
console.log(num); //three
        }
function B(){ //B函数局部作用域,可以访问到a,b变量,访问不到c变量
console.log(num); //two
         }
A();
B();
}
t();

当执行A时,将创建函数A的执行环境(调用对象),并将该对象置于链表开头,然后将函数t的调用对象链接在之后,最后是全局对象。然后从链表开头寻找变量num。

即:A()->t()->window,所以num是”three";

但执行B()时,作用域链是: B()->t()->window,所以num是”two";

另外,有一个特殊的例子我觉得应该发一下。利用“JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。” 这句话,解释了下面的例子。

var x = 10;

function a() {
console.log(x);
} function b () {
var x = 5;
a();
} b();//输出为10

虽然b函数调用了a,但是a定义在全局作用域下,同样也是运行在全局作用域下的,所以其内部的变量x,向上寻找到了全局变量x=10;所以b函数的输出为10;

更深层次的讲解请参照:JavaScript 开发进阶:理解 JavaScript 作用域和作用域链

经典案例

下面是一个经典的事件绑定例子:

<div id = "test">
<p>栏目1</p>
<p>栏目2</p>
<p>栏目3</p>
<p>栏目4</p>
</div>
</body>
<script type="text/javascript">
function bindClick(){
var allP = document.getElementById("test").getElementsByTagName("p"),
i=0,
len = allP.length;
for( ;i<len;i++){
allP[i].onclick = function(){
alert("you click the "+i+" P tag!");//you click the 4 P tag!
}
}
}
bindClick();//运行函数,绑定点击事件
</script>

上面的代码给P标签添加点击事件,但是不管我们点击哪一个p标签,我们获取到的结果都是“you click the 4 P tag!”。

我们可以把上述的JS代码给分解一下,让我们看起来更容易理解,如下所示。前面使用一个匿名函数作为click事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。

function bindClick(){
var allP = document.getElementById("test").getElementsByTagName("p"),
i=0,
len = allP.length;
for( ;i<len;i++){
allP[i].onclick = AlertP;
}
function AlertP(){
alert("you click the "+i+" P tag!");
}
}
bindClick();//运行函数,绑定点击事件

这里应该没有什么问题吧,前面使用一个匿名函数作为click事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。也可以做下测试哦。

理解上面的说法了,那么就可以很简单的理解,为什么我们之前的代码,会得到一个相同的结果了。首先看一下for循环中,这里我们只是对每一个匹配的元素添加了一个click的回调函数,并且回调函数都是AlertP函数。这里当为每一个元素添加成功click之后,i的值,就变成了匹配元素的个数,也就是i=len,而当我们触发这个事件时,也就是当我们点击相应的元素时,我们期待的是,提示出我们点击的元素是排列在第几行。当click事件触发时,执行回调函数AlertP,但是当执行到这里的时候,发现alert方法中,有一个变量是未知的,并且在AlertP的局部作用域中,也没有查找到相应的变量,那么按照作用域链的查找方式,就会向父级作用域去查找,这里的父级作用域中,确实是有变量i的,而i的值,却是经过for循环之后的值,i=len。所以也就出现了我们最初看到的效果。

解决办法如下所示:

function bindClick(){
var allP = document.getElementById("test").getElementsByTagName("p"),
  i=0,
  len = allP.length;
for( ;i<len;i++){
AlertP(allP[i],i);
}
function AlertP(obj,i){
obj.onclick = function(){
alert("you click the "+i+" P tag!");
}
}
}
bindClick();

这里,objiAlertP函数内部,就是局部变量了。click事件的回调函数,虽然依旧没有变量i的值,但是其父作用域AlertP的内部,却是有的,所以能正常的显示了,这里AlertP我放在了bindClick的内部,只是因为这样可以减少必要的全局函数,放到全局也不影响的。

这里是添加了一个函数进行绑定,如果我不想添加函数呢,当然也可以实现了,这里就要说到自执行函数了。可以跳到本文的自执行函数,也可以看参考引文的深度讲解:浅析作用域链–JS基础核心之一

函数声明与赋值

声明式函数、赋值式函数与匿名函数

匿名函数:function () {}; 使用function关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。下文会讲到。

JS中的函数定义分为两种:声明式函数与赋值式函数。

<script type="text/javascript">
Fn(); //执行结果:"执行了声明式函数",在预编译期声明函数及被处理了,所以即使Fn()调用函数放在声明函数前也能执行。
function Fn(){ //声明式函数
alert("执行了声明式函数");
}
</script>
<script type="text/javascript">
Fn(); //执行结果:"Fn is not a function"
var Fn = function(){ //赋值式函数
alert("执行了赋值式函数");
}
</script>

JS的解析过程分为两个阶段:预编译期(预处理)与执行期。
预编译期JS会对本代码块中的所有声明的变量和函数进行处理(类似与C语言的编译),此时处理函数的只是声明式函数,而且变量也只是进行了声明(声明提前)但未进行初始化以及赋值。所以才会出现上面两种情况。

当正常情况,函数调用在声明之后,同名函数会覆盖前者。

<script type="text/javascript">
function Fn(){ //声明式函数
alert("执行了声明式函数");
}
var Fn = function(){ //赋值式函数
alert("执行了赋值式函数");
}
Fn();//执行结果:"执行了赋值式函数",同名函数后者会覆盖前者
</script>

同理当提前调用声明函数时,也存在同名函数覆盖的情况。

<script type="text/javascript">
Fn(); //执行结果:"执行了函数2",同名函数后者会覆盖前者
function Fn(){ //函数1
alert("执行了函数1");
}
function Fn(){ //函数2
alert("执行了函数2");
}
</script>

 

代码块

JavaScript中的代码块是指由<script>标签分割的代码段。JS是按照代码块来进行编译和执行的,代码块间相互独立,但变量和方法共享。如下:

<script type="text/javascript">//代码块一
var test1 = "我是代码块一test1";
alert(str);//因为没有定义str,所以浏览器会出错,下面的不能运行
alert("我是代码块一");//没有运行到这里
var test2 = "我是代码块一test2";//没有运行到这里但是预编译环节声明提前了,所以有变量但是没赋值
</script>
<script type="text/javascript">//代码块二
alert("我是代码块二"); //这里有运行到
alert(test1); //弹出"我是代码块一test1"
alert(test2); //弹出"undefined"
</script>

上面的代码中代码块一中运行报错,但不影响代码块二的执行,这就是代码块间的独立性,而代码块二中能调用到代码一中的变量,则是块间共享性。

但是当第一个代码块报错停止后,并不影响下一个代码块运行。当然在下面的例子中,虽然代码块二中的函数声明预编译了,但是在代码块1中的函数出现Fn函数为定义错误(浏览器报错,并不是声明未赋值的undefined),说明代码块1完全执行后才执行代码块2。

<script type="text/javascript">//代码块1
Fn(); //浏览器报错:"undefined",停止代码块1运行
alert("执行了代码块1");//未运行
</script>
<script type="text/javascript">//代码块2
alert("执行了代码块2");//执行弹框效果
function Fn(){ //函数1
alert("执行了函数1");
}
</script>
所以js函数解析顺序如下:
  step 1. 读入第一个代码块。
  step 2. 做语法分析,有错则报语法错误(比如括号不匹配等),并跳转到step5。
  step 3. 对var变量和function定义做“预编译处理”(永远不会报错的,因为只解析正确的声明)。
  step 4. 执行代码段,有错则报错(比如变量未定义)。
  step 5. 如果还有下一个代码段,则读入下一个代码段,重复step2。
  step6. 结束。

:需要在页面元素渲染前执行的js代码应该放在<body>前面的<script>代 码块中,而需要在页面元素加载完后的js放在</body>元素后面,body标签的onload事件是在最后执行的。

<script type="text/javascript">
alert("first");
function Fn(){
alert("third");
}
</script>
<body onload="Fn()">
</body>
<script type="text/javascript">
alert("second");
</script>

自执行函数

也就是在函数名后添加括号,函数就会自执行。在绑定事件时,像我这样的初学者有时会犯如下的错误,window.onclick = ab();这样函数ab一开始就会执行。正确的做法应该将ab后的括号去掉。而这种加括号的做法其实是把ab函数运行的结果赋值给点击事件。

下面两个例子清楚地反映了函数赋值后的情况。

1:

function ab () {
var i=0;
alert("ab");
return i;
}
var c=ab();//执行ab函数
alert(typeof c+" "+c);//number 0

2:

function ab () {
var i=0;
alert("ab");
return i;
}
var c=ab;//只赋值
alert(typeof c+" "+c);//function function ab () {var i=0;alert("ab");return i;}

注:但是这个函数必须是函数表达式(诸如上文提到的赋值式函数),不能是函数声明。详细请看:js立即执行函数:(function(){...})()与(function(){...}())

文中主要讲到匿名函数的自执行方法,即在function前面加!、+、 -甚至是逗号等到都可以起到函数定义后立即执行的效果,而()、!、+、-、=等运算符,都将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义,告诉javascript引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码(jq使用的就是这种方法)。举例如下所示。

(function(a){
console.log(a); //firebug输出123,使用()运算符
})(123); (function(a){
console.log(a); //firebug输出1234,使用()运算符
}(1234)); !function(a){
console.log(a); //firebug输出12345,使用!运算符
}(12345); +function(a){
console.log(a); //firebug输出123456,使用+运算符
}(123456); -function(a){
console.log(a); //firebug输出1234567,使用-运算符
}(1234567); var fn=function(a){
console.log(a); //firebug输出12345678,使用=运算符
}(12345678)

其作用就是:实现块作用域。

javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以( function(){…} )()内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。代码如下:

function test(){
(function (){
for(var i=0;i<4;i++){
}
})();
alert(i); //浏览器错误:i is not defined
}
test();

可以对比最开始介绍作用域时候的代码。

闭包(Closure)

闭包对于初学者来说很难,需要学习很多很多才能领会,所以也是先把作用域链匿名函数的知识作为铺垫。我这里的闭包内容属于基础篇,以后可能会贴一些更为核心的内容。我这里参照了大神们的讲解来说。参考引文:学习Javascript闭包(Closure),JavaScript 匿名函数(anonymous function)与闭包(closure),浅析作用域链–JS基础核心之一

闭包是能够读取其他函数内部变量的函数,所以在本质上,闭包将函数内部和函数外部连接起来的一座桥梁。

闭包是在函数执行结束,作用域链将函数弹出之后,函数内部的一些变量或者方法,还可以通过其他的方法引用。

两个用处:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

为了帮助理解,我找了几个例子:

1.(阮一峰老师的讲解)

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); //
  nAdd();
  result(); //

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

2.(某大神)

function foo() {
var a = 10;
function bar() {
a *= 2;
return a;
}
return bar;
}
var baz = foo();
alert(baz()); //
alert(baz()); //40
alert(baz()); // var blat = foo();
alert(blat()); //

现在可以从外部访问 a; 
a 是运行在定义它的 foo 中,而不是运行在调用 foo 的作用域中。 只要 bar 被定义在 foo 中,它就能访问 foo 中定义的变量 a,即使 foo 的执行已经结束。也就是说,按理,"var baz = foo()" 执行后,foo 已经执行结束,a 应该不存在了,但之后再调用 baz 发现,a 依然存在。这就是 JavaScript 特色之一——运行在定义,而不是运行的调用。 
其中, "var baz = foo()" 是一个 bar 函数的引用;"var blat= foo()" 是另一个 bar 函数引用。 
用闭包还可实现私有成员,但是我还没理解,所以就先不贴出来,想看的请参照参考引文:JavaScript 匿名函数(anonymous function)与闭包(closure)

结束

第一次写这么长的文章,大部分是引用,但是所有内容都是亲自实践并思考后才贴出来,作为初学者可能有解释和引用不当的地方,还请大家指出。有问题的地方还请各位老师同学多来指教探讨。

再次感谢所有引文作者,知识的增长在于传播,感谢辛苦的传播者。

参考文献:

JavaScript 开发进阶:理解 JavaScript 作用域和作用域链,

JavaScript的作用域和块级作用域概念理解,

Js作用域与作用域链详解,

浅析作用域链–JS基础核心之一,

javascript 匿名函数的理解(透彻版),

JS中函数执行顺序的问题,

js立即执行函数:(function(){...})()与(function(){...}()), 

学习Javascript闭包(Closure),

JavaScript 匿名函数(anonymous function)与闭包(closure)

JavaScript从作用域到闭包的更多相关文章

  1. 我认知的javascript之作用域和闭包

    说到javascript,就不得不说javascript的作用域和闭包:当然,还是那句老话,javascript在网上都说得很透彻了,我也就不过多的强调了: 作用域:javascript并没有像其他的 ...

  2. 剖析JavaScript函数作用域与闭包

    在我们写代码写到一定阶段的时候,就会想深究一下js,javascript是一种弱类型的编程语言,而js中一个最为重要的概念就是执行环境,或者说作用域.作用域重要性体现在哪呢?首先,函数在执行时会创建作 ...

  3. 你不知道的JavaScript(作用域和闭包)

    作用域和闭包 ・作用域 引擎:从头到尾负责整个JavaScript的编译及执行过程. 编译器:负责语法分析及代码生成等. 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非 ...

  4. JavaScript 函数作用域和闭包

    函数作用域和闭包  词法作用域   它们在定义它们的作用域里运行,而不是在执行的作用域运行,但是只有在运行时,作用域链中的属性才被 定义(调用对象),此时,可访问任何当前的绑定.   调用对象     ...

  5. javascript的作用域和闭包(三)闭包与模块

    一些很重要的说明:前面三篇博客详细的介绍了,引擎与编译器和作用域的关系,重点需要理解的是编译器中的分词与词法分析,JavaScript的特有的“赋值操作的左右侧”引用操作:编译阶段的词法作用域的工作原 ...

  6. JavaScript的作用域与闭包

    JavaScript的作用域以函数为界,不同的函数拥有相对独立的作用域.函数内部可以声明和访问全局变量,也可以声明局部变量(使用var关键字,函数的参数也是局部变量),但函数外部无法访问内部的局部变量 ...

  7. JavaScript之作用域和闭包

    一.作用域 作用域共有两种主要的工作模型:第一种是最为普遍的,被大多数编程语言所采用的词法作用域,另外一种叫作动态作用域: JavaScript所采用的作用域模式是词法作用域. 1.词法作用域 词法作 ...

  8. 前端知识体系:JavaScript基础-作用域和闭包-JavaScript的作用域和作用域链

    JavaScript的作用域和作用域链 作用域: 变量的作用域无非两种:全局作用域和局部作用域 全局作用域: 最外层函数定义的变量拥有全局作用域.即对任何内部函数来说都是可以访问的. <scri ...

  9. Javascript的作用域和闭包(一)

    一.作用域是什么? 几乎所有的编程语言最基本的功能之一,就是能够存储变量的值,并且能访问和修改这些值. 修改变量值的过程我们通常在程序执行时,称为改变一个对象的状态.有了状态,让程序变得有非常有趣. ...

随机推荐

  1. php引用&amp&semi;符号详解——————给变量起小名

    学习了这篇博客[http://blog.csdn.net/jiedushi/article/details/6428585] php中引用采用的是“写时拷贝”的原理,就是除非发生写操作,指向同一个地址 ...

  2. C&num; 网络编程之豆瓣OAuth2&period;0认证具体解释和遇到的各种问题及解决

            近期在帮人弄一个豆瓣API应用,在豆瓣的OAuth2.0认证过程中遇到了各种问题,同一时候自己须要一个个的尝试与解决,终于完毕了豆瓣API的訪问.作者这里就不再吐槽豆瓣的认证文档了,毕 ...

  3. 基于8211lib库对s57电子海图的解析和存储

    电子海图是为适用航海需要而绘制的包含海域地理信息和航海信息的一种数字化的专题地图,符合国际标准的电子海图数据统称为S-57电子海图.本文主要在S-57电子海图数据的理论模型和数据结构的基础上,实现对S ...

  4. JAVA的继承,构造函数,窗体

    import java.awt.*; import javax.swing.*; import javax.swing.JFrame; import java.awt.event.WindowList ...

  5. 从零开始,轻松搞定SpringCloud微服务系列

    本系列博文目录 [微服务]之一:从零开始,轻松搞定SpringCloud微服务系列–开山篇(spring boot 小demo) [微服务]之二:从零开始,轻松搞定SpringCloud微服务系列–注 ...

  6. pycharm上传代码到远程服务器

    本来不打算写了,可是,还是记不住 源自https://blog.csdn.net/zhangyu4863/article/details/80188207 我的是pycharm2018.1.4专业版: ...

  7. 第 9 章 数据管理 - 077 - 跨主机使用 Rex-Ray volume

    跨主机使用 Rex-Ray volume 在docker1上创建mysql容器,并挂载使用mysqldata数据卷 磁盘文件直接挂载在了docker1 上 验证数据 也是存在的 Rex-Ray 可以提 ...

  8. 黄聪:Windows2012-IIS8安装SSL证书

    开始菜单->右上角搜索按钮-> mmc 弹出窗口 关闭控制台会提示保存 打开iis新建站点,为了兼容不支持sni的浏览器,绑定默认https

  9. 【vue】vue-router的用法

    依赖安装:(c)npm install vue-router 过程: import Vue from 'vue'; import Router from 'vue-router'; Vue.use(R ...

  10. 关于C&num;中”扩展方法必须在非泛型静态类中定义&OpenCurlyDoubleQuote;问题的解决

    问题描述: 在某个窗口下的编码中使用了以下扩展方法FindControl,以求根据字符串的值去操作控件(本文中的控件为Label控件)的属性. public static Control FindCo ...

相关文章