JavaScript作用域,执行上下文和闭包


内容是对畅喵大佬和冻伢大佬所写博客的总结和自己的一点点体会哈哈哈哈 冲!

作用域


定义

1.作用域是指程序源代码中定义变量的区域

2.作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限

词法作用域和动态作用域


JavaScript采用的是词法作用域 (也称为静态作用域),函数的作用域在函数定义的时候就确定了。

与词法作用域相对立的是动态作用域,函数的作用域是在函数调用时才确定的。

一个例子:

var a = 1;

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

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

bar(); //其结果是1

1.如果JavaScript采用的是词法作用域时,起执行过程为:

执行foo函数,先在foo函数内部查找是否有局部变量a,没有的话则根据书写的位置,查找其上面一层的代码即 (var a = 1) , 所以会打印1

2.如果JavaScript采用的是动态作用域时,其执行过程为:

执行foo函数,先在foo函数内部查找是否有局部变量a,没有的话则从调用foo函数的bar函数作用域中去寻找a变量,查找其上面一层的代码即 (var a = 1) , 所以会打印2

结论:JavaScript采用的词法作用域

执行上下文


定义:执行上下文是评估和执行JavaScript代码的环境的抽象概念。每当JavaScript代码在运行的时候,它都是在执行上下文中运行

JavaScript执行顺序

JavaScript代码在执行时,JavaScript引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”。那么会进行“准备工作”的每段代码是如何划分的?

可执行代码

JavaScript的可执行代码一共有三种,分别是:

1.全局代码
2.函数代码
3.eval代码 (不常用)

举个例子,当执行到一个函数时,就会进行“准备工作”,这里的“准备工作”更专业一点来说,就叫做“执行上下文(execution context)” 这里即叫做函数执行上下文。

举个例子来说明一下全局代码和可执行代码 ~ :

全局代码

let str = 'hello world';
function foo() {
    
    
  // 函数体中的代码不算全局代码
}
console.log(str); // hello world

函数代码

function foo() {
    
    
  console.log('I like JavaScript'); // 函数代码
  console.log('hello world'); // 函数代码
}

执行上下文栈

代码中的函数有很多,那么我们应该如何管理这么多的执行上下文?

JavaScript引擎创建了执行上下文栈(Execution context stack,ECS)来管理所创造的执行上下文

下面将举例子来模拟执行上下文栈的行为 ~ :

1.为了模拟执行上下文栈的行为,定执行上下文栈是一个数组:

ECStack = [];

2.当JavaScript开始执行解释代码的时候最先遇到是全局代码,所以在初始化的时候会向执行上下文栈中压入一个全局执行上下文,我们用globalContext来表示它,并且在程序结束之前,这个全局执行上下文将一直存在。

ECStack = [
    globalContext
];

现在我们将就下面这段代码块来模拟执行上下文的行为 ~ :

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

function fun2() {
    
    
    fun3();
}

function fun1() {
    
    
    fun2();
}

fun1();

当执行一个函数的时候,就会创造一个上下文并压入栈中,并且当函数执行完毕后,函数的执行上下文将从栈中剔除。

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中调用了fun2 还需要创造fun2的执行上下文
ECStack.push(<fun2> functionContext);

// fun2中调用了fun3 还需要创造fun3的执行上下文
ECStack.push(<fun3> functionContext);

// fun3执行完毕
ECStack.pop();

// fun2执行完毕
ECStack.pop();

// fun1执行完毕
ECStack.pop();

创建执行上下文

对于每个执行上下文,都有三个重要属性:

1.变量对象(Variable object,VO)
2.作用域链(Scope chain)
3.this

变量对象

定义:变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明

全局上下文

全局上下文的变量对象就是全局对象window(浏览器)

函数上下文

在函数上下文中,我们用活动对象(activation object,AO)来表示变量对象(VO)

活动对象(activation object,AO):未进入执行阶段之前,变量对象(VO)中的属性都不能访问,但是进入执行阶段之后,变量对象(VO)转变为活动对象(AO),里面的属性都能被访问,然后开始进行执行阶段的操作。VO和AO是同一个对象在不同生命周期时的不同的名字

执行上下文的执行过程

执行上下文的代码会分为两个阶段进行处理:

1.进入执行上下文
2.代码执行

举个例子来说明AO在不同阶段的变化:

function foo(a) {
    
    
  var b = 2;
  function c() {
    
    }
  var d = function() {
    
    };
}

foo(1);
1.进入执行上下文

变量对象会包括:

1.函数的所有形参
2.函数声明
3.变量声明

此时的AO:

AO = {
    
    
    arguments: {
    
     // aruguments是一个对应传给函数的参数的类数组对象
        0: 1,    //arguments[0] = 1
        length: 1  // arguments.length = 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){
    
    },
    d: undefined
}
2.代码执行

此时的AO:

AO = {
    
    
    arguments: {
    
    
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c() {
    
    },
    d: reference to FunctionExpression "d"
}
练习
function foo() {
    
    
    console.log(a);
    a = 2;
}

foo();  // a is not defined

function bar() {
    
    
    a = 2;
    console.log(a);
}

bar();  // 2


//两端代码执行console的时候 AO:
AO = {
    
    
    arguments: {
    
    
        length: 0
    }
}
函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。
但是由于第二段代码 a = 2; 位于 consol.log(a)之前,所以在执行console的时候全局变量a = 2

作用域链

定义:当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

[[scope]]内部属性,当函数创建的时候,就会保存所有父变量到其中,可以理解为一个函数的[[scope]]就是所有父级变量对象的层级链

举个例子 ~ :

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

上面各自的[[scope]]为:

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

bar.[[scope]] = [
    fooContexts.AO,
    globalContexts.VO
];

让我们举个例子来细细捋一下函数执行上下文中作用域链和变量对象的创建过程:

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

其执行过程如下:

1.创建checkscope函数,保存其作用域链到内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO // 全局代码的变量对象
];

2.执行checkscope函数,创建checkscope函数的执行上下文,并将checkscope函数的执行上下文压入栈中

ECStack = [
    checkscopeContext, //checkscope函数执行上下文
    globalContext //全局执行上下文
];

3.checkscope函数并不立即执行,开始做准备工作,第一步:复制函数scope属性创造作用域链

checkscopeContext = {
    
    
    Scope: checkscope.[[scope]], //将第一步函数的scope属性复制,创造作用域链
}

4.第二步:用arguments创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明 ( 即上下文执行过程中的进入执行上下文 )

checkscopeContext = {
    
    
    AO: {
    
    
        arguments: {
    
    
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

5.将活动对象压入checkscope作用域顶端( 即函数激活 )

checkscopeContext = {
    
    
    AO: {
    
    
        arguments: {
    
    
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, globalConext.[[Scope]]]
}

6.开始执行函数,并修改AO的属性值

checkscopeContext = {
    
    
    AO: {
    
    
        arguments: {
    
    
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, globalContext.[[Scope]]]
}

7.函数执行完毕 从执行上下文栈中弹出

ECStack = [
    globalContext
];

闭包

定义:闭包是指有权访问另一个函数作用域中的变量的函数

举例说明 ~

var data = [];

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

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

在讲闭包之前首先让我们分析一下这一段代码

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

var foo = checkscope();
foo();

执行过程如下:

1.遇到全局代码,创建全局上下文,并将全局上下文压入上下文栈

ECStack = [
    globalContext
];

2.全局上下文初始化

globalContext = {
    
    
        VO: [global],
        Scope: [globalContext.VO],
    }

3.初始化的同时,创建checkscope函数,保存其作用域链到函数的内部属性[[scope]]

checkscope.[[scope]] = {
    
    
   globalContext.VO
}

4.执行checkscope函数,创建checkscope函数执行上下文,checkscope执行上下文被压入执行上下文栈

ECSstack = [
     checkscopeContext,
     globalContext
]

5.checkscope函数执行上下文初始化

  1. 复制函数[[scope]]属性创建作用域链
checkscopeContext = {
    
    
     scope:checkscope.[[scope]]
}
  1. 用arguments创建活动对象,并初始化活动对象,加入形参,函数声明,变量声明
checkscopeContext = {
    
    
        AO: {
    
    
            arguments: {
    
    
                length: 0
            },
            scope: undefined,
            f: reference to function f(){
    
    }
        },
    }
  1. 将活动对象压入checkscope作用域顶端
checkscopeContext = {
    
    
        AO: {
    
    
            arguments: {
    
    
                length: 0
            },
            scope: undefined,
            f: reference to function f(){
    
    }
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }

6.f函数被创建,保存作用域到f函数的内部属性[[scope]]

fscope.[[scope]] = {
    
    
     checkscopeContext.AO, globalContext.VO
}

7.f函数初始化

  1. 复制函数[[scope]]属性创建作用域
  2. 用arguments创建活动对象,同时初始化活动对象,加入形参,变量声明,函数声明
  3. 将活动对象呀如f作用域链顶端
fContext = {
    
    
        AO: {
    
    
            arguments: {
    
    
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
    }

8.f函数执行,沿着作用域链寻找scope

9.f函数执行完毕,从执行上下文栈中弹出

ECSstack = [
     checkscopeContext,
     globalContext
]

10.checkscope函数执行完毕,从执行上下文栈中弹出

ECSstack = [
     globalContext
]

猜你喜欢

转载自blog.csdn.net/Horizonar/article/details/109912192