由于作用域相关的东西太多,所以拆分成多个章节
一、作用域概述
二、标识符与作用域
三、全局和模块作用域
四、函数作用域
五、块作用域
六、作用域 与 this
首先让我们思考下,什么是作用域?
作用域的一般说来是一个抽象概念,不过还有个类似的概念叫:context,也就是代码执行上下文
所以,context 就是我们所说的作用域 。。。。吗?
1. context
让我们看看下面代码:
function test() { var name = ‘test‘ function showName(params) { console.log(name) } showName() } test()
代码的执行过程:
在showName作用域的时候,我们能够访问到 name属性,所以看起来,好像context就是我们平时所说的作用域
不过我们在看下面的代码:
function test() { var name = ‘test‘ function showName(params) { console.log(name) } return showName } var showName = test() showName()
当运行showName的时候, 我们的执行上下文栈如下:
name 这个属性不见了,但是运行showName的时候,明明能访问name,由此可见,context并不是我们所说的作用域。
但是为什么还能访问name,《函数作用域》章节会有详细解答,不过建议还是跟随先看完本章内容
2. 作用域在EcamScript中的定义
作用域本身还是抽象概念,具体的特性由js引擎去实现,但在EcamScript中有个相关定义,叫:EnvironmentRecord
在规范中,是这样解释Environment Record 的:
Environment Record is a specification type used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of ECMAScript code. Usually an Environment Record is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement. Each time such code is evaluated, a new Environment Record is created to record the identifier bindings that are created by that code.
Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record.
中文解释:环境记录是一个在此文档特殊定义的类型,用来存储变量和方法的标识符,是基于词法结构嵌套的(也就是我们常说的词法作用域,作用域在定义时确定)。通常来说,环境记录被用来匹配一些特殊的词法结构,比如函数声明(函数作用域),块语法(块作用域)或者 try中的Catch作用域。
每当此类型的代码被执行,一个新的作用域就会被创建以用于绑定在相关代码中创建的标识符(变量,函数等)
每个环境记录都有一个内部插槽叫OuterEnv,在创建此作用域的时候就确定,指向当前作用域,它可能是null(全局作用域的outerEnv) 也可能是外部(父级)作用域的引用。由此作用域链就出来了
注:1.Record 在es中定义为一个属性集合,也就相当于js中的Object
由上面的解释,我们可以做出如下总结:
- 作用域就是一个对象
- 作用域链是以作用域中的OuterEnv属性形成一个链表
- 每次执行特殊结构代码(比如function, Block)的时候都会创建一个,但是我们的context只会在 执行Global,Module,function中创建
3. 作用域的类型
作用域其实还有类型的区别的,一种叫 Declarative Environment Records, 另外一个叫 Object Environment Records
Declarative Environment Records 比较好理解,就是 声明作用域,简而言之就是声明的都会分配给这个作用域
Object Environment Records:这个就有点新奇了,从字面来说就是 对象作用域,至于其作用,就让我们先看看下面的代码:
let obj = { name: ‘obj‘ }
// with 中有一个 对象作用域 with(obj) { console.log(name) // obj }
运行代码的话,就会打印出obj,从结果可以推断出,name访问的就是with后面跟随的obj对象,可以思考下和普通的变量访问的区别
现在我们基本能理解对象作用域的机制了:变量是通过查找作用域绑定的对象的属性,而普通作用域的变量查找是在此作用域声明的变量
4. 作用域的方法
作用域做为一个对象,自然也是可以有方法的,下面我们来说说里面的几个重要方法:
CreateImmutableBinding:创建不可变的绑定(变量),比如 const name, 就会创建一个 不可能变的 name变量
CreateMutableBinding:创建可变的绑定(变量),比如 除开const,其他声明创建的变量都是都是可变的
HasBinding:检查是否声明了这个变量
InitialBindingValue:设置绑定的值,比如: var name = ‘123‘ 中 name = ‘123’ 就会调用此方法
SetBindingValue:改变绑定的值,比如: name = ‘123‘
HasThisBinding:是否绑定了this对象
注: 1.Create..Binding 和 InitialBindingValue是声明变量的重点,也是实现变量死区的关键,具体的在《变量和作用域》章节说明
5. 常见的几种作用域
Global作用域: 全局作用域,代码运行前就会创建
Module作用域:模块作用域,运行 <script type=‘module‘>中的代码会创建
Function作用域: 函数作用域,每次运行函数都会创建一个新的函数作用域
Block作用域:块作用域, 运行 { } 这种的就会创建
6. 作用域的继承体系
没错,你没看错,作用域也有继承体系
上面提及作用域有两种类型:Declarative Environment Records, Object Environment Records
Block作用域是Declarative Environment Records
Function作用域 继承于 Declarative Environment Records, 有些新增的方法或属性: 比如 BindingThis, ThisBindingStatus, ThisValue 等等
Global作用域就好玩了,里面即包含Declarative Environment Records, 也包含 Object Environment Records, 具体的在后面的 《全局作用域》章节有详细解释
7. 再探 作用域 和 context的关系
上面提到作用域 和context 是两个东西,但是它们之间还是有关系的
context 里面有两个属性:VarEnv, LexicalEnv, 也就是有一个变量作用域:用于存储通过var或function声明变量,一个词法作用域:用于存储let,const,class, import声明的变量
VarEnv, LexicalEnv 都是作用域,只是通过名称来区别他们的作用
初始化context的时候,会根据context的类型,创建对应的作用域,VarEnv 和LexicalEnv 同时指向了这个新创建的作用域
当然, 作用域的创建不仅仅是在context创建的时候才会创建,比如Block作用域,并不会创建context,但是会创建作用域
8. 总结
1. 作用域是一个对象,用于绑定声明的变量,方法等
2. 作用域有两种类型,分别为:DeclarationEnv或者ObjectEnv,他们的区别在于 变量的查找方式,一个是通过查找声明的,一个是查找绑定对象的属性
3. 作用域有几种表现形式,分别问:全局,模块,函数,块作用域,每种作用域都继承与DeclarationEnv或者ObjectEnv,都有自己的方法属性
4. 在作用域创建的时候,会传入一个作用域做为新创建的作用域的父级作用域,全局作用域的父级作用域为null,由此形成了作用域链,
5. context和作用域是两个东西,不过context有两个属性:varEnv(存储var或function声明的变量), lexcialEnv(存储const,let等声明的变量),context创建的时候,也会创建一个新的作用域,并且两个属性都指向这个作用域