全局和模块作用域
由于作用域相关的东西太多,所以拆分成多个章节
一、作用域概述
二、标识符与作用域
三、全局和模块作用域
四、函数作用域
五、块作用域
六、作用域 与 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
- If GlobalEnv.HasVarDeclaration(name) is true, throw a SyntaxError exception.
- If GlobalEnv.HasLexicalDeclaration(name) is true, throw a SyntaxError exception.
GlobalEnv.HasVarDeclaration:
- Let varDeclaredNames be envRec.[[VarNames]].
- If varDeclaredNames contains N, return true.
- Return false.
GlobalEnv.HasLexicalDeclaration:
- Let DclRec be envRec.[[DeclarativeRecord]].
- 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下面直接运行
模块作用域也不像全局作用域那样,内部包含 对象作用域和声明作用域,它本身就是一个声明作用域
还需要记住一点的是:在模块作用域中,会视作是严格模式,不管你是否声明了严格模式

浙公网安备 33010602011771号