博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JavaScript高级之词法作用域和作用域链
阅读量:6695 次
发布时间:2019-06-25

本文共 4595 字,大约阅读时间需要 15 分钟。

主要内容:

  1. 分析JavaScript的词法作用域的含义
  2. 解析变量的作用域链
  3. 变量名提升时什么

  

一、关于块级作用域

  说到JavaScript的变量作用域,与咱们平时使用的类C语言不同. 例如C#中下面代码:

1 static void Main(string[] args)2 {3     if(true)4     {5         int num = 10;6     }7     System.Console.WriteLine(num);8 }

 

这段代码如果进行编译,是无法通过的,因为"当前上下文中不存在名称num". 因为这里变量的作用域是由花括号限定的,称为块级作用域.

在块级作用域下,所有的变量都在定义的花括号内,从定义开始到花括号结束这个 范围内可以使用. 出了这个范围就无法访问. 也就是说代码

1 if(true)2 {3     int num = 10;4     System.Console.WriteLine(num);5 }

 

这里可以访问,因为变量的定义与使用在同一个花括号内.

但是在JavaScript中就不一样,JavaScript中没有块级作用域的概念.

 

二、JavaScript中的作用域

在JavaScript中,下面代码:

1 if(true) {2     var num = 10;3 }

alert(num); 运行的结果是弹窗10. 那么在JavaScript中变量的作用范围是怎么限定的呢?

2.1 函数限定变量作用域

  在JavaScript中,只有函数可以限定一个变量的作用范围. 什么意思呢? 就是说,在JavaScript中,在函数里面定义的变量,可以在函数里面被访问,但是在函数外

无法访问. 看如下代码:

1 var func = function() {2     var num = 10;3 };4 try {5     alert(num);6 } catch ( e ) {7     alert( e );8 }

这段代码运行时,会抛出一个异常,变量num没有定义. 也就是说,定义在函数中的变量无法在函数外使用,当然在函数内可以随意的使用, 即使在赋值之前. 看下面代码:

1 var func = function() { 2     alert(num); 3     var num = 10; 4     alert(num); 5 }; 6 try { 7     func(); 8 } catch ( e ) { 9     alert( e );10 }

这段代码运行后,不会抛出错误,弹窗两次,分别是 undefined 和 10(至于为什么,下文解释).

从这里可以看得出,变量只有在函数中可以被访问. 同理在该函数中的函数也可以访问.

 

2.2 子域访问父域

  前面说了,函数可以限定变量的作用域,那么在函数中的函数就成为该作用域的子域. 在子域 中的代码可以访问到父域中的变量. 看下面代码:

1 var func = function() {2     var num = 10;3     var sub_func = function() {4         alert(num);5     };6     sub_func();7 };8 func();

这段代码执行得到的结果就是 10. 可以看到上文所说的变量访问情况. 但是在子域中访问父域的代码也是有条件的. 如下面代码:

var func = function() {    var num = 10;    var sub_func = function() {        var num = 20;        alert(num);    };    sub_func();}; func();

这段代码比前面就多了一个"var num = 20;",这句代码在子域中,那么子域访问父域的情况就发生了变化,这段代码打印的结果是 20. 即此时子域访问的num是子域中的变量,而不是父域中的.

由此可见访问有一定规则可言. 在JavaScript中使用变量,JavaScript解释器首先在当前作 用域中搜索是否有该变量的定义,如果有,就是用这个变量;如果没有就到父域中寻找该变量. 以此类推,直到最顶级作用域,仍然没有找到就抛出异常"变量未定义". 看下面代码:

1 (function() {2     var num = 10;3     (function() {4         var num = 20;5         (function(){6             alert(num);7         })()8     })();9 })();

这段代码执行后打印出20. 如果将"var num = 20;"去掉,那么打印的就是10. 同样,如果再去掉"var num = 10",那么就会出现未定义的错误.

三、作用域链

  有了JavaScript的作用域的划分,那么可以将JavaScript的访问作用域连成一个链式树状结构. JavaScript的作用域链一旦能清晰的了解,那么对于JavaScript的变量与闭包就是非常清晰的了.

下面采用绘图的办法,绘制作用域链.

3.1 绘制规则:

1) 作用域链就是对象的数组

2) 全部script是0级链,每个对象占一个位置
3) 凡是看到函数延伸一个链出来,一级级展开
4) 访问首先看当前函数,如果没有定义往上一级链检查
5) 如此往复,直到0级链

3.2 举例

看下面代码:

1 var num = 10; 2 var func1 = function() { 3     var num = 20; 4     var func2 = function() { 5         var num = 30; 6         alert(num); 7     }; 8     func2(); 9 };10 var func2 = function() {11     var num = 20;12     var func3 = function() {13         alert(num);14     };15     func3();16 };17 func1();18 func2();

下面分析一下这段代码:

-> 首先整段代码是一个全局作用域,可以标记为0级作用域链,那么久有一个数组
  var link_0 = [ num, func1, func2 ]; // 这里用伪代码描述
-> 在这里func1和func2都是函数,因此引出两条1级作用域链,分别为
  var link_1 = { func1: [ num, func2 ] }; // 这里用伪代码描述
  var link_1 = { func2: [ num, func3 ] }; // 这里用伪代码描述
-> 第一条1级链衍生出2级链
  var link_2 = { func2: [ num ] }; // 这里用伪代码描述
-> 第二条1级链中没有定义变量,是一个空链,就表示为
  var link_2 = { func3: [ ] };
-> 将上面代码整合一下,就可以将作用域链表示为:

1 // 这里用伪代码描述 2 var link = [ // 0级链 3     num, 4     { func1 : [    // 第一条1级链 5         num, 6         { func2 : [    // 2级链 7             num 8         ] } 9     ]},10     { func2 : [    // 第二条1级链11         num,12         { func3 : [] }13     ]}14 ];

-> 用图像表示为

图:01_01作用域链.bmp 

注意:将链式的图用js代码表现出来,再有高亮显示的时候就非常清晰了.

   有了这个作用域链的图,那么就可以非常清晰的了解访问变量是如何进行的:

    在需要使用变量时,首先在当前的链上寻找变量,如果找到就直接使用,不会向上再找;如果没有找到,那么就向上一级作用域链寻找,直到0级作用域链.

  如果能非常清晰的确定变量所属的作用域链的级别,那么在分析JavaScript 代码与使用闭包等高级JavaScript特性的时候就会非常容易(至少我是这样哦).

三、变量名提升与函数名提升

有了作用域链与变量的访问规则,那么就有一个非常棘手的问题. 先看下面 的JavaScript代码:

1 var num = 10;2 var func = function() {3     alert(num);4     var num = 20;5     alert(num);6 };7 func();

 

执行结果会是什么呢?你可以想一想,我先不揭晓答案.

先来分析一下这段代码. 

  这段代码中有一条0级作用域链,里面有成员num和func. 在func下是1级作用 域链,里面有成员num. 因此在调用函数func的时候,就会检测到在当前作用域中
变量num是定义过的,所以就会使用这个变量. 但是此时num并没有赋值,因为代码是从上往下运行的. 因此第一次打印的是 undefined,而第二次打印的便是20. 
  你答对了么? 
  

  像这样将代码定义在后面,而在前面使用的情况在JavaScript中也是常见的 问题. 这时就好像变量在一开始就定义了一样,结果就如同下面代码:

1 var num = 10;2     var func = function() {3     var num;    // 感觉就是这里已经定义了,但是没有赋值一样4     alert(num);5     var num = 20;6     alert(num);7 };8 func();

 

那么这个现象常常称为变量名提升. 同样也有函数名提升这一说. 如下面代码:

1 var func = function() { 2     alert("调用外面的函数"); 3 }; 4 var foo = function() { 5     func(); 6  7     var func = function() { 8     alert("调用内部的函数"); 9     };10 11     func();12 };13 foo();

 

 好了,这段代码结果如何?或则应该有什么不一样,我先不说没留着读者思考吧! 下一篇再做解答.

  由于有了这些不同,因此在实际开发的时候,推荐将变量都写在开始的地方, 也就是在函数的开头将变量就定义好,类似于C语言的规定一样. 这个在js库中也

是这么完成的,如jQuery等.

转载于:https://www.cnblogs.com/raind/p/8551937.html

你可能感兴趣的文章
打造自己的 APP「冰与火百科」(一):分析定位
查看>>
智能合约语言 Solidity 教程系列5 - 数组介绍
查看>>
手把手0基础项目实战(一)——教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)...
查看>>
Win10安全特性之执行流保护
查看>>
【附源码】为了帮助程序员谈恋爱,我做了这个APP
查看>>
vue+elementUI开发实践问题总结
查看>>
SpringBoot2使用WebFlux函数式编程
查看>>
面试官问我:什么是JavaScript闭包,我该如何回答
查看>>
V8 中更快的异步函数和 promises
查看>>
Android开发必用工具及其进阶途径
查看>>
android自定义view(自定义数字键盘)
查看>>
在线代码编辑器 Codemirror 的轻量级 React 组件
查看>>
Yii1.1 CGridView 简单使用
查看>>
【刷算法】字符串的全排列
查看>>
进击的二维码 | ArcBlock 课堂预告
查看>>
自定义SeekBar 实时显示百分比进度
查看>>
JavaScript 专题之函数柯里化
查看>>
Java类集框架 —— LinkedHashMap源码分析
查看>>
iOS CAGradientLayer颜色渐变
查看>>
Android 路由实践(二)
查看>>