[TOC]

写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。Github地址

变量提升

变量声明提升

举个🌰:

var bar = 1;
function test() {
    console.log(bar); // undefined
    var bar = 2;
    console.log(bar); // 2
}
test();
1
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();
1
2
3
4
5
6
7
8

再看个🌰:

function test(bar) {
    console.log(bar); // 3
    var bar = 2; // 这里其实也存在变量声明提升
    console.log(bar); // 2
}
test(3);
1
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
1
2
3
4
5
6

两步走:

  • 代码预解析(将变量进行提升)
  • 代码执行

相当于:

var a;
console.log('a' in window);
if (!('a' in window)) {
    console.log(111);
    a = 1;
}
console.log(a);
1
2
3
4
5
6
7

函数和变量同名(同时提升)

foo();
var foo = 'foo';
function foo() {
    console.log('foo()');
}
// 输出:foo()
1
2
3
4
5
6

相当于:

function foo() {
    console.log('foo()');
}
var foo;
foo(); // 这里可以正常执行,因为函数声明提升优先级更高
foo = 'foo';
1
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);
}
1
2
3
4
5
6
7
8

相当于:

function bar() {
    console.log(2);
}
var foo;
foo();
bar();
foo = function() {
    console.log(1);
}
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);
1
2
3
4
5
6
7
8
9
10
11
12

输出结果如下:

ƒ bar() {
        return '2';
    }
1
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()');
}
1
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);
1
2
3
4
5
6
7
8
9
10
11
12

执行上下文栈(Execution Context Stack)

每个程序中执行上下文可以存在若干个,每次调用函数都会创建一个新的执行上下文。JavaScript引擎通过执行上下文栈来管理所有的执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循后进先出(last-in-first-out)的原则。

  1. 在全局代码执行前,js引擎就会创建一个栈来存储管理所有的执行上下文对象
  2. 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
  3. 在函数执行上下文创建后,将其添加到栈中(压栈)
  4. 在当前函数执行完后,将栈顶的对象移除(出栈)
  5. 当所有的代码执行完后,栈中只剩下window

从上面的流程图,我们需要记住几个关键点:

  1. JavaScript执行在单线程上,所有的代码都是排队执行。
  2. 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  3. 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  4. 浏览器的js执行引擎总是访问栈顶的执行上下文
  5. 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

再来看个🌰:

var color = 'blue';
function changeColor() {
    var anotherColor = 'red';
    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }
    swapColors();
}
changeColor();
1
2
3
4
5
6
7
8
9
10
11

需要注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。

上述代码运行按照如下步骤:

  1. 当上述代码在浏览器中加载时,javaScript引擎会创建一个全局执行上下文并且将它推入当前的执行栈;
  2. 调用changeColor函数时,此时changeColor函数内部代码还未执行,js执行引擎立即创建一个changeColor的执行上下文(简称EC),然后把这执行上下文压入到执行栈(简称ECStack)中;
  3. 执行changeColor函数过程中,调用swapColors函数,同样地,swapColors函数执行之前也创建了一个swapColors的执行上下文,并压入到执行栈中;
  4. swapColors函数执行完成,swapColors函数的执行上下文出栈,并且被销毁;
  5. changeColor函数执行完成,changeColor函数的执行上下文出栈,并且被销毁。

为了巩固一下执行上下文的理解,我们再来绘制一个例子的演变过程,这是一个简单的闭包例子:

function f1() {
    var n = 999;
    function f2() {
        alert(n);
    }
    return f2;
}
var result = f1();
result(); // 999
1
2
3
4
5
6
7
8
9

因为f1中的函数f2在f1的可执行代码中,并没有被调用执行,因此执行f1时,f2不会创建新的上下文,而直到result执行时,才创建了一个新的执行上下文。具体演变过程如下:

总结

  • Javascript是单线程
  • 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待;
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈;
  • 函数的执行上下文的个数没有限制,函数每被调用一次,就会产生一个新的执行上下文,即使是调用的自身函数,也是如此;
  • 一个程序中的执行上下文有n+1个(n为函数调用的次数,1为全局执行上下文)。

参考文档

  1. 【译】理解 Javascript 执行上下文和执行栈
  2. 深入理解JavaScript执行上下文和执行栈
  3. 前端基础进阶(二):执行上下文详细图解
  4. 深入理解javascript原型和闭包(11)——执行上下文栈

评 论: