作用域和作用域链

作用域

在JavaScript中的作用域有全局作用域局部作用域(在JavaScript中局部作用域即是函数作用域)以及块级作用域

  • 全局作用域:在最外层函数定义的变量即拥有全局作用域,对于任意函数来说,都可以访问到。例如:

    {
      var a = 1;
      var fn = function(){
        console.log(a);//1
      }
      fn();
    }
  • 局部作用域:和全局作用域相反,局部作用域的变量即是在特定代码块中才能过访问,对于外部是不能够访问的。注意:在函数内部定义变量的时候,如果不用var,那么你声明的就是全局变量了。

    {
      var a = 1;
      var fn = function(){
        var b = 2;
        console.log(a)//1
      }
      fn();
      console.log(b);//b is not defined
    }
  • 块级作用域:在代码块中使用let定义的变量,只能在当前代码块中进行访问。块级作用域可以形成暂时性死区。

    var fn = function(){
      for(let i = 0; i < 10; i++){
        console.log(i);//1-9
      }
      console.log(i);//undefined
    }
    fn()

作用域链

个人理解,作用域就是在函数内部可以访问外部变量的机制,使用链式查找哪些变量可以被函数内部访问。说起作用域链,那么不得不说执行环境了。

执行环境(Execution Context)

EC是JavaScript中一个最为重要的概念。EC定义了变量和函数有权访问的其他数据。JavaScript中,函数在运行时都会产生一个执行环境,并且JS引擎还会产生一个与当前EC相关联的变量对象(Variable Object,即VO)。EC中所有定义的变量和方法都包含在VO中。全局执行环境是最外围的执行环境,它是一个“兜底”的执行环境。

JS引擎在进入一段可执行的代码时,需要完成以下三个初始化工作:

首先,创建一个全局对象(Global Object,即GO),将Math、String、Data等常用的js对象作为其属性,但是这个GO在全局是不可见的,不可直接访问的。因此它还有另外一个属性window,并将window指向了自身,这样就可以在全局通过访问window,来访问GO的属性和方法了。

var globalObject = {
  Math,
  String,
  Data,
  Function,
  ...
  window:this
}

其次,JS引擎会创建一个执行环境栈(Execution Context Stack 即ECS),与此同时还会创建一个全局环境EC。当JS的执行流执行到一个函数时,JS引擎就会把该函数的EC推到ECS中,当函数执行完之后,再把EC从ECS中弹出,将执行流的控制权交还给上一层的EC。ECMAScript的执行流就是由这种机制控制着。

var ecStack = [];
//执行到函数fn
ecStock.push(EC);
//执行完fn
ecStack.pop(EC);

最后,JS引擎会创造一个和EC相关连的变量对象VO。如果这个环境是一个函数,则将其活动对象(Action Object,即AO)作为其变量对象。初始时AO只包含一个变量,即arguments。作用域链的下一个变量对象来自于外部包含环境,而下一个变量对象来自下一个包含环境,这样一直延伸到全局环境。全局变量对象(GO)始终都是作用域链的最后一个对象。

每一个函数在定义的时候,都会创建一个与之关联的[[scopes]]属性,该scope总是指向定义函数时的执行环境EC。举个🌰:

var fn = function(){};
console.dir(fn);

scopes

如上图所示,函数fn的[[scopes]][0]即是它的执行环境,GO。

再看:

var a = 1;
var fn = function(){
  var b = a + 1;
  return function(){
    return a + b + 1;
  }
}
var t = fn();
console.dir(t)

scopes

fn中始终都没有定义变量a,那么JS引擎就会沿着Scope Chain一直向上寻找a,最终在GO中找到了a。

作用域链

当JavaScript的代码块在运行时,就会创建与之相关的作用域链。

作用域链的作用就是保证当前环境对其有权访问的变量和方法进行有序的访问 ——JavaScript高级程序设计

作用域链的前端(也就是开头)就是当前执行环境EC的变量对象,它的变量对象来自于它的外部包含环境,再下一个变量对象同时也来自再下一个外部包含环境,这样一直延伸到全局执行环境,同时,GO也是作用域链的最后一个对象。

标识符解析

当在某个环境中为了读取或写入从而引入一个标识符时,必须通过搜索来确定该标识符代表了什么。搜索过程从当期作用域链的前端开始,向上逐级搜索,如果在局部环境中查找到了该标识符的定义,则停止搜索‘否则将一直沿着作用域链向上查找,直到找到GO上。如果找不到,则会报错。

延长作用域

虽然作用域只有两种:全局和局部(函数),但是还是有其他办法可以用来延长作用域链。其主要思路就是:有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象在代码执行后就被销毁。

1、try-catch语句的catch块

2、with语句

闭包

什么是闭包?个人理解就是由于函数的嵌套,并且对外提供访问接口就会产生闭包。

var fn = function(){
  var b = 2;
  return function(){
    return b
  }
}
var t = fn();
console.log(t());//2

在上面的代码中,正常情况下,函数fn执行完之后,应该JS的垃圾回收机制被标记“等待清除”,等待下一次垃圾回收机制执行的时候被清除,但是由于返回函数中引用了fn中的变量b,所以函数fn并不会被清除,而是一直保存着内存中,直到没有任何引用,才会被清除。

从上面的🌰我们可以得出,闭包有什么作用呢?

  • 闭包可以使得程序在函数外部可以访问到函数内部的变量
  • 闭包可以在内存中维持一个变量
var fn = function(){
  var array = new Array();
  for(var i = 0; i < 10; i++){
    array[i] = function(){
      return i
    }
  }
    return array
}
var arr = fn();
console.log(arr[0]());//10
console.log(arr[1]());//10

正常情况下,我们期待的结果就是返回0和1,但是结果出人意料。为什么呢?

其实我们可以先看下arr[0]执行时,它的scope chain:

scopes

我们可以清楚的看到,此时arr[0]中并没有i的定义,它会沿着作用域链向上找i,那么就会找到fn中的i,但是此时,i已经变成10了。

从此我们也可以得出一个结论:

JS的函数中的变量的值不是在编译的时候确定的,而是在运行时再去寻找的。

如果我们想让上面的🌰按照我们预期的执行,那么考虑使用立即执行函数。

var fn = function(){
  var array = new Array();
  for(var i = 0; i < 10; i++){
    array[i] = (function(num){//函数参数的传递是按值传递,会创建i的副本,而不是直接使用变量i
      return num
    })(i)
  }
  return array
}
var arr = fn();
console.log(arr[0])
console.log(arr[1])
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!