首发地址: https://github.com/jeuino/Blo...
在上一篇《JavaScript 之执行上下文》中介绍了什么是执行上下文与执行上下文栈,本篇文章主要总结了:javascript
在《 JavaScript 引擎(V8)是如何工做的》中说到过,做用域是由 V8 的 Parser 解析器肯定的。那么什么是做用域呢,咱们下面来聊一聊。java
在软件设计中,有一个公共的原则——最小受权(暴露)原则。这个原则是指在软件设计中, 应该最小限度地暴露必要内容, 而将其余内容都“隐藏” 起来。git
这样作的优势是:github
在 JavaScript 中就是经过做用域来实现最小受权原则的。编程
做用域规定了变量和函数的可访问性。浏览器
做用域实施了一套严格的规则,用于规定在 JavaScript 运行时如何查找变量和函数,也就是肯定当前执行代码对变量和函数的访问权限。bash
做用域共有两种主要的工做模型。 第一种是最为广泛的, 被大多数编程语言所采用的词法做用域。 另一种叫做动态做用域。session
JavaScript 采用的是词法做用域,也称为静态做用域。编程语言
词法做用域是由你在写代码时变量和函数声明的位置来决定的。函数
词法做用域的“父子关系”,取决于代码书写时的嵌套关系。
咱们来分析下面这段代码:
var a = 1; // foo 函数声明在全局执行上下文中 function foo() { console.log(a); } // bar 函数声明在全局执行上下文中 function bar() { var a = 2; foo(); } bar();
这里补充一个小知识,就是上图描述文字中出现的 RHS 是什么意思,这涉及到了 JavaScript 执行过程当中引擎是如何查找变量的。
引擎在执行代码时,会经过查找标识符来判断它是否已经声明过。查找的过程由做用域进行协助,可是引擎是怎么查找的呢?引擎查找变量有两种方式,分别是:
若是查找的目的是对变量进行赋值,则使用 LHS 查询(告诉做用域我须要对 a 变量进行 LHS 引用,你见过它嘛?)
不成功的 LHS 引用会致使自动隐式建立一个全局变量(非严格模式),严格模式下抛出ReferenceError 异常
若是查找的目的是获取变量的值,则使用 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 中的做用域类型分为:
局部做用域(Local Scope)
全局做用域就是最顶层的做用域,只有一个,而且能够由程序中的任何函数访问。
在 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 之执行上下文》文尾,咱们说明了,每一个执行上下文都包含三个重要的属性:
这里咱们先不关注变量对象和
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深刻之词法做用域和动态做用域
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