[TOC]
写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。Github地址。
变量提升
变量声明提升
举个🌰:
var bar = 1;
function test() {
console.log(bar); // undefined
var bar = 2;
console.log(bar); // 2
}
test();
2
3
4
5
6
7
上述代码正常输出undefined,而不是报错Uncaught ReferenceError: bar is not defined
,这是因为变量声明提升(hoisting),相当于如下代码:
var bar = 1;
function test() {
var bar;
console.log(bar); // undefined
bar = 2;
console.log(bar); // 2
}
test();
2
3
4
5
6
7
8
再看个🌰:
function test(bar) {
console.log(bar); // 3
var bar = 2; // 这里其实也存在变量声明提升
console.log(bar); // 2
}
test(3);
2
3
4
5
6
从上述代码中,可以看出:如果函数有形参,先给形参赋值。
console.log('a' in window); // true
if (!('a' in window)) {
console.log(111); // 都不会进入if判断
var a = 1;
}
console.log(a); // undefined
2
3
4
5
6
两步走:
- 代码预解析(将变量进行提升)
- 代码执行
相当于:
var a;
console.log('a' in window);
if (!('a' in window)) {
console.log(111);
a = 1;
}
console.log(a);
2
3
4
5
6
7
函数和变量同名(同时提升)
foo();
var foo = 'foo';
function foo() {
console.log('foo()');
}
// 输出:foo()
2
3
4
5
6
相当于:
function foo() {
console.log('foo()');
}
var foo;
foo(); // 这里可以正常执行,因为函数声明提升优先级更高
foo = 'foo';
2
3
4
5
6
当遇到函数和变量同名且都会被提升的情况,函数声明提升优先级更高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。
函数声明和函数表达式区别
创建函数有两种方式,一种是通过函数声明function fn(){}
,另一种是通过函数表达式var fn = function(){}
,那这两种方式在函数提升上有什么区别?
foo(); // Uncaught TypeError: foo is not a function
bar(); // 2
var foo = function() {
console.log(1);
}
function bar() {
console.log(2);
}
2
3
4
5
6
7
8
相当于:
function bar() {
console.log(2);
}
var foo;
foo();
bar();
foo = function() {
console.log(1);
}
2
3
4
5
6
7
8
9
变量和函数都会提升,遇到函数表达式var foo = function(){}
时,首先会将var foo
提升到函数体顶部,然而此时的foo的值为undefined,所以执行foo()报错。而对于函数bar(),则是提升了整个函数,所以bar()能够顺利执行。上述例子表明:函数表达式定义的函数不会发生函数提升。
函数声明提升、参数、变量声明提升优先级
function test(bar) {
function bar() {
return '1';
}
console.log(bar);
var bar = 2;
// 后面的函数声明会覆盖前面的
function bar() {
return '2';
}
}
test(3);
2
3
4
5
6
7
8
9
10
11
12
输出结果如下:
ƒ bar() {
return '2';
}
2
3
优先级顺序:函数声明提升 > 参数 > 变量声明提升
代码分类
根据代码位置分为以下两类:
- 全局代码
- 函数代码
执行上下文(Execution Context)
什么是执行上下文
简而言之,执行上下文就是当前javaScript代码被解析和执行时所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文中运行。
特别注意:某个执行上下文中的代码执行完之后,该执行上下文就会被销毁,保存在其中的所有变量和函数也会随之销毁。但是全局执行上下文会等到应用程序退出(例如关闭网页或浏览器)时才会被销毁。
执行上下文只有在函数被调用的时候才会被创建。
执行上下文的类型
执行上下文总共有三种类型:
- 全局执行上下文:这是默认的、最基础的执行上下文,是最外层的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:
- 创建一个全局对象,在浏览器中这个全局对象就是window对象,所有的全局变量和最外层函数都是作为window的属性和方法创建的。
- 将this指针指向这个全局对象,一个程序中只能存在一个全局执行上下文。
- 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
- Eval函数执行上下文:运行在eval函数中的代码也获得了自己的执行上下文,但由于javascript开发人员不常用eval函数,所以在这里不再讨论。
执行上下文的生命周期
执行上下文的生命周期通常包括三个阶段:创建阶段 → 执行阶段 → 回收阶段,这里重点介绍创建阶段。
全局执行上下文
创建阶段
- 在执行全局代码前将window对象确定为全局执行上下文。
- 对全局数据进行预处理
- var定义的全局变量 ==> undefined(变量声明提升),添加为window的属性
- function声明的全局函数 ==> 赋值(函数声明提升),添加为window的方法
- this指向 ==> 赋值为window
执行阶段
开始执行全局代码。
来看个🌰:
console.log(a1, window.a1); // undefined undefined
fn();
console.log(this); // window
var a1 = 2;
function fn() {
console.log('fn()');
}
2
3
4
5
6
7
函数执行上下文
创建阶段
- 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
- 对局部数据进行预处理
- 形参变量 ==> 赋值(实参) ==> 添加为执行上下文的属性
- 初始化函数的参数arguments ==> 赋值(实参列表),添加为执行上下文的属性
- var定义的局部变量 ==> undefined(变量声明提升),添加为执行上下文的属性
- function声明的函数 ==> 赋值(函数声明提升),添加为执行上下文的方法
- this ==> 赋值(调用函数的对象)
执行阶段
开始执行函数体代码
回收阶段
执行上下文出栈等待虚拟机回收执行上下文。
注意: 对于函数执行上下文,其内部的代码执行完毕后,该执行上下文将被销毁,保存在其中的变量和函数也随之销毁,而全局执行上下文,需所有程序执行完毕或网页关毕后才会销毁。
来看个🌰:
function bar(a) {
console.log(a); // 3
console.log(b); // undefined
foo(); // 'foo()'
console.log(this); // window
console.log(arguments);
var b = 2;
function foo() {
console.log('foo()');
}
}
bar(3, 4);
2
3
4
5
6
7
8
9
10
11
12
执行上下文栈(Execution Context Stack)
每个程序中执行上下文可以存在若干个,每次调用函数都会创建一个新的执行上下文。JavaScript引擎通过执行上下文栈来管理所有的执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循后进先出(last-in-first-out)的原则。
- 在全局代码执行前,js引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
- 在函数执行上下文创建后,将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后,栈中只剩下window
从上面的流程图,我们需要记住几个关键点:
- JavaScript执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的js执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
再来看个🌰:
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
2
3
4
5
6
7
8
9
10
11
需要注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。
上述代码运行按照如下步骤:
- 当上述代码在浏览器中加载时,javaScript引擎会创建一个全局执行上下文并且将它推入当前的执行栈;
- 调用changeColor函数时,此时changeColor函数内部代码还未执行,js执行引擎立即创建一个changeColor的执行上下文(简称EC),然后把这执行上下文压入到执行栈(简称ECStack)中;
- 执行changeColor函数过程中,调用swapColors函数,同样地,swapColors函数执行之前也创建了一个swapColors的执行上下文,并压入到执行栈中;
- swapColors函数执行完成,swapColors函数的执行上下文出栈,并且被销毁;
- changeColor函数执行完成,changeColor函数的执行上下文出栈,并且被销毁。
为了巩固一下执行上下文的理解,我们再来绘制一个例子的演变过程,这是一个简单的闭包例子:
function f1() {
var n = 999;
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
2
3
4
5
6
7
8
9
因为f1中的函数f2在f1的可执行代码中,并没有被调用执行,因此执行f1时,f2不会创建新的上下文,而直到result执行时,才创建了一个新的执行上下文。具体演变过程如下:
总结
- Javascript是单线程
- 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待;
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈;
- 函数的执行上下文的个数没有限制,函数每被调用一次,就会产生一个新的执行上下文,即使是调用的自身函数,也是如此;
- 一个程序中的执行上下文有n+1个(n为函数调用的次数,1为全局执行上下文)。
参考文档
- 【译】理解 Javascript 执行上下文和执行栈
- 深入理解JavaScript执行上下文和执行栈
- 前端基础进阶(二):执行上下文详细图解
- 深入理解javascript原型和闭包(11)——执行上下文栈