1. JavaScript 运行机制

在说运行之前,我们需要明确几个概念

栈数据结构

在 JavaScript 中,没有严格意义的堆内存和栈内存。但是某些场景我需要了解栈数据结构,因为 JavaScript 的执行上下文就是借用了栈数据结构的存储方式。

栈的定义及介绍:

  • 栈是一种特殊的列表
  • 栈是一种高效的数据结构,比如我们日常生活中洗盘子,不必拿起来上面的盘子,洗下面的盘子,数据只能在栈顶删除或者增加,操作块
  • 栈被称为一种先进后出的数据结构
  • 插入新元素又称为进栈、入栈或压栈,从一个栈删除元素有称为出栈或退栈

下面看一下栈数据结构的简单实现

function Stack() {
    this.dataStore = [];  //保存栈内元素
    this.top = 0;  //标记课插入新元素的位置 栈内压入元素该变量变大 弹出元素 变量变小
    this.push = push; // 入栈操作
    this.pop = pop;  // 出栈操作
    this.peek = peek;  //返回顶元素
    this.clear = clear;  // 清空栈
    this.length = length;  //栈的长度
}
function push(element) {
    this.dataStore[this.top++] = element;
}
function pop() {
    return this.dataStore[--this.top]
}
    function peek() {
    return this.dataStore[this.top - 1]
}
function length() {
    return this.top;
}
function clear() {
    this.top = 0;
    this.dataStore=[];
}

堆数据结构

堆数据结构是一种树状结构。
好比书架,我们只要知道书的名字,就可以取到想要的书,好比在 JSON 的数据中,key-value 是可以无序的,我们只要知道 key,就可以拿到对应的 value。

队列

队列这个数据结构虽然暂时不会出现在 JavaScript 的运行机制中,但是后面的事件循环(Event Loop),我们会用到。
队列是一种先进先出的数据结构。好比一根水管,一端流入一端流出。
下面简单看一下 js 实现队列的简单代码:


function Queue() {

            this.dataStore = [];
            this.enqueue = enqueue;  //入队
            this.dequeue = dequeue;  //出对=队
            this.front = front;  //对首
            this.back = back;  //队尾
            this.isEmpty = isEmpty;
            this.toString = toString;
        }
        function enqueue(element) {
            this.dataStore.push(element);
        }
        function dequeue() {
            return this.dataStore.shift();
        }
        function front() {
            return this.dataStore[0];
        }
        function back() {
            return this.dataStore[this.dataStore.length - 1];
        }
        function isEmpty() {
            return this.dataStore.length === 0 ? true : false;
        }
        function toString() {
            var str = '';
            for (var i = 0; i < this.dataStore.length; i++) {
                str += this.dataStore[i] + '\n'
            }
            return str
        }

变量对象与数据基础类型

JavaScript 在执行上下文生成后,会创建一个叫变量对象的特殊对象(下面会详细说),JavaScript 的基本数据类型都会保存在变量对象上
基础数据类型都是简单的数据段,JavaScript 中,目前有 7 中基本数据类型,分别是 Null、Undefined、Number、String、Boolean、Symbol、BigInt。

引用数据类型与对内存

我们知道,在 JavaScript 中,引用数据类型都是存在堆内存当中的,引用类型的值是按引用传递访问的。这里的引用,其实举手变量对象中保存的地址,该地址指向堆内存中的实际内存。

执行上下文(EC)

每当 JavaScript 编译阶段结束,都会进入一个执行上下文,执行上下文(EC)也可以理解为当前代码的执行环境。具体执行机制,到下面会完整的列出。
既然是代码的执行环境,那么必定会有多个执行上下文,我们下面通过图例来看:


var color = 'blue';

function changeColor() {

    var anotherColor = 'red';

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;

    }

    swapColors();

}

changeColor();

这段代码,配合下面的图,我们会清楚的知道执行上下文是什么。
首先,第一步,我们知道,当代吗开始执行之前,会有一个全局上下文环境入栈,即 window

img

紧接着,可执行代码开始执行,将 changeColor 的执行上下文入栈

img

第三步,就是 swapColors 的执行上下文入栈

img

当 swapColors 执行完,开始出栈

img

我们知道先入后出原则,一次出栈

img

比较特殊的是,全局上下文并不会出栈,它会等到浏览器关闭后出栈

变量对象(VO)

上面的概念中,我们有两次提到了变量对象,那么变量到底是什么,变量对象译为 Variable Object(VO)
目前我们先看一下 VO 到底是什么,下面会说 js 的运行机制,一切就豁然开朗了
变量对象(VO)的创建过程:

  • 1.建立 arguments 对象,检查当前上下文中的参数,建立该对象下的属性及属性值
  • 2.检查当前上下文的函数声明,也就是我们经常看到的函数提升至上下文顶部
  • 3.检查上下文中的变量声明,也就是我们的说的变量提升,先声明,值为 undefined

VO 分为两个阶段:

  • 创建阶段
    • 创建作用域链(Scope Chain)
    • 创建变量,函数及参数
    • 定义 this,但是这里不会赋值
  • 执行阶段
    • 变量赋值
    • this 绑定

活动对象(AO)

什么是 AO
可以理解为 AO 就是 VO,当执行上下文是函数执行环境时,AO===VO,这里只要明白这个就够用了
我们具体看一下 VO 和 AO 这个特殊的对象长什么样


//以window为例
windowEC = {

    VO: Window,
    scopeChain: {},
    this: Window

}

下面看一个可执行代码的 AO/VO


function test() {

    console.log(foo);
    console.log(bar);

    var foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';

    }

    function foo() {
        return 'hello';

    }

}

test();

我们分别看一下创建阶段以及执行阶段


// 创建阶段
EC(test):{

    ScopeChain:{ Scope },

    AO = {
        arguments: {...},
        foo: <foo reference>,
        bar: undefined

    }

}
// 执行阶段
EC(test):{

    ScopeChain:{ Scope },

    VO = {

        arguments: {...},
        foo: 'Hello',
        bar: <bar reference>,
        this: Window

    }

}

JavaScript 执行过程

  • 编译阶段
    • 词法分析
    • 语法分析
    • 可执行代码生成
  • 执行阶段
    • 1.函数执行,先创建 EC,只有函数可以创建 EC,windowEC 特例,压栈 ECS
    • 2.EC 生成 VO/AO
      • 这里也存在一个小插曲,如果函数内没有 var 声明的变量赋值,是不会进入 AO 的,也就是说函数执行完,该变量不会销毁
    • 3.AO 创建及执行
    • 4.垃圾回收

函数执行栈(ECS)

其实上面已经说过了,我们来看一段代码就会知道


function func1(){

    console.log('func1');

}
function func2(){

    console.log('func2');

}
function func3(){

    console.log('func3');

}
func1();
//此时的EC:
ECStack:{
    func1,
    globalContext
}

2.Event Loop

我们想一下,如果函数式异步的,那么执行又是怎样的

  • 1.js 引擎遇到异步事件暂时挂起
  • 2.等到异步事件执行完毕,将事件加入事件队列中
  • 3.当事件队列执行完毕,压如 ECS
  • 4.执行回调同步代码

3.闭包

什么是闭包?
我的理解很短,闭包就是函数执行完毕,内部变量仍可以被外部访问。
可能说的太笼统,下面用一张图看一下
img

4.鬼畜的 JavaScript

此处借鉴于掘金的 块级作用域的默认变量
直接上代码以及注释吧

var a;
if(true){
a=5;
function a(){}
a=0;
console.log(a);   //0
}
console.log(a);  //5
//因为在块级作用域内,变量不会提升到全局作用域顶层,而是在运行后,反射到全局作用域
//而函数直接可以从块级作用域提升到全局作用域
console.log(a);  //  undefined
if(true){
    console.log(a,window.a);  // function a()  undefined
    function a(){}
    console.log(a);  //  functiona()
}
//重新说明,块级作用域和函数作用域完全不同,函数要等到执行完重写window,而块级作用域却不是,只会进行有意义的重写一次
//那么什么是有意义的重写,就是外层作用域没有声明及赋值
//在块级作用域,提升这些无异于其他作用域,函数提升到当前作用域顶层,提升到作用域顶层,并不代表会重写外层变量,是否重写,要看外层变量是否有实际存在意义
0