执行上下文
JavaScript 是顺序执行代码程序的。
function foo() {
console.log('嘿嘿');
}
foo(); // '嘿嘿'
var myName = 'heihei';
myName; // ‘heihei’
JavaScript 的可执行代码有哪几种?
- 全局代码
- 函数代码
- eval 代码
当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。
调用栈
调用栈就是用来管理函数调用关系的一种数据结构。
function foo() {
console.log('嘿嘿');
}
foo(); // '嘿嘿'
在执行函数 foo 之前,JavaScript 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量。
接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?
JavaScript 引擎通过利用栈结构来管理执行上下文。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中。
var a = 'heihei1';
function foo() {
var b = 'heihei2';
}
function bar() {
var c = 'heihei3';
foo();
}
bar();
第一步,创建全局上下文,并将其压入栈底
接下来,开始执行全局代码。
执行 foo 函数调用,创建函数上下文,压栈。
接下来,开始执行 foo 函数代码。
执行到 foo() ,开始创建 foo 上下文,压栈。
执行 foo 函数。
当 foo 函数返回时,该函数的执行上下文就会从栈顶弹出。
当 bar 函数返回时,该函数的执行上下文就会从栈顶弹出。
至此,整个 JavaScript 流程执行结束了。调用栈是 JavaScript 引擎追踪函数执行的一个机制
foo();
myName;
var myName = 'heihei';
function foo() {
console.log('嘿嘿');
}
变量提升
按照我们正常的从上到下执行来说,我们的正常逻辑应该会这样认为:
- 执行函数 foo ,找不到 foo 就报错。
- 执行 myName ,找不到就报错。
但是确实打印出了 “undefined” 和 “嘿嘿”。
所以我们可以得出一个结论。
- 变量定义之前就使用它,该值为 undefined。
- 函数定义之前使用它,会正常执行函数。
var myName = ‘heihei’;做了下面的事情。
function foo() {} 可以看作为:
变量提升:
是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
JavaScript 代码的执行流程
即使存在提升,但是变量和函数在代码里的位置是不会改变的,而是在 编译阶段
被 JavaScript 引擎放入内存中。
第一部分:变量提升部分的代码。
第二部分,可执行代码部分
执行上下文
执行上下文是 JavaScript 执行一段代码时的运行环境。每个执行上下文,都有三个重要的属性:
- 变量环境。
- 词法环境。
- 可执行代码。
- 作用域链。
- this。
变量环境。
当进入执行上下文时,这时候还没有执行代码。
变量对象会包括:
- 函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建。
- 没有实参,属性值设为 undefined
- 函数声明。
- 由名称和对应值组成一个变量对象的属性被创建。
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性。
- 变量声明
- 由名称和对应值 (undefined),组成一个变量对象的属性被创建。
- 如果变量名称根已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
JavaScript 会把声明以外的代码编译为字节码
(可执行的代码)。
foo();
myName = 'heihei';
执行阶段
JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。
看一个更加完整的变量环境。
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在进入执行上下文之后:
代码执行:
词法环境
在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。
变量提升所带来的问题
- 变量容易在不被察觉的情况下被覆盖掉
- 本应销毁的变量没有被销毁
ES6 是如何解决变量提升带来的缺陷,ES6 引入了 let 和 const 关键字。
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
接下来我们就来一步步分析上面这段代码的执行流程。
第一步是编译并创建执行上下文
接下来,第二步继续执行代码
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶,当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。
变量查找过程
当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级
)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
var myName = 'heihei1';
function foo() {
console.log(myName);
}
function bar() {
var myName = 'heihei2';
foo();
}
bar();
上面代码调用栈如下:
如果按照调用栈的顺序来查找变量,那么打印出来的myName = ‘heihei2’;
但实际情况并非如此,打印出来的应该是 ‘heihei1’;
作用域链
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。
比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。
我们可以看出来,如果在 bar 函数或者 foo 函数中使用了外部变量,JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。
foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?
词法作用域:
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
var myName = 'heihei1';
function foo() {
var myName = 'heihei2'
function bar() {
var myName = 'heihei3';
}
}
foo();
闭包
var myName = 'heihei1'
function foo() {
var myName = 'heihei2';
function bar() {
console.log(myName);
}
return bar;
}
var tmp = foo();
tmp();
我们可以看到 bar 函数访问了 foo 函数的 myName 属性,根据词法作用域的规则,内部函数总是可以访问外部函数的变量的。所以当 foo 函数返回给全局变量 tmp 的 bar 函数时,虽然 foo 函数已经执行结束,但是还是引用着 foo 的变量。
当我们执行 tmp() 的时候
JavaScript 引擎会沿着,当前执行上下文 —> foo 函数闭包 ----> 全局执行上下文 来查找 myName ,所以 myName = ‘heihei1’
闭包是怎么回收的
如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。