首发地址:https://github.com/jeuino/Blo...
概述
在上一篇《JavaScript 之执行上下文》中介绍了什么是执行上下文与执行上下文栈,本篇文章主要总结了:
- 什么是作用域?
- 什么是词法作用域和动态作用域?它们的区别是什么?
- JavaScript 采用了什么类型的作用域?
- JavaScript 中作用域的类型?
- 执行上下文中的作用域链?
作用域 Scope
在《 JavaScript 引擎(V8)是如何工作的》中说到过,作用域是由 V8 的 Parser 解析器确定的。那么什么是作用域呢,我们下面来聊一聊。
在软件设计中,有一个公共的原则——最小授权(暴露)原则。这个原则是指在软件设计中, 应该最小限度地暴露必要内容, 而将其他内容都“隐藏” 起来。
这样做的优点是:
- 可以降低多文件引入时,变量或函数命名出现冲突的概率;
- 如果将所有内容都暴露给全局环境,那么会占用很多无用内存,只有当关掉浏览器或当前窗口时,全局变量才会被回收;
- 如果程序出现错误,可以更小范围的确定出错区域;
在 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深入之词法作用域和动态作用域
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。