Js 作用域、执行上下文、变量对象

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

JavaScript作用域

作用域(Scope)

作用域的基本定义

JavaScript权威指南p56

  • 一个变量的作用域(Scope)是程序源代码中定义这个变量的区域。

  • Scope 就是范围的意思,那么变量的作用域就是变量的“范围”,字面意思说就是变量能够在哪些“范围”来起作用(或者被访问)。

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

这里使用书上的例子

var scope = "global"; // 声明一个全局变量

function checkScope(){
    var scope = 'local'; // 声明一个同名的局部变量
    return scope // 返回局部变量的值,而不是全局变量的值
}

console.log(checkScope()); // local
复制代码

为了解读上面的代码就得抛出全局作用域和局部作用域的概念了。

全局作用域和局部作用域

函数外部定义的scope变量的作用域就是全局作用域,在JavaScript代码中的任何地方都是有定义的。函数内部声明的scope变量的作用域就是局部作用域,在函数内部才有定义。

这里checkScope函数返回scope首先在函数作用域查找scope变量,如果没有在去全局作用域去查找。自然先找到局部作用域(函数作用域)下的scope变量。

静态作用域与动态作用域

JavaScript采用的是静态作用域。

静态作用域(词法作用域)和动态作用域的核心区别就是,函数作用域是在什么时候确定的。

静态作用域是在函数定义的时候就确定了,由字面意思就是,一旦函数定义了,函数的作用域就确定无法改变了。动态作用域是函数调用的时候才确定。由字面意思来说就是函数调用位置不同,函数作用域也就不同。

var value = 1;

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

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

bar(); // 1

// 结果是 ???
复制代码

假设JavaScript采用静态作用域,让我们分析下执行过程:

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

看下面2个例子

f()函数定义的位置不同最终返回的值也不同

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
console.log(checkscope()); // local scope
复制代码

这里函数f()定义在函数checkscope()函数中,当执行函数f()时,返回scope时自然去定义函数的位置找到,先从 f()函数内部查找是否有局部变量 scope,如果没有,就根据书写的位置,查找上面一层的代码,也就是 scope = "local scope"

var scope = "global scope";
function f(){
    return scope;
}
function checkscope(){
    var scope = "local scope";
    return f();
}
console.log(checkscope()); // global scope
复制代码

这里函数f()定义在函数checkscope()函数中,当执行函数f()时,返回scope时自然去定义函数的位置找到,先从 f()函数内部查找是否有局部变量 scope,如果没有,就根据书写的位置,查找上面一层的代码,也就是 scope = "global scope"

这2个例子进一步证明JavaScript使用的是静态作用域。

执行上下文栈

可执行代码

JavaScript有3种可以执行代码:全局代码、函数代码、eval代码。

在执行这些可执行代码的时候会进行“准备工作”,也就是创建执行上下文。

为了方便管理众多的执行上下文,就设计了一个执行上下文的栈。

执行上下文的推入弹出

首先执行全局代码,创建一个全局执行上下文,并推入执行上下文栈中,只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个全局执行上下文。

对应执行一个函数时,会创建一个函数执行上下文,并将这个函数执行上下文推入执行上下文栈中,等函数执行完毕之后就会弹出栈外。

变量对象

执行函数时会创建一个执行上下文,那么这个执行上下文就有3个属性(变量对象(Variable object,VO)、作用域链(Scope chain)、this),其中一个就是变量对象。

变量对象概念

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

全局上下文的变量对象

全局上下文的变量对象是全局对象

全局对象

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

函数上下文的变量对象

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

活动对象和变量对象是一个东西,在进入一个执行上下文时,这个执行上下文对应的变量对象被激活,就改名叫活动对象了,也就可以访问活动对象身上的属性了。

活动对象创建时间

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

执行过程

1.进入执行上下文(变量对象的初始化)

进入执行上下文的时候,会创建一个活动对象,根据三大规则进行初始化。

2.执行代码(变量对象的加工)

当执行代码的时候,会顺序执行代码,根据代码,修改变量对象的值。

变量对象内容

  • 函数的所有形参 (如果是函数上下文)

由名称和对应值组成的一个变量对象的属性被创建

没有实参,属性值设为 undefined

  • 函数声明

由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建

如果变量对象已经存在相同名称的属性,则完全替换这个属性

  • 变量声明

由名称和对应值(undefined)组成一个变量对象的属性被创建;

如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

作用域链

作用域链是如何创建和变化

函数定义时

函数定义时,会将不完整作用域链保存在函数内置属性[[scope]]内

当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

执行函数时(函数激活,进入执行上下文那个时刻)

进入执行上下文,创建变量对象,并激活,进AO推进[[scope]]保存的作用域链的前端。

一个函数从定义到执行完毕,涉及执行上下文,变量对象,作用域链等等的过程

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

1.执行全局代码,创建一个全局执行上下文,并推入执行上下文栈中,全局执行上下文有一个属性是全局对象。

2.checkscope函数定义时,创建一个静态作用域,函数的内置属性[[scope]]会保存一个作用域链,里面保存了所有父级变量对象。

3.执行checkscope函数时,会创建一个执行上下文,将其推入执行上下文栈中。

4.不会立刻执行函数,会对执行上下文的属性进行加工和创建

一、将函数内置属性[[scope]]保存的不完整作用域链,作为作用域链的属性值保存到执行上下文身上。

二、创建一个变量对象并激活为活动对象,这里只是对活动对象的初始化。

三、在初始化之后,将活动对象推入作用域链前端。
复制代码
  1. 正式开始执行checkscope函数,顺序执行代码,根据代码,修改活动对象的值。

  2. 查找到 scope2 的值,返回后函数执行完毕数之后,将函数执行上下文弹出执行上下文栈,进行销毁。

简单描述上面的过程

  • 执行全局代码,创建全局执行上下文,压入执行上下文栈,全局上下文保存了全局对象,

  • 定义checkscope函数,确定静态作用域,内置属性[[scope]]被赋值不完整作用域链,

  • 执行checkscope函数,创建函数执行上下文,推入执行上下文栈,将[[scope]]保存的作用域链保存到函数执行上下文上,创建初始化活动对象,将活动对象推入作用域链,

  • 顺序执行代码,加工活动对象,

  • 函数执行完毕,函数执行上下文弹出执行上下文栈,

注意:

  • 函数执行上下文,活动对象都是在创建之后先压入对应的执行上下文栈,作用域链内,在进行一系列加工。

  • 活动对象不会弹出作用域链,执行上下文会弹出栈。

  • 定义函数时会保存不完整作用域链。

  • 执行上下文的创建,活动对象的创建,作用域链的创建都是在执行可执行代码时创建的

猜你喜欢

转载自juejin.im/post/7107537430793682951
今日推荐