执行上下文
浏览器的JavaScript引擎会创造一个特殊的环境来处理这些JavaScript代码的转换和执行。
分类
- 全局执行上下文:(仅此一个)每当 JavaScript 引擎接收到脚本文件时,它首先会创建一个默认的执行上下文。浏览器中的全局对象就是 window对象,this 指向这个全局对象
- 函数执行上下文:(很多个)每当函数被调用时,JavaScript引擎就会在GEC内部创建另一种执行上下文。只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
- eval():(不常用)
创建
AO:创建执行上下文时与之关联的会有一个变量对象,该上下文中的所有变量和函数全都保存在这个对象中。
VO:进入到一个执行上下文时,此执行上下文中的变量和函数都可以被访问到,可以理解为被激活
- 创建变量对象(VO)
- VO:是一个在执行上下文中创建的类似于对象的容器,存储执行上下文中变量和函数声明。这种将变量和函数声明存储在内存中优先于执行代码的过程被称为变量提升(后面有章节专门分析)。
- 创建作用域链
- JS作用域链:JavaScript引擎一路向上遍历执行上下文直至解析处在函数内部触发的变量和函数。JS引擎会先从自己的作用域中找,如果未找到,则会一直向上查找父作用域直至GEC作用域。
- 每一个函数执行上下文都会创建一个作用域:作用域相当于是一个空间/环境,变量和函数定义在这个空间里,并且可以通过一个叫做作用域查找的过程访问。
- 如果函数被定义在另一个函数内部,处在内部的函数可以访问自己内部的代码以及外部函数(父函数)的代码。这种行为被称作词法作用域查找。也叫闭包。但外部函数并不能访问内部函数的代码。
- 设置this关键字的值
- this关键字指的是执行上下文所属的作用域。一旦作用域链被创建,JS引擎就会初始化this关键字的值。
- 全局上下文中的"this"值为window(严格模式指向undefined)
- 函数执行上下文中,this取决于函数是如何被调用的,如果是被一个对象调用的,那么this被设置为那个对象,否则this是全局对象window(严格模式指向undefined)
执行
在执行阶段,JS引擎会再次读取执行上下文,并用变量的实际值更新VO。
执行栈又称调用栈,记录了脚本整个生命周期中生成的执行上下文。
JavaScript是单线程语言,也就是说它只能在同一时间执行一项任务。因此,其他的操作、函数和事件发生时,执行上下文也会被创建。由于单线程的特性,一个堆叠了执行上下文的栈就会被创建,称为执行栈
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
作用域链
作用域链:用于确定在当前执行环境中如何查找变量。它关系到词法作用域(lexical scoping),即变量的可见性和作用域是由它们在源代码中的位置决定的。
词法作用域:函数的作用域在函数定义的时候就已经确定了,而非在执行时。这意味着函数可以访问它被定义时的环境中的变量,不论它在哪里被调用。
- 当代码在一个环境中执行时,JavaScript 需要访问变量。
- 如果一个变量在当前作用域中没有找到,JavaScript 会查找这个变量的外层作用域。
- 这个查找过程会持续进行,直到找到变量或者达到全局作用域。
- 如果在全局作用域中仍未找到该变量,通常会抛出一个错误(在非严格模式下是 undefined)。
闭包
这是我为闭包专门写的一篇文章。闭包
变量提升
分类
- 函数
function func(){}
函数声明会被完整地提升,意味着既提升了函数名也提升了函数体。因此,在声明之前就可以安全地调用这些函数。var func = function() {}
函数表达式(无论是命名的还是匿名的)的行为就像变量一样,如果使用var声明,其名称会被提升,但赋值(函数体)不会;如果使用let或const声明,就会遵循TDZ的规则。
- 变量
- 对于var声明的变量,提升会将声明移至作用域顶部,而赋值则保留在原来的位置。
- 对于let和const声明的变量,提升的行为更复杂一些。虽然它们也会被提升,但是不会被初始化,直到代码执行到声明的地方。在声明之前的任何访问都会引发一个ReferenceError,这个区域通常被称为“暂时性死区”
变量提升常见例题
- 重名问题
- 函数声明式和 var 声明的变量重名,则最终值取决于var定义的变量
- 如果是函数表达式,那么取决于早晚,谁最晚就是谁
// 函数声明式
var fn = 12
function fn() {
console.log('林一一')
}
console.log(window.fn)
fn()
/* 输出
* 12
* Uncaught TypeError: fn is not a function
*/
// 函数表达式
var fn = 12
var fn = function() {
console.log('c一一')
}
console.log(window.fn)
fn()
/* 输出
* functon() {...}
* c一一
*/
var a = 2;
function a() {
console.log(3);
}
console.log(typeof a);
// number
- 带 var 和不带 var 的区别
- 全局作用域中不带var声明变量虽然也可以但是建议带上 var声明变量,不带 var 的相当于给window对象设置一个属性。
- 私有作用域(函数作用域),带 var 的是私有变量。不带 var 的是会向上级作用域查找,如果上级作用域也没有那么就一直找到 window 为止,这个查找过程叫作用域链。
- 全局作用域中使用 var 申明的变量会映射到 window 下成为属性。
console.log(a, b)
var a =12, b ='c一一'
function foo(){
console.log(a, b)
var a = b = 13 // b 未使用var关键字,相当于给window.b赋值
console.log(a, b)
}
foo()
console.log(a, b)
// undefined undefined 变量提升
// undefined c-- a在函数作用域中被变量提升,但是b拿到的事window
// 13 13 作用域中的a是13,window.b是13
// 12 13 window.a为12,window.b是13
- 等号左边下的变量提升
print()
var print = function() {
console.log('林一一')
}
print()
/*输出
Uncaught TypeError: print is not a function var进行了变量提升,为undefined,所以报错
/
- 条件判断下的变量提升
- 在当前作用域中不管条件是否成立都会进行变量提升
if(!("value" in window)){
var value = 2019;
}
console.log(value);
console.log('value' in window);
// undefined
// true
面试例题:
function func() {
console.log('1')
}
func()
function func() {
console.log('2')
}
func()
// 输出:2 2 ,因为function定义的函数会被变量提升
var func = function() {
console.log('1')
}
func()
var func = function() {
console.log('2')
}
func()
// 输出:1,2,因为函数表达式不能被提升
var b = 1;
function f() {
console.log(b);
var b = 2;
}
f();
// 输出:undefined,因为在f()作用域中,var定义的变量会被变量提升
let a = 3;
{
console.log(a);
let a = 4;
console.log(a);
}
// 输出:报错,let定义的变量具有暂时性死区,所以在定义之前使用会报错