Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 之作用域与作用域链 #8

Open
jejuin opened this issue Mar 24, 2020 · 0 comments
Open

JavaScript 之作用域与作用域链 #8

jejuin opened this issue Mar 24, 2020 · 0 comments
Labels
javascript Improvements or additions to documentation
Projects

Comments

@jejuin
Copy link
Owner

jejuin commented Mar 24, 2020

作用域

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

这样做的优点是:

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

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

作用域基本上是变量的一个集合以及如何通过名称访问这些变量的规则。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

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

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

词法作用域

词法作用域是由你在写代码时变量和函数声明的位置来决定的。通过它就能够预测代码在执行过程中如何查找标识符。

词法作用域的“父子关系”,取决于代码书写时的嵌套关系,和函数是怎么调用的没有关系

一个作用域内的代码可以访问这个作用域内以及任何包围在它之外的作用域中的变量。

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

var a = 1;

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

bar();

Image  2

这里补充一个小知识,就是上图描述文字中出现的 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

Image  6

如果 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 ()

当我们开始执行上述代码时,首先会创建一个全局执行上下文。

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

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深入之词法作用域和动态作用域
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

@jejuin jejuin added the javascript Improvements or additions to documentation label Mar 24, 2020
@jejuin jejuin added this to 执行机制 in JavaScript Mar 24, 2020
@jejuin jejuin closed this as completed Nov 19, 2020
@jejuin jejuin reopened this Nov 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javascript Improvements or additions to documentation
Projects
No open projects
JavaScript
执行机制
Development

No branches or pull requests

1 participant