声明和作用域
由于作用域相关的东西太多,所以拆分成多个章节
一、作用域概述
二、标识符与作用域
三、全局和模块作用域
四、函数作用域
五、块作用域
六、作用域 与 this
在上一章大体讲述了作用域的一些基本概念,这章我们将要讲述的是变量和作用域的关系,毕竟作用域的主要功能之一是就存储我们声明的变量
1.声明的格式
声明格式为: 声明类型 标识符 初始化器(可选)
比如: var | let name = 'cat'
声明类型:var,let 标识符:name 初始化器: = 'cat'
比如:class Person {...}
声明类型:class 标识符:Person 初始化器: {...}
再比如:function Person() {...}
声明类型:function 标识符:Person 初始化器: {...}
在 var name = 'cat' 这个表达式中,常常会说这个可以看做是两个实现的组合,一个是var name, 一个是 name = 'cat'
这种表达其实是错误的,比如 const name = 'cat',那么 我们能把它拆为两个吗?是不行的,运行的时候,会报错:Missing initializer in const declaration
说明 初始化器 和 赋值还是不一样的,当然在内部的实现也是不一样的,具体怎么不一样,下面声明的提升过程会给出答案
2.声明的提升
大家都知道,在实际运行代码之前,会有一个声明提升的操作,这里涉及到两个作用域的声明操作
1.就是把当前作用域内的所有声明提前声明
2.把当前作用域内的块级作用域内的非词法声明也提前声明
注:1.块级作用内的function声明的初始化的值是undefined,而不像在当前作用域内的function声明,是始化值就是那个函数对象
2. 上面两步不是按顺序执行的,而是为了强调即使不在当前作用域也可能被提前声明
第一点想必大家都很了解, 但是第二点可能就有点反直觉了,在哪个作用域内的声明就属于哪个作用域为什么var声明(包括function声明)就会不属于这个作用域呢?
这是由于历史原因,在es6之前,是没有块级作用域的,{} 这个是不产生块级作用域,所以这是为了兼容老的代码而做的特殊处理
3.提升的执行过程
注:1.这里说的是普通声明过程,在函数声明的时候,会有更复杂的执行机制
2. 这里的function包括 生成器函数,异步函数,为了方便,统一叫function
3.这里是非严格模式
4.这里面的方法都是调用当前作用域对象的方法,在不同的作用域,其实现可能不一样,比如全局作用域的var创建
1.扫描当前作用域的代码,得到 varNames(var 声明),varScopeList(import,export,function), lexicalNames(词法声明,包括let const class 声明),用于检查
当前作用域的声明是否符合要求,比如是否 var name和 let name同时存在
2. 设置 functionNames 和 functionToInitialize为空数组
3. 倒序遍历varScopeList,发现如果是function声明并且不在functionNames里面的话,就分别插入name和初始化器到 functionNames 和 functionToInitialize里面
注:为什么要倒序呢?是因为,函数始终是采用最后声明的那个,所以为了性能优化,就没必要去初始化前面的,所以采用倒序
4.遍历varDeclarations,如果已经声明:执行continue,否则:执行CreateMutableBinding创建绑定,执行InitializeBinding(undefined)初始化绑定值为空
5.遍历lexDeclarations,如果是const声明,执行CreateImmutableBinding,否则执行CreateMutableBinding,
6.遍历functionsToInitialize,执行CreateMutableBinding,创建函数对象,执行InitializeBinding(函数对象)
注:这里只是把大体流程描述出来,里面还有很多细节未提及,比如创建函数对象,这里还有很多细节,《函数与作用域》中会详细说到
上面描述的的是函数提升的过程,然后还有另外一个比较重要的是赋值操作,调用的方法是:SetMutableBinding(value)
这里的实现很重要,很多我们常见的错误就是从这里来的
1.如果执行SetMutableBinding对用的作用域没有创建此标识符的绑定
1.1 如果是严格模式,报ReferenceError
1.2 执行CreateMutableBinding,执行InitializeBinding(value)
2.如果此绑定还未初始化,也就是还未执行InitializeBinding,那么报ReferenceError,也就是我们常说的变量死区
3.如果这个是是一个mutable绑定,那么就吧值设为value
4.否则,这个是一个immutable绑定,也就是const声明的变量,那么就报typeError
ok,到这里,我们就可以解释为什么说 初始化器 和 赋值还是不一样的, 不一样的地方在于:初始化器调用的是InitializeBinding, 而赋值的调用的SetMutableBinding
如果一个已声明的标识符但还未初始化之前(也就是为调用InitializeBinding)去赋值的话,根据上面执行就会报错,也就是常说的变量死区
4.标识符的查找
注:这里为什么说是标识符的查找,而不是变量的查找呢?因为变量只是标识符的一种,在作用域内还绑定了其他类型的标识符,比如函数,class等等
在解释查找过程之前,先说一个相关的概念,叫引用,其结构为:{ base,ReferencedName,Strict, ThisValue }
base: 调用此方法的对象,可能是我们的js实际运行的值(原始类型或对象类型的值,除了null,undefined),也可能是作用域(envRecord),也可能是unresolvable
ReferencedName:就是我们绑定的标识符
Strict:是否是严格模式
ThisValue :super对应的值
引用有个方法叫:getValue,用于获取这个引用的实际的值
引用有三种类型:一种是属性引用,一种是声明的引用,一种是不可解析的引用,分别对应base的三种值
这里标识符的查找其实是执行primaryExpress类型的IdentifierReference表达式,其执行过程为:
1.ResolvBinding(execute context的方法)
1.GetIdentifierReference(这是作用域的方法)
1.1 如果为env为null(就是globalEnv的outEnv), 则返回一个base值为unresolvable的引用, 再去取值(调用getValue)的话,就会报错
1.2 否则env不为空,那么调用env.hasBinding, 得到是否存在
1.3 如果存在,则返回一个base值env的引用
1.4 否则不存在,那么就令env = env.outerEnv,继续调用GetIdentifierReference
2.得到一个引用ref,如果当前只是需要一个引用(比如左值表达式),那么返回这个引用
3.否则需要实际的值,则运行getValue
1.如果v不是 referrence,就直接返回v
2.检查v.base 是否为 unresolvable,如果是的话,报ReferenceError
3.检查是否是属性引用
1. 如果是属性引用,那么let baseObj = toObject(ref.base) // 这里就是我们常说的装箱操作
2. 返回baseObj[get](ref.ReferencedName, ref.getThisValue) // getThisValue 的逻辑是 如果thisValue不为空(就是处于super调用),那么返回thisValue,否则返回base值
4.到这个时候,base肯定是作用域
3.1 返回 base.GetBindingValue(ref.ReferencedName, ref.Strict)