全局和模块作用域

 

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

 

一、作用域概述

 

二、标识符与作用域

 

三、全局和模块作用域

 

四、函数作用域

 

五、块作用域

 

六、作用域 与 this

 

 

一、全局作用域

1.概述

 全局作用域可以说是最特殊的作用域了,为什么这么说呢?

在之前提到过有两种类型的作用域: Declarative Environment (声明作用域)和  Object Environment(对象作用域)

然而全局作用域包含了这两种作用域,而不是单一的作用域,我们看下下面的代码会打印出什么:

可以先思考下,想想答案是什么:

<script>
  var name = 'cat'
  let sex = 1
     console.log(window.name, window.sex)
</script>

 

这里会输出:cat, undefined

其原理是在全局作用域中,如果是variable(var function)声明的话,就会在对象作用域去创建新的属性,而全局作用域的对象作用域绑定的就是全局对象(window),

所以在声明name的时候,就会给window创建一个新的name属性,所以访问window.name 是有值的,而如果是词法声明的话,就像普通的声明一样,存储于声明作用域中,

所以window上并没有去创建sex属性,当然window.sex也就是undefined的了

 

既然全局作用域包含两个作用域,根据之前的理解对作用域,每个作用域都可以包含自己的变量,也就是说声明作用域可以声明变量name,对象作用域也能声明变量name,

就像下面代码:

<script>
  var name = 'cat'
  let name = 'cat'
</script>

但是,我们试着去运行这段代码的话,明显会 报错,提醒我们重复声明了,说明全局作用域又实现了普通作用域那样的特性,

其实现原理可能是:在全局作用域的声明提升中,也会遍历当前已经声明的,然后发现已经有var name声明了,所以报错

ok,我们来看看下面这个更好玩的例子,运行这段代码:

<script>
  var name = 'cat'
</script>

<script>
    let name = 'cat'
</script>

运行结果,同样是报错,根据上面的说法,全局作用域得不到当前已经声明了name的信息,难道是去全局对象上去找?

注:属性的查找好像只是一个很简单的查找操作,算法复杂度是O(1),但其实不是这么简单的,特别是当属性很多的时候,当然这不是本文的重点

 

2. 全局中作用域的声明

要想了解上面的疑问,我们得先看看,在全局作用域中的声明是如何实现的

通过上面,我们指导全局包含两个作用域,所以哪些声明是绑定在声明作用域的,哪些是绑定在对象作用域中呢?

在es规范规范中是这样说明:把 var 和function类型的声明绑定对象作用域,并且把其标识符或者说名字放在varNames中, 其他的绑定到声明作用域

这里提到一个新的术语:varNames,它就是用来解决多个脚本执行的时候,其他脚本怎么知道之前在对象作用域中已经声明了哪些标识符

上面提到varNames只存储了绑定到对象作用域的声明,那么为什么声明作用域为什么就不用了呢,因为我们所有声明作用域就是同一个,可以直接调用其方法就能得知是否已经声明

而且声明作用域不像对象作用域(全局对象)的属性那么多,查找相对也不是那么消耗性能,所以也没必要去存储

 

好了,下面贴一下es中,全局作用域在声明提升的时候,是怎么去检查的:

注:这是删减版,并且没包含严格模式处理相关逻辑

For each element name of lexNames, do

  1. If GlobalEnv.HasVarDeclaration(name) is true, throw a SyntaxError exception.
  2. If GlobalEnv.HasLexicalDeclaration(name) is true, throw a SyntaxError exception.

GlobalEnv.HasVarDeclaration:

  1. Let varDeclaredNames be envRec.[[VarNames]].
  2. If varDeclaredNames contains N, return true.
  3. Return false.

GlobalEnv.HasLexicalDeclaration:

  1. Let DclRec be envRec.[[DeclarativeRecord]].
  2. Return DclRec.HasBinding(N).  // 这里就执行普通的属性查找

遍历词法声明,检查varNames里面是否有name,如果有就报错

这样也就能解释,在不同脚本中,全局作用域也能获取到其他脚本已经声明的变量了

 

2.全局作用域标识符的查找

上面提到,全局作用域中包含两个作用域,所以也有可能,包含两个相同的属性,比如下面代码:

<script>
  window.name = 'cat'
  console.log(name)

  window.sex= 0
  let sex = 1
  console.log(sex)
</script>

  

第一个name访问就是我们全局作用域中的对象作用域,也就是获取的window.name 的值,那么第二个访问的是我们声明的sex还是window.sex 呢?

根据直觉,肯定是我们声明的sex,打印出是1,当然这也是对的,全局作用域中两个作用域访问顺序就是,先访问声明作用域,然后才是对象作用域,

还有个细节就是,一旦声明作用域声明了某个标识符,如果你去访问的话,就会直接返回这个标识符的绑定值, 不管是否已经初始化了,当然,如果是未初始化,那么

就会报错,而不是再去对象作用域去找, 可以运行如下代码验证下:

 

<script>
      window.sex = 0
  console.log(sex)
  let sex = 1
</script>

 

二、模块作用域

模块作用域相对比较简单一点,如果script标签的type="module", 那么运行此脚本创建的是模块作用域,并且它的outerEnv就是全局作用域,

可以运行如下代码,看是否会报错:

<script>
  let sex = 1
</script>

<script type="module">
  let sex = 1
</script>

 结果是肯定不会报错的,这样验证了模块作用域和全局作用域是不同的,虽然它们的代码都是直接在script下面直接运行

模块作用域也不像全局作用域那样,内部包含 对象作用域和声明作用域,它本身就是一个声明作用域

还需要记住一点的是:在模块作用域中,会视作是严格模式,不管你是否声明了严格模式

 

posted @ 2021-01-29 12:02  唐强136  阅读(331)  评论(0)    收藏  举报