JavaScript:作用域链、作用域

·  阅读 233

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

  本文会涉及上下文、变量对象等内容,有不清楚的同学可以先看上篇文章

作用域链

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

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

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

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

// 实例一
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属性中,可以直接用一个数组来表示作用域链,数组的第一项为作用域链最前端,最前端是该函数的变量对象,数组的最后一项为作用域链最末端,最末端为全局变量对象。

  实例一中一共会常见三个执行上下文,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博客,一起学习交流。

分类:
前端
标签:
分类:
前端
标签: