JavaScript:做用域链、做用域

  本文主要涵盖了做用域链、做用域等内容。javascript

  本文会涉及上下文、变量对象等内容,有不清楚的同窗能够先看上篇文章前端

做用域链

  函数对象和其它对象同样,拥有能够经过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被建立的做用域中对象的集合,这个集合被称为函数的做用域链,它决定了哪些数据能被函数访问。java

  上面是一段很官方的话,毕竟是官方写的。固然官方说的很对,但有点难理解,下面我就依据上述的内容作补充。git

  简单的说做用域链就是是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。github

  再简单点说就是内部上下文全部变量对象的列表。啥意思???看下面的实例一:编程

// 实例一
function foo() {
  var a = 1;
  function bar() {
    var b = 2;
    console.log(b);
  }
  bar();
}
foo();
复制代码

  上面的实例一,全局代码,foo函数、bar函数的执行上下文前后建立,每个执行上下文都包含变量对象this以及做用域链。其中bar函数的执行上下文:数组

barEC = {
  VO: {xxx}, // 变量对象
  this: xxx,
  scopeChain: [barContext.VO, fooContext.VO, globalContext.VO] // 做用域链
}
复制代码

  函数的做用链一般维护在该函数的执行上下文中scopeChain属性中,能够直接用一个数组来表示做用域链,数组的第一项为做用域链最前端,最前端是该函数的变量对象,数组的最后一项为做用域链最末端,最末端为全局变量对象。bash

  实例一中一共会常见三个执行上下文,bar函数的执行上下文位于执行上下栈的最顶端,因此其执行上下文的做用域链包括当前执行上下文的变量对象以及其商城环境的一系列的变量对象VO(foo)VO(global),因此bar函数的执行上下文的做用域链中有三个变量对象。闭包

  做用域链是一个链表,是一个线性表,也就是说当前做用域和上层做用域并非包含关系,是一个有方向的链式关系,而且是单向的,最前端是起点,最末端是终点。因此咱们能够沿着这个单向的链表查询变量对象中的标识符,这样也就能够访问到上一层做用域的变量。同时也保证了当前执行环境对符合访问权限的变量和函数的有序访问。接下来说讲[[scope]]属性。编程语言

[[Scope]]

  函数中有一个内部属性[[scope]],当函数建立的时候,就会保存全部的父变量对象到其中,也就能够理解[[scope]]就是全部父辈变量对象的层级链,可是[[scope]]并不表示完整的做用域链。看下面实例二:

// 实例二
function foo() {
  var a = 1;
  function bar() {
    var b = 2;
    console.log(b);
  }
  bar();
}
foo();
复制代码

  函数建立时,foo函数和bar函数的[[scope]]

foo.[[scope]] = [
  globalContext.VO
]
bar.[[scope]] = [
  fooContext.VO, globalContext.VO
]
复制代码

  从上面的实例二看到,各自函数的[[scope]]只包含了各自全部父辈变量对象,没有把本身的变量对象存入,此时本身的变量对象还没建立,因此[[scope]]并不表示完整的做用域链。

  当函数被激活时,进入函数执行上下文,也就建立了变量对象和做用域链,并会将本身的变量对象添加到做用域链的最前端。

barContext = {
  VO: {xxx}, // 变量对象
  this: xxx,
  scopeChain:  [barContext.VO].concat(bar.[[scope]])
}
// [barContext.VO].concat(bar.[[scope]]) => [barContext.VO, fooContext.VO, globalContext.VO]
复制代码

  上面就是做用域链建立的整个过程,回过头在看看刚开始的那个官方对做用域链的定义应该就很好理解了。

做用域

  做用域,收集并维护一张全部被声明的标识符(变量)的列表,并对当前执行中的代码如何访问这些变量强制实施了一组严格规则。简单的说就是经过标识符名称查询变量的一组规则,明肯定义了如何在某些位置存储变量,以及如何在稍后找到这些变量。

  做用域决定了代码区块中的变量和其余资源的可见性。做用域能够理解为是一个独立的地盘,不会让变量外泄、暴露出去。也就是说做用域最大的做用就是隔离变量,不一样做用域下同名变量不会有冲突。

词法做用域

  在编程语言中,做用域分为两种:一种是词法做用域,另外一种是动态做用域。JavaScript中做用域是词法做用域,词法做用域也叫作静态做用域,也就是在词法分析的阶段就被定义了,简单的说就是在代码书写的时候就被定义了。而动态做用域是值在代码被执行的时候才决定。看下面的实例三:

// 实例三
var a = 1;
function foo() {
  console.log(a); // 结果是啥??? => 1
}
function bar() {
  var a = 2;
  foo();
}
bar();
复制代码

  JavaScript采用的是静态做用域,输出的1。执行foo函数,会先从foo函数内部查找是否有局部变量a,若是有,则当前局部变量a的值;若是没有,就根据书写的位置,查找上层的代码,也就是到了全局做用域,也就是a等于1,因此会输出1。

  若是JavaScript采用的是动态做用域,,输出的会2。执行foo函数,依然会从foo函数内部查找是否有局部变量a,若是没有,就从调用函数的做用域,也就是bar函数内部查找a变量,因此会输出2.

  JavaScript采用的是静态做用域,输出的1。

全局做用域

  在全部函数声明或者大括号以外定义的变量,都在全局做用域里。

// 默认全局做用域
var str = 'Hello world';
复制代码

  在全局做用域内的变量能够在任何其余做用域内访问和修改。

var str = 'Hello world';
function foo() {
  console.log(str); // Hello world 'str'能够在foo函数内访问
  str = 'Hello javascript';
  console.log(str); // Hello javascript 'str'能够在foo函数内访问和修改
}
console.log(str); // Hello world
foo();
console.log(str); // Hello javascript
复制代码

  全部未定义直接赋值的变量自动声明为拥有全局做用域。

function foo() {
  str = 'Hello world';
  var name = 'Hello javaScript';
}
foo();
console.log(str); // Hello world
console.log(name); // 'ReferenceError: name is not defined'
复制代码

  尽可能能够在全局做用域定义变量,可是不推荐这样作,由于可能会引发命名冲突,两个或者多个变量使用相同的变量名。

  若是定义变量时使用了const或者let,那么在命名冲突时,会报错,使用const或者let不容许变量重复声明。

let str = 'Hello world';
let str = 'Hello javaScript'; // 'Error, thing has already been declared'
复制代码

  若是定义变量使用的时var,是容许重复声明的,第二次定义会覆盖第一次定义。这样会让代码的调试变得很难,是不可取的。

var str = 'Hello world';
var str = 'Hello javaScript';
console.log(str); // Hello javascript
复制代码

  因此,尽可能不要使用全局变量,使用局部变量。

局部做用域

  局部做用域是相对全局做用域而言。在代码某一个具体范围内使用使用的变量均可以在局部做用域内定义。

  JavaScript中有两种局部做用域:函数做用域和块级做用域。

函数做用域

  函数做用域是指,属于这个函数的所有变量均可以在整个函数的范围内使用以及复用。在函数以外,没法访问到。

function foo() {
  var str = 'Hello world';
  console.log(str); // Hello world
}
foo();
console.log(str); // 'ReferenceError: str is not defined'
复制代码

  函数内定义的变量在函数做用域中,并且这个函数被调用时都具备不一样的做用域。这也就意味着具备相同名称的变量能够在不一样的函数中使用。这是由于这些变量被绑定到它们各自具备不一样做用域的相应函数,而且在其余函数中不可访问。

function foo() {
  var str = 'Hello world';
  function bar() {
    var str = 'Hello javaScript';
    console.log(str); // Hello javaScript
  }
  bar();
  console.log(str); // Hello world
}
foo();
复制代码

  做用域是分层的,内层做用域能够访问外层做用域的变量,反之不行。

块级做用域

  块级做用域是对块语句而言的。

  块语句就是大括号{}中间的语句,如ifswitch条件语句或forwhile循环语句,在ES6以前,块语句不会一个新的做用域。在块语句中定义的变量将保留在它们已经存在的做用域中。

if(true) {
  // if条件语句块不会建立新的做用域
  var str = 'Hello world'; // 在全局做用域中
}
console.log(str); // Hello world
复制代码

  ES6后,能够经过letconst声明变量,会产生块级做用域,所声明的变量在指定块的做用域外没法被访问。

if(true) {
  // if条件语句块会建立新的做用域
  let str = 'Hello world';
}
console.log(str); // 'str is not defined'
复制代码

做用域与执行上下文

  做用域和执行上下文这两个概念比较容易混淆,容易误认为是相同的概念,事实并非。

  JavaScript的执行分为两阶段,一是语法检查,二是执行:

  • 语法检查:
    • 词法分析
    • 语法分析
    • 做用域规则肯定
  • 执行:
    • 建立执行上下文
    • 执行函数代码
    • 垃圾回收

  从上面就能看出,做用域和执行上下文并不同,做用域在函数定义的时候就已经肯定了,不是在函数调用的时候肯定的,而执行上下文是在函数执行前建立的。

  做用域其实就是一张全部被声明的标识符(变量)的列表,里面没有值,就是定义了如何在某些位置存储变量,以及如何在稍后找到这些变量,而后代码执行的时候能够赋值给变量。要经过做用域相对应的执行上下文来获取变量的值。

  同一做用域下,不一样的调用会产生不一样的执行上下文,继而产生不一样的变量的值。因此,做用域变量中的值是在执行的过程当中肯定的,而做用域是在函数建立时就肯定了。

  若是要查找一个做用域下某个变量的值,就须要找到这个做用域对应的执行上下文,再在其中寻找变量的值。

  做用域与执行上下文的最大区别就是:

  执行上下文是在运行时肯定的,随时可能会变;做用域是在定义时就肯定了,而且不会改变。

  一个做用域下可能包含若干个上下文环境。有可能历来没有过上下文环境(函数历来就没有被调用过);有可能有过,如今函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。

结语

  文章若有不正确的地方欢迎各位大佬指正,也但愿有幸看到文章的同窗也有收获,一块儿成长!

——————————————本文首发于我的公众号——————————————

最后,欢迎你们关注个人公众号或者 github博客,一块儿学习交流。
相关文章
相关标签/搜索