JavaScript之执行上下文/作用域/闭包

92 阅读6分钟

执行上下文

执行顺序

js代码是按上下顺序执行的,但不是一行一行执行,而是一段段代码分析执行,当执行一段代码的时候 就会碰到一些事情,比如:1. 变量提升 2. 函数提升 等问题

function foo () {
    console.log('foo1')
}

foo() // foo2

function foo () {
    console.log('foo1')
}

foo() // foo2

可执行代码(executable code)

一段段代码首先要判断是否可以执行 js 中有一下代码类型

  1. 全局代码
  2. 函数代码
  3. eval 代码 遇到这些代码 就会进入”准备工作“ 中,专业名字叫执行上下文

执行上下文

代码繁多杂乱 js如何管理这么多的执行上下文?

// 数组来模拟执行上下文栈的行为
ESCtack = [];

// JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,初始化的时候向push一个全局的globalContext
// 只有当整个应用程序结束之后,globalContext才会被清空
ESCtack = [globalContext];

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

// 执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,函数执行完毕,就会将函数的执行上下文从栈中弹出
// 假设

// func1()
ECStack.push(<fun1> functionContext);
// fun1调用了func2 
ECStack.push(<fun2> functionContext);
// fun2又调用了func3
ECStack.push(<fun3> functionContext);

// func3 执行完毕
ECStack.pop()
// func2 执行完毕
ECStack.pop()
// func1 执行完毕
ECStack.pop()

// 当前执行上下完毕 接着执行后续的代码,但是globalContext永远存在 知道js运行结束


作用域

作用域是指代码定义变量的区别

规定了如何查找变量,确定了当前执行的代码能否访问到这个变量

js是静态作用域

静态作用域和动态作用域

静态作用域: 定义时决定 动态作用域: 调用时决定

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 静态时
// 执行foo--> foo查找局部变量value ---> 没有就按照书写位置找上一层 ---> 有value 打印1

// 动态时
// 执行foo--> foo查找局部变量value ---> 没有就按照调用函数的作用域找 ----> foo 在 bar函数中调用 ---> bar函数中有value ---> 打印2

作用域链

js 查找变量时,会从当前执行上下文中找,找不到会一层一层往上找,一直到全局对象,这个链表叫作用域链

函数创建

函数的作用域在函数定义的时候决定了。

函数有一个内部属性[scope]。函数创建的时候,会保存所有父变量对象到里面,是所有父变量对象的层级链

 
function foo() {
    function bar() {
        ...
    }
}

// 函数创建的时候

foo.[[scope]] = [
    globalContext.VO
];

BAR.[[SCOPE]] = [
        fooContext.AO,
        globalContext.VO
]

函数激活

函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域的前端。

这时候执行上下文的作用域链,我们命名为 Scope

Scope = [AO].concat([[scope]])

举个例子

实在看不明白 我举个例子

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

// 执行过程:
// 1.checkscope 函数被创建,作用域保存到scope 中

checkscope.[[scope]] = {
    globalcontext.vo
}

// 2.执行checkscope函数,创建执行上下文,checkscope函数进入栈中
ECStack = [
    checkscopeContext,
    globalContext
];

// 3.checkscope函数不会立即执行,开始准备工作。
// 第一步复制函数[[scope]]属性创建作用域链
checkscopeContext = {
    scope: checkscope.[[scope]],
}

// 第二步,用arguments创建活动对象。随后初始化对象,加入参数,函数和变量的声明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

// 第三步: 将活动搞对象压入checkscope 作用域顶端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

// 做完准备工作,开始执行函数,函数执行,修改ao的属性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

// 找到scope2 的值,返回后函数执行完毕,当前函数的上下文从执行栈中删除

ECStack = [
    globalContext
];

闭包

闭包是指那些能够访问自由变量的的函数

自由变量

函数中使用的,但是既不是函数的参数,也不是函数局部变量的变量

闭包由两部分组成:函数 + 函数能访问的自由变量

var a = 1;

function foo() {
   console.log(a);
}

foo();

// foo 可以访问a 但是a不是foo的局部变量,也不是foo的参数,a时自由变量

// 那么,函数foo + foo 访问的自由变量a就构成了一个闭包

从技术的角度讲,所有的JavaScript函数都是闭包。

理论上: 因为函数都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

实际上:

  • 即使创建它的上下文已经销毁,它仍然存在(内部函数从父函数中返回)
  • 代码中引用了自由变量

分析

// 还是你
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();


首先再来复习一遍执行过程:

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  6. 执行f函数,创建f上下文,压入执行上下文栈
  7. 上下文初始化,创建变量对象、作用域等
  8. f执行完毕,从栈中弹出

那么有个问题: f 执行函数的时候。checkscope的上下文已经被销毁,从栈中弹出,怎么会读取到scope的值?

因为作用域链

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

因为这个作用域链,f 依然可以读到checkscopeContext.AO的值,当函数f 引用了checkscopeContext.AO的值的时候,即使checkscopeContext被销毁,但是js依然能让checkscopeContext.AO存活在内存中,f函数依然能通过作用域链找到,所有有了闭包

一个例子

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案都是3

// data[0]之前,全局的vo 如下
 globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

// 执行到data[0]的时候

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data0没有i值,会从全局找 i等于

改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

// 执行函数0的时候,作用域链发生改变
data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

全局上下文的ao改成
匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}