JavaScript:做用域与做用域链

1.什么是做用域(scope)?

简单来说,做用域(scope)就是变量访问规则的有效范围javascript

  • 做用域外,没法引用做用域内的变量;
  • 离开做用域后,做用域的变量的内存空间会被清除,好比执行完函数或者关闭浏览器
  • 做用域与执行上下文是彻底不一样的两个概念。我曾经也混淆过他们,可是必定要仔细区分。

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段做用域规则会肯定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段建立。java

函数做用域是在函数声明的时候就已经肯定了,而函数执行上下文是在函数调用时建立的。假如一个函数被调用屡次,那么它就会建立多个函数执行上下文,可是函数做用域显然不会跟着函数被调用的次数而发生什么变化。jquery

1.1 全局做用域

var foo = 'foo';
console.log(window.foo);   // => 'foo' 

在浏览器环境中声明变量,该变量会默认成为window对象下的属性。浏览器

function foo() {
    name = "bar"
}
foo();
console.log(window.name) // bar

在函数中,若是不加 var 声明一个变量,那么这个变量会默认被声明为全局变量,若是是严格模式,则会报错。闭包

全局变量会形成命名污染,若是在多处对同一个全局变量进行操做,那么久会覆盖全局变量的定义。同时全局变量数量过多,很是不方便管理。函数

这也是为何jquery要在全局创建变量 ,其他私有方法属性挂在 下的缘由。post

1.2 函数做用域

假如在函数中定义一个局部变量,那么该变量只能够在该函数做用域中被访问。atom

function doSomething () {
    var thing = '吃早餐';
}
console.log(thing); // Uncaught ReferenceError: thing is not defined

嵌套函数做用域:spa

function outer () {
    var thing = '吃早餐';
    function inner () {
        console.log(thing);
    }
    inner();
}

outer();  // 吃早餐

 

在外层函数中,嵌套一个内层函数,那么这个内层函数能够向上访问到外层函数中的变量。翻译

既然内层函数能够访问到外层函数的变量,那若是把内层函数return出来会怎样?

function outer () {
    var thing = '吃早餐';
    
    function inner () {
        console.log(thing);
    }
    
    return inner;
}

var foo = outer();
foo();  // 吃早餐

函数执行完后,函数做用域的变量就会被垃圾回收。而这段代码看出当返回了一个访问了外部函数变量的内部函数,最后外部函数的变量得以保存。

这种当变量存在的函数已经执行结束,但扔能够再次被访问到的方式就是“闭包”。后期会继续对闭包进行梳理。

1.3 块级做用域

不少书上都有一句话,javascript没有块级做用域的概念。所谓块级做用域,就是{}包裹的区域。可是在ES6出来之后,这句话并不那么正确了。由于能够用 let 或者 const 声明一个块级做用域的变量或常量。

好比:

for (let i = 0; i < 10; i++) {
    // ...
}
console.log(i); // Uncaught ReferenceError: i is not defined

发现这个例子就会和函数做用域中的第一个例子同样的错误提示。由于变量i只能够在 for循环的{ }块级做用域中被访问了。

扩散思考:

究竟何时该用let?何时该用const?

默认使用 const,只有当确实须要改变变量的值的时候才使用let。由于大部分的变量的值在初始化以后不该再改变,而预料以外的变量的修改是不少bug的源头。

1.4 词法做用域

词法做用域,也能够叫作静态做用域。意思是不管函数在哪里调用,词法做用域都只在由函数被声明时所处的位置决定。
既然有静态做用域,那么也有动态做用域。
而动态做用域的做用域则是由函数被调用时执行的位置所决定。

var a = 123;
function fn1 () {
    console.log(a);
}
function fn2 () {
    var a = 456;
    fn1();
}
fn2();   // 123

以上代码,最后输出结果 a 的值,来自于 fn1 声明时所在位置访问到的 a 值 123。
因此JS的做用域是静态做用域,也叫词法做用域。

上面的1.1-1.3能够看作做用域的类型。而这一小节,其实跟上面三小节仍是有差异的,并不属于做用域的类型,只是关于做用域的一个补充说明吧。

2. 什么是做用域链(scope chain)

在JS引擎中,经过标识符查找标识符的值,会从当前做用域向上查找,直到做用域找到第一个匹配的标识符位置。就是JS的做用域链。

var a = 1;
function fn1 () {
    var a = 2;
    function fn2 () {
        var a = 3;
        console.log(a);
    }
    fn2 ();
}
fn1(); // 3

console.log(a) 语句中,JS在查找 a变量标识符的值的时候,会从 fn2 内部向外部函数查找变量声明,它发现fn2内部就已经有了a变量,那么它就不会继续查找了。那么最终结果也就会打印3了。

代码分析以下:

<script type="text/javascript">
    var a = 100;
    function fun(){
        var b = 200
        function fun2(){
            var c = 300
        }
        function fun3(){
            var d = 400
        }
        fun2()
        fun3()
    }
    fun()
</script>

首先预编译,一开始生成一个GO{

  a:underfined

  fun:function fun(){//fun的函数体

      var b = 200
      function fun2(){
        var c = 300
      }
      function fun3(){
      var d = 400
      }
      fun2()
      fun3()
    }

}

逐行执行代码,GO{

  a:100

  fun:function fun(){//fun的函数体

      var b = 200
      function fun2(){
        var c = 300
      }
      function fun3(){
      var d = 400
      }
      fun2()
      fun3()
    }

}

当fun函数执行时,首先预编译会产生一个AO{

  b:underfined

  fun2:function fun2(){
       var c = 300
     }

  fun3:function fun3(){
      var d = 400
     }

}

这里注意的是fun函数是在全局的环境下产生的,因此本身身上挂载这一个GO,因为做用域链是栈式结构,先产生的先进去,最后出来,

在这个例子的状况下,AO是后于GO产生的,因此对于fun函数自己来讲,执行代码的时候,会先去本身自己的AO里找找看,若是没有找到要用的东西,就去父级查找,此题的父级是GO

此刻fun的做用域链是  第0位    fun的AO{}

          第1位    GO{}

fun函数开始逐行执行AO{

  b:200

  fun2:function fun2(){
       var c = 300
     }

  fun3:function fun3(){
      var d = 400
     }

 }

注意:函数每次调用才会产生AO,每次产生的AO还都是不同的

而后遇到fun2函数的执行,预编译产生本身的AO{

  c:underfined

}

此刻fun2的做用域链是第0位    fun2的AO{}

          第1位    fun的AO{}

          第2位    GO{}

而后遇到fun3函数的执行,预编译产生本身的AO{

  d:underfined

}

此刻fun3的做用域链是第0位    fun3的AO{}

          第1位    fun的AO{}

          第2位    GO{}

fun2和fun3的做用域链没有什么联系。

当函数fun2和fun3执行完毕,本身将砍掉本身和本身的AO的联系,

最后就是fun函数执行完毕,它也是砍掉本身和本身AO的联系。

这就是一个咱们平时看到不是闭包的函数。

 

闭包

1.闭包在红宝书中的解释就是:有权访问另外一个函数做用域中的变量的函数。

2.写法:

 1 <script type="text/javascript">
 2     function fun1(){
 3         var a = 100;
 4         function fun2(){
 5             a++;
 6             console.log(a);
 7         }
 8         return fun2;
 9     }
10     
11     var fun = fun1();
12     fun()
13     fun()
14 </script>

3.效果以下:

4.分析:

执行代码

GO{

fun:underfined

fun1:function fun1()

   {

     var a = 100;

     function fun2()

    {

        a++;

        console.log(a);

     }

     return fun2;

     }

 

}

而后第十一行开始这里,就是fun1函数执行,而后把fun1的return返回值赋给fun,这里比较复杂,咱们分开来看,

这里fun1函数执行,产生AO{

a:100

fun2:function fun2(){

    a++;
    console.log(a);
    }

}

此刻fun1的做用域链为 第0位   AO

           第1位   GO

此刻fun2的做用域链为 第0位   fun1的AO

           第1位   GO

解释一下,fun2只是声明了,并无产生调用,因此没有产生本身的AO,

正常的,咱们到第7行代码咱们就结束了,可是这个时候来了一个return fun2,把fun2这个函数体抛给了全局变量fun,好了,fun1函数执行完毕,消除本身的AO,

此刻fun2的做用域链为 第0位   fun1的AO

           第1位   GO

第十二行就是fun执行,而后,它自己是没有a的,可是它能够用fun1的AO,而后加,而后打印,

由于fun中的fun1的AO原本是应该在fun1销毁时,去掉,可是被抛给fun,因此如今fun1的AO没办法销毁,因此如今a变量至关于一个只能被fun访问的全局变量。

因此第十三行再调用一次fun函数,a被打印的值为102。

相关文章
相关标签/搜索