jejuin / Blog

我的博客:记录、总结、分享前端知识
MIT License
4 stars 0 forks source link

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

Open jejuin opened 4 years ago

jejuin commented 4 years ago

作用域

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

这样做的优点是:

  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 执行过程中引擎是如何查找变量的。

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

引擎查找变量有两种方式,分别是:

请看下面这个例子,其中 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 中的作用域类型分为:

全局作用域

全局作用域就是最顶层的作用域,只有一个,并且可以由程序中的任何函数访问。

在 JavaScript 中,以下两种情况声明的变量和函数会处于全局作用域内:

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深入之词法作用域和动态作用域](https://github.com/mqyqingfeng/Blog/issues/3)
> [How JavaScript works: Parsing, Abstract Syntax Trees \(ASTs\) + 5 tips on how to minimize parse time](https://blog.sessionstack.com/how-javascript-works-parsing-abstract-syntax-trees-asts-5-tips-on-how-to-minimize-parse-time-abfcf7e8a0c8)
> [Understanding Scope and Scope Chain in JavaScript](https://blog.bitsrc.io/understanding-scope-and-scope-chain-in-javascript-f6637978cf53?gi=5d972df019f4)
> [Understanding Scope in JavaScript](https://scotch.io/tutorials/understanding-scope-in-javascript)