js执行上下文、词法作用域、块级作用域相关学习
本文于989天之前发表,文中内容可能已经过时。
JavaScript引擎
JavaScript引擎的一个流行示例是Google的V8引擎。例如,在Chrome和Node.js中使用V8引擎,下面是一个非常简化的视图:
浏览器中的JavaScript解释器是作为一个单线程实现的,这实际上意味着,在浏览器中,一次只能发生一件事,其他操作或事件将排队在所谓的执行堆栈中。
词法作用域
函数作用域和块级作用域
块级作用域与函数声明
- ES5 只有全局作用域和函数作用域,没有块级作用域。
- ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数。
- ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
- ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
- 为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
let
、const
必须在{}
大括号包裹下产生块级作用域,来感受下let
和var
的区别吧
1 | var aa = 'bb'; |
1 | var aa = 'bb'; |
1 | function bs() { |
1 | function fn() { |
1 | function fn() { |
变量提升
暂时性死区
使用let/const声明的变量,从一开始就形成了封闭作用域,在声明变量之前是无法使用这个变量的,这个特点也是为了弥补var的缺陷(var声明的变量有变量提升)
1 | console.log(aa); // ReferenceError: Cannot access 'aa' before initialization) |
剖析暂时性死区的原理,其实let/const同样也有提升的作用,但是和var的区别在于:
- var在创建时就被初始化,并且赋值为undefined
- let/const在进入块级作用域后,会因为提升的原因先创建,但不会被初始化,直到声明语句执行的时候才被初始化,初始化的时候如果使用let声明的变量没有赋值,则会默认赋值为undefined,而const必须在初始化的时候赋值。而创建到初始化之间的代码片段就形成了暂时性死区
ES6 Class
不存在变量提升
1 | console.log(Test); // ReferenceError: Cannot access 'Test' before initialization |
解释器如何评估JS代码(evaluate)
- 扫描被调用函数中的代码
- 在代码执行前,创建执行上文
- 进入创建阶段
- 初始化作用域链
- 创建变量对象
- 创建arguments对象,检查参数上下文,初始化名称和值,并创建引用副本
- 扫描上下文中函数的声明
- 对于找到的每个函数,在变量对象中创建一个属性,该属性是确切的函数名,该函数在内存中有一个指向该函数的引用指针
- 如果函数名已经存在,指针将会被覆盖
- 扫描变量的声明
- 对于找到的每个变量,在变量对象中创建一个属性,该属性是确切的变量名,该变量的值是undefined
- 如果变量名已经存在,将不会做任何处理继续执行
- 决定this的值
- 代码执行阶段
- 变量赋值,按顺序执行代码
结合自身的感悟如下:
知道了js代码执行前有预编译这么一回事,也就能掌握变量提升,解释器根据
var
,function
等关键字进行预编译,function
声明整体前置,var
声明变量并赋值为undefined
,体现了function
是一等公民1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16(function() {
console.log(foo); // foo() { return 'hello'; }
console.log(bar); // undefined
var bar = function() {
return 'world';
};
var foo = 'hello';
function foo() {
return 'hello';
}
console.log(foo); // 'hello'
}());词法作用域决定了函数执行上下文作用域,跟函数在哪调用没有关系(
this
在任何情况下都不指向函数的词法作用域,this
是在函数运行时,创建上下文的时候确定的)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function a() {
var myOtherVar = "inside A";
b();
}
function b() {
var myVar = "inside B";
console.log("myOtherVar:", myOtherVar); // 'global otherVar'
function c() {
console.log("myVar:", myVar); // 'inside B'
}
c();
}
var myOtherVar = "global otherVar";
var myVar = "global myVar";
a();
执行上下文
执行上下文:当前代码正在执行的环境(作用域),一般有以下三种情况:
- 全局代码 – 代码首次开始执行的默认环境
- 函数代码 – 每当进入一个函数内部
- Eval代码 – eval内部代码执行时
Nothing special is going on here, we have 1
global context
represented by the purple border and 3 differentfunction contexts
represented by the green, blue and orange borders. There can only ever be 1global context
, which can be accessed from any other context in your program.You can have any number of
function contexts
, and each function call creates a new context, which creates a private scope where anything declared inside of the function can not be directly accessed from outside the current function scope. In the example above, a function can access a variable declared outside of its current context, but an outside context can not access the variables / functions declared inside.(理解为作用域链)
现在我们知道每当有函数被调用时,都会创建一个新的执行上下文。在js内部,每个执行上文创建都要经历下面2个阶段:
- 创建阶段(函数被调用,但还没有执行内部代码)
- 创建作用域链
- 创建变量和参数
- 决定this指向
- 代码执行阶段
- 变量赋值,执行代码
this 全面解析
推荐阅读:刘小夕 - 嗨,你真的懂this吗?
闭包
参考文章:
赞赏是不耍流氓的鼓励