JS中的作用域,大家都知道的,分为全局作用域和局部作用域,没有块级作用域,听起来其实很简单的,可是作用域是否能够有深入的了解,对于JS代码逻辑的编写成功率,BUG的解决能力,以及是否能写出更优秀的代码,都有很重要的影响的,如果想要写出更优雅更高效的逻辑代码,那么就要深入的了解一下作用域的问题了,确切的说,是要更深入的了解一下,怎么更有效更巧妙的利用作用域。

全局和局部作用域

这个我觉得吧,只要学习过编程语言的,就会对这些有简单的了解的。比如在JS语言中,属于window对象的属性和方法,是可以被我们自定义的函数或者方法的局部作用域访问的,而我们自定义的函数和对象内部的属性和方法,却只能在内部使用。这里,window对象就是在全局作用域中,而我们自定义的函数或者对象内部,就是局部作用域。

  • var num = 1;
  • function changeNum(){
  • var str = "zhang";
  • num = 2;
  • }
  • console.log(num);       //1
  • console.log(typeof str);//undefined
  • changeNum();
  • console.log(num);       //2
  • console.log(typeof str);//undefined

上述代码中,之所以要使用typeof str,是因为对于没有定义的变量,浏览器会抛出错误,并且阻塞浏览器继续执行后续代码的。

注:如果确定要定义为局部变量,那么千万不要忘记使用 var 操作符哦。

局部作用域的位置一般是在函数或者对象内部,为了叙述方便,接下来就只以函数的局部作用域来进行分析说明。

在函数中使用var操作符定义一个变量,那么当这个函数执行完毕之后,这个变量也会被销毁(也有的情况下不会,比如闭包,后面会说明),而全局变量会一直存在。所以在我们写代码时,尽量少的使用全局变量,滥用全局变量,简直就是一个会令人恶心的习惯,因为它会带来很多不必要的麻烦。

  • 1:变量过多,命名麻烦
  • 2:局部变量,忘记使用var定义,修改了全局变量,这样的错误对于代码的维护简直是噩梦
  • 3:全局变量会在页面卸载前一直存在,损耗不必要的内存。

暂时就想到这些,反正就是尽量少用就对了。。。。

作用域链

引自Javascript高级程序设计(第三版)(P73):当代码在一个环境中执行时,会创建变量对象的的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是一个函数,则将其活动对象作为变量对象。

每一个函数都有自己的执行环境,当执行流进一个函数时,函数环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境,这个栈也就是作用域链。

上面写了那么多,在我看起来可以用下面的简单代码来表达:

  • var a = 1;
  • //全局作用域,只能访问全局变量,也就是a变量
  • function A(){
  • var b = 2;
  • //A函数的局部作用域,可以访问到a,b变量,但是访问不到c变量
  • function B(){
  • //B函数局部作用域,可以访问到a,b,c变量
  • var c = 3;
  • }
  • }

很明显的,貌似作用域方面,也没有什么好说的。可是,有时候,我们却不得不去访问一些局部作用域内部的东西,比如两个模块函数,使用了相同的数据,这里我们也只能把这些相同的数据放入全局变量,使得两个函数模块,都可以调用这些数据。

但是想想,如果这样的需求很多,那么不久需要很多很多的全局变量,而滥用全局变量的不好之处,前面也说了,所以这并不是一种好的写法。

减少全局变量

减少全局变量的方法,其实也很多,比如把一些相同类型的全局变量存入一个对象,那么就可以把这些类型的N多个全局变量,变成一个全局的对象,之后按照对象访问即可。

当然,我觉得吧,最简单,又好用的,还是在一个函数内部,继续定义函数,就像之前在函数A内部,定义了函数B,这样我们只需要一个函数A的执行,就可以完成一整个逻辑。内部的调用,都只能算是局部变量的调用,在全局只添加了一个函数A

比如:

  • function A(){
  • var arr = [];
  • function a(){};
  • function b(){};
  • return;
  • }

这样,我们本来需要三个全局变量的问题,就变成了只需要一个。当然,如何减少全局变量的方法是有很多种的,这里不做讨论。

这里,我们就讨论一种我们最常见的方法,也算是很常用的一种代码书写方法吧,它叫:闭包。

减少全局变量方法–闭包

说到闭包,我们首先来看一个最最简单的例子,也是最最基础的例子:为多个相同的元素,绑定事件,在点击每一个元素时,提示被点击元素的排列位置。

  • <div id = "test">
  • <p>栏目1</p>
  • <p>栏目2</p>
  • <p>栏目3</p>
  • <p>栏目4</p>
  • </div>

这样的结构

  • 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();
  • //运行函数,绑定点击事件

这样的JS处理,看起来没有问题,可是在测试的时候,不管我们点击哪一个p标签,我们获取到的结果都是相同的,tell me why?说白了,这就是作用域到导致的一个问题。

下面来分析一下原因。首先呢,我们先把上述的JS代码给分解一下,让我们看起来更容易理解。

  • 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。所以也就出现了我们最初看到的效果。

了解了这里的原因,那么解决方法也就很简单了,控制这个作用域的问题呗,说白了,也就一个方法,那就是在回调函数中,用一个局部变量,来记录这个i的值,这样当再局部作用域中使用到i变量时,就会使用优先使用局部变量中的i变量的值。不会再去查找全局变量了。

所以呢,理解了这两段文字,那么如果我把代码写成下面的样式:

  • 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();
  • //运行函数,绑定点击事件

分析一下,如果这段代码这样写,那么结果会是如何呢?

说到了这里,大概也能理解一下闭包的概念了,按照之前我们说的作用域链的说法,当一个函数运行时,该函数就会被推入作用域链的前端,当函数执行结束,这个函数就会被推出作用域链,并且销毁函数内部的局部变化和方法。

但是这里呢,当bindClick运行结束后,依然可以通过click事件访问到bindClick函数内部的i变量,说明bindClick函数内部的i变量,在bindClick结束后,并没有被销毁,这也就是闭包了。

2014.10.19-PS:发现上面的这段代码,是有问题的,这样的写法,在运行时,i的值会一直是undefined,因为这个时候,i是在AlertP内部和全局作用域中查找,而这两个作用域中,并没有i的定义,正确的写法,在文章的后面有说明,所以现在想不到当时为什么会这么写了。。汗一个~~
PS:闭包,说白了也就是在函数执行结束,作用域链将函数弹出之后,函数内部的一些变量或者方法,还可以通过其他的方法引用。

OK,回到正题,这里既然知道了需要一个局部变量的i值,可以解决这个问题,那么方法也就很简单了,按我们之前说的,变量按照可访问性的话,只分为全局变量和局部变量,那么这里的就很简单了,使用一个函数,构造一个局部变量即可。

方法1:使得绑定click事件的目标对象和变量i都变成局部变量。这里可以直接把这两者作为形参,传递给另外的一个函数即可。

  • 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时,会这样绑定事件:obj.onclick = callback();

然后出错了却一直找不到错误在哪里,后来才之后,当一个函数名添加了括号之后,就是函数执行了,那么也就明白了,上面的写法,其实就是把callback函数执行后的返回结果作为了objclick事件的回调函数了。

而函数名的话,也就是一个function函数的引用吧,根据函数名查找到对应的function处理模块,所以这里很容易的也就想到了,自执行函数也就是直接在一个匿名函数的后面添加一对小括号,那么这个匿名函数就会自己执行了。所以也就是自执行函数了。

比如我们在页面加载之后,想要立即提示用户,页面加载完毕,我们习惯于这么写:

  • function loadSuccess(){
  • alert("page onload success!");
  • }
  • loadSuccess();

这是我们常用的方法,这里首先定义个函数,并把函数名命名为loadSuccess,之后调用这个函数。很常用很简单。

这里我们通常也可以使用自执行函数来完成这个提示,你就可以这样写:

  • (function(){
  • alert("page onload success!");
  • })();

完成相同的功能,这里必须把这个匿名函数放在小括号内部,不然浏览器会报错的。

原因呢,也是JS中的常识之一,那就是function A(){}这样的定义函数的方法,会在浏览器进行预编译的时候进行解析,而var A = function(){}这样的定义函数的方法,则是当JS解析到该行代码时,才会被解析。

这里呢,如果在上面的自执行函数中,不添加第一个小括号,浏览器就会在预编译时,对该部分进行解析,但是这个时候,因为没有对这部分function进行命名,浏览器在预编译时就会报错,而导致无法进行下去了。

使用下面这段函数,就可以证明,是在预编译的时候,报错的而导致无法执行的

  • alert("123");
  • function(){
  • alert("page onload success!");
  • }();

当然啦,加括号本就不是必须的,比如我们使用表达式定义函数时,var A = function(){}这种写法,就不是在预编译的时候进行的,所以,如果我们的自执行函数会把返回值定义到另外一个变量,是可以省略掉小括号的。

比如:

  • alert("123");
  • var a = function(){
  • alert("page onload success!");
  • }();

这样写也会连续有两个alert执行,完成我们之前说的功能,也不会报错,只是这时,自执行函数是没有返回值的,所以最后的a变量,是undefined。不过呢,为了统一起见,也为了看着方便,所以还是对各种写法的自执行函数的写法,都添加上小括号吧。

至于为什么,添加了小括号()(),这样写,就可以,那就是因为,这样的写法就变成一个表达式了。。。。

可以这么证明一下:

  • (function A(){
  • alert("page onload success!");
  • });
  • A();

只是这样的写法,和表达式定义函数就类似了,而且还会有一个问题就是,A函数,只有在这个括号内部使用。在外部使用,需要先把这个表达式进行赋值才行,如果赋值,那不就是成了使用赋值表达式定义函数了。

说的远了点,回来继续:到这里也大概了解了自执行函数的执行方法了吧。那使用自执行函数的方法,进行事件的绑定,大概也能猜到它的原理了吧。obj.onclick = callback();。如果我把callback函数的返回值,定义成一个函数,那当click事件触发时,不就是触发了这个返回的函数了。

所以呢,我们可以这样写:

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

没有什么问题吧?应该很容易理解到吧。

可是这样的写法呢,添加了一个函数变量,如果不添加呢。。。OK的,把后面的函数直接替换过去就行了。。。。

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

这样看起来,对比之前的写法,应该就能很明显的了解到,为什么这么写,能得到我们想要的结果了吧。

OK,这也是闭包的最简单的应用了,其他的闭包写法也有,只是就原理方面来说,和上面这种是相同的原理,所以这里就不一一列举了,用到闭包的地方其实很多(比如惰性载入函数,单例模式中的对象定义等),如果您能理解到这最简单闭包的原理,那么其他用到闭包的地方,见到了,也就能理解了。或者说,想要使用的时候,也就能想到应该怎么用了吧。

之前的文章中,也有一篇文章中的代码,主要就是使用的闭包的思想,可以参考:jQuery源码学习(二)–proxy

备注

计时器在一些动态页面,做一些动画效果时,是不可或缺的一个元素,它和alert方法相同,都是属于window对象的方法。使用计时器时,是有少许差别的,这里就以setTimeout为例简单说明:

看例子:代码中中的两个setTimeout执行后的结果分别是什么?

  • var a = 1;
  • function B(){
  • var a = 2;
  • setTimeout("C()",1000);
  • setTimeout(C,2000);
  • function C(){
  • alert("a="+a);
  • }
  • }
  • function C(){
  • alert("a="+a);
  • }
  • B();

测试一下也就知道了,分别为12,因为setTimeout是把后面执行的方法,第一种写法,只会查找全局变量中,是否有A函数,而第二种写法,会优先查找当前作用域中是否有A函数,如果局部没有的话,则顺序查找到全局作用域中。

有一种情况,是说,计时器内部调用的函数的this指向,是指向window的,这里可以说有错,也可以说没错,看一个例子:假设给id=test的一个元素绑定一个click事件。查看其中的this的值。

  • document.getElementById("test").onclick = function(){
  • alert(this);            //指向触发该事件的元素对象
  • setTimeout("A()",1000); //这里调用指向window
  • }
  • function A(){
  • alert(this);
  • }

这里就不考虑在IE8-的浏览器了。

按照最初写的两个计时器的例子,在写出如下的代码:

  • document.getElementById("test").onclick = function() {
  • alert(this);             //指向触发该事件的元素对象
  • setTimeout(A,1000);      ////这里依然指向window
  • function A(){
  • alert(this);
  • }
  • };
  • function A(){
  • alert(this);
  • }

为什么?不是按理说,这里应该是调用的内部的A方法吗?为什么this却是指向的window

有一个不确定的想法是:当调用了计时器时,会把当前作用域中的方法,内部的this指向window对象了。而且仅仅是修改了方法内部的this指向,如果有私有变量的取值,依然按照原函数所在的位置,根据作用域,进行取值。

可以这么证明一下:

  • var a = 1;
  • document.getElementById("test").onclick = function() {
  • alert(this);
  • var a = 123;
  • setTimeout(A,1000);
  • function A(){
  • alert("a="+a);
  • alert(this);
  • }
  • }
  • function A(){
  • alert("a="+a);
  • alert(this);
  • }

this的指向是和上面一个实例相同的,而alert中的a变量的取值,却是优先获取局部作用域中的值。

当然啦,这里如果把计时器中的调用方法,更换一下,那结果就不相同了哦。

  • var a = 1;
  • document.getElementById("test").onclick = function() {
  • alert(this);
  • var a = 123;
  • setTimeout("A()",1000);
  • function A(){
  • alert("a="+a);
  • alert(this);
  • }
  • }
  • function A(){
  • alert("a="+a);
  • alert(this);
  • }

这里,有兴趣的可以试试吧,说到这里,也发现,虽然使用计时器会强制把调用函数的内部的this指向改变成指向window的,但是对于作用域链的影响却只有写法不同带来的影响。即:setTimeout("A()",1000);setTimeout(A,1000);的不同。当然对于第二种写法,我们可以使用callapply强行改变A内部this的指向,不过这些跟本文的内容,貌似没有什么关系,就不多说了。

其实,按照我本来的想法,这里该写一下计时器(setTimeout,setInterval)和call,apply这几个和作用域链的关系,但是写到这里,又感觉他们的并没有什么关系,所以关于作用域链,就到这里。

OK了,如果您有什么新的想法,或者认识,或者发现文中的错误,请指教,非常感谢!

浅析作用域链–JS基础核心之一的更多相关文章

  1. 作用域链–JS基础核心之一

    JS中的作用域,大家都知道的,分为全局作用域和局部作用域,没有块级作用域,听起来其实很简单的,可是作用域是否能够有深入的了解,对于JS代码逻辑的编写成功率,BUG的解决能力,以及是否能写出更优秀的代码 ...

  2. 一步步学习javascript基础篇(2):作用域和作用域链

    作用域和作用域链 js的语法用法非常的灵活,且稍不注意就踩坑.这集来分析下作用域和作用域链.我们且从几道题目入手,您可以试着在心里猜想着答案. 问题一. if (true) { var str = & ...

  3. js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?

    日常在群里讨论一些概念性的问题,比如变量提升,作用域和闭包相关问题的时候,经常会听一些大佬们给别人解释的时候说执行上下文,调用上下文巴拉巴拉,总有点似懂非懂,不明觉厉的感觉.今天,就对这两个概念梳理一 ...

  4. js基础梳理-如何理解作用域和作用域链?

    本文重点是要梳理执行上下文的生命周期中的建立作用域链,在此之前,先回顾下关于作用域的一些知识. 1.什么是作用域(scope)? 在<JavaScritp高级程序设计>中并没有找到确切的关 ...

  5. JS基础:闭包和作用域链

    简介 一个定义在函数内部的函数与包含它的外部函数构成了闭包,内部函数可以访问外部函数的变量,这些变量将一直保存在内存中,直到无法再引用这个内部函数. 例如: var a = 0; function o ...

  6. 浅析 JS 中的作用域链

    作用域链的形成 在 JS 中每个函数都有自己的执行环境,而每个执行环境都有一个与之对应的变量对象.例如: var a = 2 function fn () { var a = 1 console.lo ...

  7. (O)js核心:作用域链

    作用域 在一个函数被调用的时候,函数的作用域才会存在.此时,在函数还没有开始执行的时候,开始创建函数的作用域:   函数作用域的创建步骤: 1.函数形参的声明. 2.函数变量的声明. 3.普通变量的声 ...

  8. JavaScript学习笔记——JS中的变量复制、参数传递和作用域链

    今天在看书的过程中,又发现了自己目前对Javascript存在的一个知识模糊点:JS的作用域链,所以就通过查资料看书对作用域链相关的内容进行了学习.今天学习笔记主要有这样几个关键字:变量.参数传递.执 ...

  9. 谈JS中的作用域链与原型链(1)

    学习前端也有一段时间了,觉得自己可以与大家分享一些我当初遇到疑惑的东西,希望能给对此问题有疑惑的朋友带来一点帮助. 先来普及一下JS的概念(不要嫌我啰嗦,可能一些朋友开始学习JS是跟着视频和写好的代码 ...

随机推荐

  1. linux的定时任务crontab

    每隔一分钟执行以下语句: #打印当前时间: date "+%Y-%m-%d %T" 保存为/usr/test/test.sh 查看系统中当前用户有多少个定时任务: crontab ...

  2. Android:控件布局(绝对布局)AbsoluteLayout

    绝对布局也叫坐标布局,指定元素的绝对位置,因为适应性很差,一般很少用到.可以使用RelativeLayout替代. 常用属性: android:layout_x  --------组件x坐标 andr ...

  3. C#基础精华06(Linq To XML,读取xml文件,写入xml)

    1.XML概述: 可扩展标记语言XML(eXtensible Markup Language)是一种简单灵活的文本格式的可扩展标记语言,侧重于存储数据. 2.XML特点 xml 标记语言 html x ...

  4. Winform 数据验证

    http://blog.scosby.com/post/2010/02/11/Validation-in-Windows-Forms.aspx 总结:1. CancelEventArgs e ,调用e ...

  5. laravel Authentication and Security

    Creating the user modelFirst of all, we need to define the model that is going to be used to represe ...

  6. Compiler options do not specify -mv64+, but configuration is for C64x+

     2013-06-20 10:02:47 错误报告: "pin_connect_cfg.s62", ERROR!   at line 365: [ ***** USER ERROR ...

  7. 大四实习准备6_android服务

    2015-5-9 1.服务是什么 android四大组件之一,有一些特点: 1)服务的运行不依赖于用户界面,即使程序被切换到后台.或者用户打开了另外一个应用程序,服务仍然能够保持正常运行.(当对应的程 ...

  8. NOI2002 荒岛野人

    这题其实黑书上有,只是我脑残的没想起来…… 其实就是拓展欧几里得算法 我参看的题解:http://www.cnblogs.com/Rinyo/archive/2012/11/25/2788373.ht ...

  9. python扩展实现方法--python与c混和编程

    前言 需要扩展Python语言的理由: 创建Python扩展的步骤 1. 创建应用程序代码 2. 利用样板来包装代码 a. 包含python的头文件 b. 为每个模块的每一个函数增加一个型如PyObj ...

  10. 编写高效的C程序与C代码优化 via jobbole

    http://blog.jobbole.com/82582/ 原文出处: codeproject 译文出处:CodingWu的博客 欢迎分享原创到伯乐头条