javascript系列 ————词法作用域、作用域链(二)

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将 变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情 况下是这样的)。

                                                                                                   _____________《你不知道的javascript》

目录

作用域

块级作用域:即块级的作用范围

在js中词法作用域规则:

作用域链结构

绘制作用域链

变量的访问(搜索)规则

分析代码

补充

动态作用域

函数作用域


作用域

"词法作用域是作用域的一种工作模型",通俗的讲“没有作用域的概念就没有词法作用域的概念。”..

什么是作用域

表示的就是范围,即作用范围,就是一个名字在什么地方可以别使用,什么时候不能使用。

作用域就是一套规则,用于确定在何处以及如何查找变量(标识符)的规则,通俗的讲,作用域就是查找变量的地方。在某函数中找到该变量,就可以说在该函数作用域中找到了该变量;在全局中找到该变量,就可以说在全局作用域中找到了该变量!

先看一段及其简单的代码:

function foo(){
    var a=123;
    console.log(a);//result 123
}
foo()

在foo函数执行的时候,输出一个a变量,那么这个a变量是哪里来的嘞,有看到函数第一行有定义a变量的代码var a = '123' 

再看下面这段代码: 

var num=123;
function foo(){
    console.log(num);//result 123
}
foo()

同样的道理:在输出num的时候,在自己函数体内没有找到num,那么就会到函数外层的全局中查找,找到了就停止查找并输出了。

注意以上两段代码都有查找变量,第一段代码是在函数中找到a变量,第二段代码是在全局中找到b变量。函数作用域全局作用域,把这两个词换入到原来那句话中,第一段代码是在函数作用域中找到a变量,第二段代码是在全局作用域中找到num变量。通俗的讲,作用域就是查找变量的地方。在某函数中找到该变量,就可以说在该函数作用域中找到了该变量;在全局中找到该变量,就可以说在全局作用域中找到了该变量!

块级作用域:即块级的作用范围

JavaScript没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域。

而在C、Java、C#等编程语言中,下面的语法报错(伪代码)

{
    var numb = 123;
    {
        console.log( numb );    // num => 123
     }
}
console.log( numb );   //报错

不同于其他编程语言,在JavaScript里并没有块级作用域,也就是说在for、if、while等语句内部的声明的变量与在外部声明是一样的,在这些语句外部也可以访问和修改这些变量的值。实例如下:

function fun() {
      
      if(0 < 2) {
          var name = "jeri";
      }    
      console.log(name); // 输出:jeri
      name = "change";
      console.log(name); // 输出:change
  }
 
 fun();

在js 中采用词法作用域

所谓的词法(代码)作用域,就是代码在编写过程中体现出来的作用范围,代码一旦写好,不用执行,作用范围已经确定好了,这个就是所谓的词法作用域

在js中词法作用域规则:

  1. 函数允许访问函数外的数据,
  2. 这个代码结构中只有函数可以限定作用域。
  3. 作用规则首先使用提升规则分析
  4. 如果当前作用规则中有名字了,就不考虑外面的名字。

例子1:

var num=123;
function foo(){
    console.log(num);
}
foo()//result 123

例子2:

 if(false){
     var num=123;
}
console.log(num);//undefined

例子3:

var num=123;
function foo(){
    var num=456;
    function func(){
        console.log(num);//456
    }
    func();
}
foo();

作用域链结构

可以发现只函数才能制作作用域结构,那么只要是代码,至少有一个作用域,即全局作用域。

凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域,那么将这样的所有作用域列出来,可以有一个结构:函数内指向函数外的链式结构

例如:

function f1(){
   function f2(){
   }
}
var num=456;
function f3(){
   function f4(){
   }
}

作用域链结构与DOM树结构很相似。

绘制作用域链

步骤:

  1. 看整个全局是一条链,即顶级链,记为0级链
  2. 看全局作用域中有什么成员声明,就以方格的形式绘制到0级链上
  3. 再找函数,只有函数可以限制作用域,因此从函数中引出新链,标记为1级链
  4. 然后再每一个1级链中在此往复刚才的行为。

变量的访问(搜索)规则

  1. 首先看变量在第几条链上,在该链上看是否有变量的定义与赋值,如果有直接使用
  2. 如果没有就到上一级链上找(n-1级链),如果有直接使用,停止继续查找。
  3. 如果还没有在此往上找……直到全局链(0级),还没有就是is not defined 
  4. 注意,切记:同级链不可混合查找

绘制如下程序的作用域链

function f1(){
      var num=123;
      function f2(){
          console.log(num);
      }
      f2();
}
var num=456;
f1();//123

  1. 首先函数f1()和变量num=456,在0级链上
  2. 而f1下面又可以展开1级链,
  3. 1级链上有num=123 和 函数f2。
  4. 程序f1()调用进入左边1级链,而f1中又调用了f2函数,f2函数中console.log(num)可以看作在2级链,
  5. 此时,程序会向这一条链向上查找,首先2级链没有num声明,向上到达1级链,
  6. 刚好1级链上有num=123,所以就直接使用123,
  7. 程序最后的结果打印123,

分析代码

  1. 在分析代码的时候切记从代码运行角度上来分析,如果代码给变量赋值了,一定要标记到图中;
  2. 如果代码比较复杂,可以在图中表示代码的内容,有时候还要将原型图与作用域图结合起来分享。

分析代码如下:

var num=123;
function f1(){
    console.log(num);
}
function f2(){
    var num =456;
    f1();
}
f2();//123

作用域链图绘制:

作用链图预解析:

  1. 首先把num=123,函数f1,函数f2画在0级链上。
  2. f1中只有一句console.log(num),画出一条1级链,
  3. f2也画出1级链,连上有num=456和函数调用语句f1();
  4. 调用f2(),进入f2函数的作用域链,而在f2中又调用了f1函数,
  5. 程序进入f1的作用链,所以console.log(num)会在此链上查找是否存在num,
  6. 没有,继续向上一级查找,刚好在0级链上找到了num=123,
  7. 所以f1函数中的console.log(num)打印就是123.

补充

一个作用域中取。到哪个作用域中取呢?有人说过要到父作用域中取,其实有时候这种解释会产生歧义,如下面例子:

var x=10;
function fn(){
    console.log(x);//10而不是20
}
function show(f){
    var x=20;
    (function(){
        f();
    })()
}
show(fn);

换句话说,更为贴切:‘要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”。’其实这就是所谓的“静态作用域”。

在fn函数中,取自由变量x的值时,要到哪个作用域中取?——要到创建fn函数的那个作用域中取——无论fn函数将在哪里调用。上面描述的只是跨一步作用域去寻找。如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了(undefined)。

动态作用域

动态作用域与词法作用域相对而言的,不同于词法作用域在定义时确定,动态作用域在执行时确定,其生存周期到代码片段执行为止。动态变量存在于动态作用域中,任何给定的绑定的值,在确定调用其函数之前,都是不可知的。

在代码执行时,对应的作用域链常常是保持静态的。然而当遇到with语句、call方法、apply方法和try-catch中的catch时,会改变作用域链的。以with为例,在遇到with语句时,会将传入的对象属性作为局部变量来显示,使其便于访问,也就是说把一个新的对象添加到了作用域链的顶端,这样必然影响对局部标志符的解析。当with语句执行完毕后,会把作用域链恢复到原始状态。实例如下:

var name = "global";
  
  // 使用with之前
  console.log(name); // 输出:global
  
  with({name:"jeri"}){
      console.log(name); // 输出:jeri
  }
  
 // 使用with之后,作用域链恢复
 console.log(name); // 输出:global

在作用域链中有动态作用域时,this引用也会变得更加复杂,不再指向第一次创建时的上下文,而是由调用者确定。比如在使用apply或call方法时,传入它们的第一个参数就是被引用的对象。实例如下:


 function globalThis() {
     console.log(this);
 }
 
 globalThis(); // 输出:Window {document: document,external: Object…}
 globalThis.call({name:"jeri"}); // 输出:Object {name: "jeri"}
 globalThis.apply({name:"jeri"},[]); // 输出:Object {name: "jeri"}

因为this引用是动态作用域,所以在编程过程中一定要注意this引用的变化,及时跟踪this的变动。

函数作用域

 函数作用域,顾名思义就是在定义函数时候产生的作用域,这个作用域也可以称为局部作用域。和全局作用域相反,函数作用域一般只在函数的代码片段内可访问到,外部不能进行变量访问。在函数内部定义的变量存在于函数作用域中,其生命周期随着函数的执行结束而结束。实例如下:

 var name = "global";
  
  function fun() {
      var name = "jeri";
      console.log(name); // 输出:jeri
  
      with ({name:"with"}) {
          console.log(name); // 输出:with
      }
     console.log(name); // 输出:jeri
 }
 
 fun();
 
 // 不能访问函数作用域
 console.log(name); // 输出:global

 

 

参考


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值