声明和作用域

由于作用域相关的东西太多,所以拆分成多个章节

一、作用域概述

二、标识符与作用域

三、全局和模块作用域

四、函数作用域

五、块作用域

六、作用域 与 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) 

 

posted @ 2021-01-28 21:24  唐强136  阅读(129)  评论(0)    收藏  举报