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 contextrepresented by the purple border and 3 differentfunction contextsrepresented 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吗?
闭包
参考文章:
微信打赏
赞赏是不耍流氓的鼓励