JavaScript 之作用域与作用域链

首发地址:https://github.com/jeuino/Blo...
概述

在上一篇《JavaScript 之执行上下文》中介绍了什么是执行上下文与执行上下文栈,本篇文章主要总结了:

  • 什么是作用域?
  • 什么是词法作用域和动态作用域?它们的区别是什么?
  • JavaScript 采用了什么类型的作用域?
  • JavaScript 中作用域的类型?
  • 执行上下文中的作用域链?
作用域 Scope

《 JavaScript 引擎(V8)是如何工作的》中说到过,作用域是由 V8 的 Parser 解析器确定的。那么什么是作用域呢,我们下面来聊一聊。

在软件设计中,有一个公共的原则——最小授权(暴露)原则。这个原则是指在软件设计中, 应该最小限度地暴露必要内容, 而将其他内容都“隐藏” 起来。

这样做的优点是:

  1. 可以降低多文件引入时,变量或函数命名出现冲突的概率;
  2. 如果将所有内容都暴露给全局环境,那么会占用很多无用内存,只有当关掉浏览器或当前窗口时,全局变量才会被回收;
  3. 如果程序出现错误,可以更小范围的确定出错区域;

在 JavaScript 中就是通过作用域来实现最小授权原则的。

作用域规定了变量和函数的可访问性。

作用域实施了一套严格的规则,用于规定在 JavaScript 运行时如何查找变量和函数,也就是确定当前执行代码对变量和函数的访问权限。

作用域共有两种主要的工作模型。 第一种是最为普遍的, 被大多数编程语言所采用的词法作用域。 另外一种叫作动态作用域

JavaScript 采用的是词法作用域,也称为静态作用域。

词法作用域

词法作用域是由你在写代码时变量和函数声明的位置来决定的。

词法作用域的“父子关系”,取决于代码书写时的嵌套关系。

我们来分析下面这段代码:

var a = 1;

// foo 函数声明在全局执行上下文中
function foo() {
    console.log(a);
}
// bar 函数声明在全局执行上下文中
function bar() {
    var a = 2;
    foo();
}

bar();

这里补充一个小知识,就是上图描述文字中出现的 RHS 是什么意思,这涉及到了 JavaScript 执行过程中引擎是如何查找变量的。

引擎在执行代码时,会通过查找标识符来判断它是否已经声明过。查找的过程由作用域进行协助,但是引擎是怎么查找的呢?引擎查找变量有两种方式,分别是:

  • LHS 查询

如果查找的目的是对变量进行赋值,则使用 LHS 查询(告诉作用域我需要对 a 变量进行 LHS 引用,你见过它嘛?)
不成功的 LHS 引用会导致自动隐式创建一个全局变量(非严格模式),严格模式下抛出ReferenceError 异常

  • RHS 查询

如果查找的目的是获取变量的值,则使用 RHS 查询(告诉作用域我需要对 a 变量进行 RHS 引用,你见过它嘛?)
不成功的 RHS 查询会抛出 ReferenceError 异常,不会隐式创建一个全局变量。

请看下面这个例子,其中 RHS 共使用了三次,LHS 共使用了两次,你能找到都是在哪里使用了 RHS 和 LHS 吗?

function add(a, b) {
    return a + b;
}
add(1, 2)

好了,我们再回到词法作用域上。看了上述分析,可能你还是不太明白什么是词法作用域,下面我们再来看下什么是动态作用域,通过与动态作用域进行对比,你应该会有一个更清晰的认知。

动态作用域

词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。

动态作用域不关心函数和变量是在何处声明的,只关心它们是从何处调用的;

动态作用域是基于调用栈的,而不是代码中的作用域嵌套。

我们从动态作用域的角度,再来分析上面那段代码:

var a = 1;

function foo() {
    console.log(a);
}

function bar() {
    var a = 2;
    // 在 bar 函数内调用 foo 
    foo();
}

bar(); // 1

如果 bar 函数中也没有找到 a,则会顺着调用栈到全局环境中查找,此时输出结果为 1。

bash 采用了动态作用域

现在应该对这两个概念有个清晰的认知了吧。

JavaScript 中的作用域

JavaScript 中的作用域类型分为:

  • 全局作用域(Global Scope)
  • 局部作用域(Local Scope)

    • 函数作用域
    • 块级作用域(ES6)

全局作用域

全局作用域就是最顶层的作用域,只有一个,并且可以由程序中的任何函数访问。
在 JavaScript 中,以下两种情况声明的变量和函数会处于全局作用域内:

  • 在全局执行上下文中定义的变量和函数是全局范围的;
var a = 1; // window.a

function foo() {} // window.foo
  • 未定义直接赋值的变量(非严格模式下)
function foo () {
    a = 1; // window.a
}

全局作用域中的数据,都可以通过 window 对象的属性来访问。

局部作用域

函数作用域

每个函数都有自己的作用域。函数作用域有权访问全局作用域,反之不行。

// Global Scope
function fn() {
    // Local Scope #1
    function someOtherFunction() {
        // Local Scope #2
    }
}

块级作用域

块作用域是 ES6 的新特性,它指的是变量不仅可以属于所处的作用域,也可以属于某个代码块( { .. } 内部)。

只有使用 let 和 const 关键字声明的变量才会产生块级作用域。

if (true) {
    // if 条件语句不会创建一个作用域 

    // a 处于全局作用域中
    var a = 'a';
    // b 处于当前块级作用域内
    let b = 'b';
    // c 也处于当前块级作用域内
    const c = 'c';
}

console.log(a); // a
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(c); // Uncaught ReferenceError: c is not defined
作用域链

我们都知道,局部作用域有权访问自身作用域和全局作用域;如果一个函数内部嵌套了一个函数,则嵌套的函数也是有权访问自身作用域、声明所在函数作用域以及全局作用域的。

每个作用域都存在一条由可访问的作用域形成的作用域链(Scope Chain)。

举个例子:

var a = 1

function foo () {
    var b = a + 1;

    function bar () {
        var c = b + a
    }
    bar ()
}

foo ()

当我们开始执行上述代码时,首先会创建一个全局执行上下文。在上一篇《JavaScript 之执行上下文》文尾,我们说明了,每个执行上下文都包含三个重要的属性:

  • 变量对象(Variable Object,VO)
  • 作用域链(Scope Chain)
  • this指向
这里我们先不关注变量对象和 this,后面会有单独的文章进行介绍。

执行上下文的伪代码可以表示如下:

global_EC = {
    scopeChain: {
        //  current scope + scopes of all its parents
        global_scope
    },
    variableObject: {
        // All the variables including inner variables & functions, function arguments
    },
    this: {}
}

在作用域链(scopeChain)中按照"从大到小"的顺序依次存放着当前作用域和它的所有父级作用域。
在全局执行上下文中,它的作用域链只包含一个作用域,即全局作用域。

scopeChain = [global_scope]

当执行 foo 函数时,foo 执行上下文的作用域链如下所示:

scopeChain = [global_scope, foo_scope]

当执行 bar 函数时,bar 执行上下文的作用域链如下所示:

scopeChain = [global_scope, foo_scope, bar_scope]

作用域链的查询:
当解释器在执行代码遇到一个变量时,它首先会在当前作用域内查找其值;如果找不到,它会遍历作用域链,继续从上一级作用域查找;依此类推,直到找到变量或到达作用域链的末尾(全局作用域)时结束。

下一篇

原来下篇文章是想写执行上下文中的变量对象的,但是想在介绍变量和函数是如何引用的之前,先总结一下它们是如何存储的。所以调整了一下发文顺序。

传送门:《JavaScript 之内存空间》

参考:

JavaScript深入之词法作用域和动态作用域
How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
Understanding Scope and Scope Chain in JavaScript
Understanding Scope in JavaScript


原文链接:segmentfault.com

上一篇:react native 环境运行报错问题解决
下一篇:JavaScript 之内存空间

相关推荐

官方社区

扫码加入 JavaScript 社区