(理解)新ECMA文档中代码执行术语解析
新的ECMA代码执行描述
-
在执行学习JavaScript代码执行过程中,我们学习了很多ECMA文档的术语:
- 执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;
- 执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;
- 变量对象:Variable Object,上下文关联的VO对象,用于记录函数和变量声明;
- 全局对象:Global Object,全局执行上下文关联的VO对象;
- 激活对象:Activation Object,函数执行上下文关联的VO对象;
- 作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;
-
在新的ECMA代码执行描述中(ES5以及之上),对于代码的执行流程描述改成了另外的一些词汇:
- 基本思路是相同的,只是对于一些词汇的描述发生了改变;
- 执行上下文栈和执行上下文也是相同的;
词法环境(Lexical Environments)
- 词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符;
- 一个词法环境是由环境记录(Environment Record)和一个外部词法环境(outer Lexical Environment)组成
- 一个词法环境经常用于关联一个函数声明、代码块语句、try-catch语句,当它们的代码被执行时,词法环境被创建出来
- 也就是在ES5之后,执行一个代码,通常会关联对应的词法环境;
- 那么执行上下文会关联哪些词法环境呢?
LexicalEnvironment和VariableEnvironment
- LexicalEnvironment(词法环境)用于处理let、const声明的标识符:
翻译:
let和const声明定义了作用域为运行执行上下文的LexicalEnvironment的变量。变量是创建时,它们包含的词法环境被实例化,但可能不能以任何方式访问,直到变量的对LexicalBinding进行计算。由带有初始化器的LexicalBinding定义的变量被赋予其初始化器的值赋值表达式是在LexicalBinding被求值时,而不是在变量被创建时。如果在let声明中使用LexicalBinding没有初始化器,则变量在LexicalBinding计算时被赋值为undefined。
- VariableEnvironment(变量环境)用于处理var和function声明的标识符:
翻译:
var语句声明了运行执行上下文的VariableEnvironment范围内的变量。Var变量被创建当它们所包含的词汇环境被实例化并在创建时初始化为未定义时。在任何范围内一个通用的Bindingldentifier VariableEnvironment可以出现在多个VariableDeclaration中,但这些声明可以一起出现只定义一个变量。由带有初始化器的VariableDeclaration定义的变量被赋给其初始化器的值
AssignmentExpression是在执行VariableDeclaration时,而不是在创建变量时
环境记录(Environment Record)
- 在这个规范中有两种主要的环境记录值:声明式环境记录和对象环境记录
- 声明式环境记录:声明性环境记录用于定义ECMAScript语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与
ECMAScript语言值关联起来的Catch子句 - 对象式环境记录:对象环境记录用于定义ECMAScript元素的效果,例如WithStatement,它将标识符绑定与某些对象的属性
关联起来
- 声明式环境记录:声明性环境记录用于定义ECMAScript语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与
翻译:
在本规范中使用了两种主要类型的环境记录值:声明性环境记录和对象环境记录。声明性环境记录用于定义ECMAScript语言语法元素的效果,如FunctionDeclarations、VariableDeclarations,以及直接将标识符绑定与ECMAScript语言值关联的Catch子句。对象环境记录包括用于定义ECMAScript元素的效果,例如将标识符绑定与某些对象的属性相关联的WithStatement。全球环境记录和函数环境记录是专门用于脚本全局声明和top-的专门化函数中的级别声明。
新ECMA描述内存图
- 这个Global Execution Context,在Environment Record中是存在两个空间的,var跟let的是分开放的
(掌握)let-cosnt的基本使用和注意事项
- 在ES5中我们声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const
- let、const在其他编程语言中都是有的,所以也并不是新鲜的关键字;
- 但是let、const确确实实给JavaScript带来一些不一样的东西;
- let关键字:
- 从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量;
- const关键字:
- const关键字是constant的单词的缩写,表示常量、衡量的意思
- 它表示保存的数据一旦被赋值,就不能被修改
- 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容
- 注意:
- 另外let、const不允许重复声明变量;
JavaScript中的引用类型有以下几种:
- Object: 对象是 JavaScript 中最常用的引用类型。对象可以是一个键值对的集合,可以用来存储各种类型的数据。
- Array: 数组是一种特殊的对象,用于存储一组有序的数据。
- Function: 函数是可以被调用的对象,它可以接收参数并返回结果。
- Date: 日期是用于表示日期和时间的对象。
- RegExp: 正则表达式是用于在字符串中执行模式匹配的对象。
- Map: Map 是一种用于存储键值对的数据结构
- Set: Set 是一种用于存储单独的项目的数据结构
这些都是JavaScript中的引用类型, 是对象类型,是非基本类型。
//var变量提升,声明前访问也只会undefined
console.log(foo);const info = {
name:"小余"
}
info.name = "大满"
console.log(info.name);
//info = {}这样info是会报错的,因为info指向发生了改变,我们info是通过const赋值,不能改变
console.log(info);
var foo = 123
//let声明之前访问报错
console.log(foo);
let foo = 123
//const的内容确定后不可改变,除非赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象
const message = "Hello World"
message = "小余"
console.log(message);//报错
let message = "Hello World"
message = "小余"
console.log(message);//小余
//const 赋值引用类型
const info = {
name:"小余"
}
info.name = "大满"
console.log(info.name);
//info = {}这样info是会报错的,因为info指向发生了改变,我们info是通过const赋值,不能改变
console.log(info);
(掌握)let-const没有作用域提升和暂时性死区
let/const作用域提升
-
let、const和var的另一个重要区别是作用域提升:
- 我们知道var声明的变量是会进行作用域提升的;
- 但是如果我们使用let声明的变量,在声明之前访问会报错
-
那么是不是意味着foo变量只有在代码执行阶段才会创建的呢?
-
事实上并不是这样的,我们可以看一下ECMA262对let和const的描述
-
这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值
有像var那样被提前创建出来,但是在赋值之前不能像var那样被访问还能返回undefined,这是在赋值之前不能够被访问的
-
翻译:
Let和const声明定义了作用域为运行执行上下文的变量LexicalEnvironment。当包含变量的词法环境为时,将创建变量实例化,但在变量的LexicalBinding被求值之前不能以任何方式访问。由带有初始化器的LexicalBinding定义的变量被赋予其初始化器的值赋值表达式是在LexicalBinding被求值时,而不是在变量被创建时。如果一个let声明中的LexicalBinding没有初始化器,变量被赋值在计算LexicalBinding时未定义。
暂时性死区 (TDZ)
-
我们知道,在let、const定义的标识符真正执行到声明的代码之前,是不能被访问的
-
从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)
通俗的说就是这段访问不了的区域叫做暂时性死区
-
//暂时性死区
function foo(){
console.log("小余");
console.log("测试");
// console.log(bar);
let bar = "bar"
let good = "好的"
}
- 使用术语 “temporal” 是因为区域取决于执行顺序(时间),而不是编写代码的位置;
//暂时性死区和定义的位置没有关系,和代码执行的顺序有关系
function foo(){
console.log(message);
}
let message = "Hello World"
foo()//Hello World,foo函数定义在message的上面,但是依旧可以打印出来内容不报错,因为我们是在let message的后面进行调用的
console.log(message);//Hello World
//暂时性死区形成之后,在该区域内这个标识符不能访问
let message = "Hello World"
function foo(){
console.log(message);//能访问到吗?很显然不行,我们在很早之前作用域的时候也做过类似的操作,在控制台打印message,他会先在自己的当前作用域寻找,message其实在这里面已经形成,词法坏境实例化,只是不能访问,message是在内部存在的,这就阻止了控制台打印的message继续向外层作用域寻找(虽然外层作用域有,但是已经在内层就被拦截住了)
let message = "小余"
}
foo()//报错啊啦啦
console.log(message);
let/const有没有作用域提升呢?
- 从上面我们可以看出,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问的
- 那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
- 事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解;
- 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;
- 在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;
- 所以我的观点是let、const没有进行作用域提升,但是会在解析阶段被创建出来