js 作用域 作用域链

作用域

作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

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

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

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

上面两段代码输出结果都是 local scope
原因也很简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。
引用《JavaScript权威指南》的回答就是:

JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效

作用域有上下级吗?

作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。

function foo(a) {
    var b = a * 2;
    
    function bar(c) {
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2); // 2, 4, 12

在这个例子中有三个逐级嵌套的作用域。其中
①是全局作用域;
②是foo函数作用域;
③是bar函数作用域;
即是在foo作用域下创建了bar函数,那么“foo作用域”就是“bar作用域”的上级。
在这里插入图片描述

从【自由变量】到【作用域链】

自由变量

在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。
举个例子:

var a = 10;
function foo () {
	var b = 20;
	console.log(a + b);		// 这里的a在这里就是一个自由变量
}
foo();

如上程序中,在调用foo()函数时,取b的值就直接可以在foo作用域中取,因为b就是在这里定义的。而取a的值时,就需要到另一个作用域中取。到哪个作用域中取呢?
如果是到父作用域中取,其实有时候这种解释会产生歧义。例如:

var a = 10;
function fn () {
	console.log(a);
}
function foo(fn) {
    var a = 20;
    
    
    (function(){
    	fn();	
	})()
}
foo(fn); // 10

所以:

要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”——这就是所谓的“静态作用域”。

总结一下取自由变量时的这个“作用域链”过程:(假设a是自由量)

第一步,现在当前作用域查找a,如果有则获取并结束。如果没有则继续;

第二步,如果当前作用域是全局作用域,则证明a未定义,结束;否则继续;

第三步,(不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;

第四步,跳转到第一步。

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。

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

结构

作用域链本质上是一个指向变量对象的指针列表(在文中我们使用数组表示),它只引用但不实际包含变量对象。作用域链的前端始终都是当前执行上下文的变量对象,如果这个执行上下文属于函数执行上下文,则用活动对象作为变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象

函数创建

函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

举个例子:

var a = 10;
function foo(a) {
    var b = a * 2;
    
    function bar(c) {
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2);

全局阶段

首先在执行全局代码前,我们会先创建全局上下文。创建全局上下文的第一步是创建全局变量对象,然后将全局变量对象放入作用域链的顶端(执行上下文中的[[Scope]]属性指向作用域链)
在这里插入图片描述

此时全局上下文中的[[Scope]]属性可以这样表示

globalContext.[[Scope]] = [
    globalContext.VO
];

注意在创建完作用域链后,JavaScript 引擎还做了另一件事,这也是实现作用域链的最关键的一步,它会为变量对象中的所有函数添加一个[[Scope]]属性,而这个属性的值就是我们刚才介绍的全局上下文中的[[Scope]]属性值。如下:

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

函数阶段

全局上下文创建后,开始执行代码,根据代码动态的修改变量对象中的属性,当我们执行到foo时,让我们来看一看在函数阶段是如何创建作用域链的。

首先在执行foo之前,我们会为函数创建对应的执行上下文。注意重点来了函数上下文首先会复制函数的[[Scope]]属性用来创建作用域链,然后用 arguments 创建活动对象,最后再将活动对象压入作用域链顶端,如下所示:
在这里插入图片描述
执行 foo 函数,创建 foo 函数执行上下文,foo 函数执行上下文被压入执行上下文栈

ECStack = [
  fooContext,
  globalContext
];

foo 函数执行上下文初始化:

  1. 复制函数 [[scope]] 属性创建作用域链,
  2. 用 arguments 创建活动对象,
  3. 初始化活动对象,即加入形参、函数声明、变量声明,
  4. 将活动对象压入 foo 作用域链顶端。

同时 bar 函数被创建,保存作用域链到 bar 函数的内部属性[[scope]]

fooContext = {
  AO: {
      arguments: {
      	  0: 2,
          length: 1
      },
      b: undefined,
      bar: reference to function bar(){}
  },
  Scope: [AO, globalContext.VO],
  this: undefined
}

执行 bar 函数,创建 bar 函数执行上下文,bar函数执行上下文被压入执行上下文栈

ECStack = [
  barContext,
  fooContext,
  globalContext
];

bar 函数执行上下文初始化, 以下跟foo 函数执行上下文初始化相同:

  1. 复制函数 [[scope]] 属性创建作用域链,
  2. 用 arguments 创建活动对象,
  3. 初始化活动对象,即加入形参、函数声明、变量声明,
  4. 将活动对象压入 bar 作用域链顶端。
barContext = {
  AO: {
      arguments: {
          0: 12,
          length: 1
      }
  },
  Scope: [AO, fooContext.AO, globalContext.VO],
  this: undefined
}

bar函数执行,沿着作用域链查找 a, b 值
bar函数执行完毕,bar函数上下文从执行上下文栈中弹出

ECStack = [
  fooContext,
  globalContext
];

foo 函数执行完毕,foo 执行上下文从执行上下文栈中弹出

ECStack = [
  globalContext
];

总结作用域链是如何创建:

  1. 全局上下文阶段,创建全局对象。

  2. 将全局对象压入作用域链

  3. 为全局对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)

  4. 每一个函数上下文阶段,复制函数的[[Scope]]属性,创建作用域链

  5. 创建活动对象,并用 arguments 创建活动对象

  6. 将活动对象压入当前上下文中的作用域链

  7. 为活动对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)

扩展作用域链

虽然执行上下文的类型总共只有两种———全局和局部(函数),但还是有办法延长作用域链的。这么说是因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。有两种情况会发生这种现象,如下:

  • try-catch 语句中的 catch 块
    当 catch 语句执行时,会创建一个新的变量对象,其中包含了被抛出的错误对象的声明,然后将这个变量对象压入当前上下文作用域链中。
  • with 语句
    当 with 语句执行时,会将 with () 中指定的对象压入当前上下文作用域链中。
function outer(){
    var number  = 1;

    with(location){

    }
}

此时会将 location 对象添加到当前上下文作用域链的顶端。

参考资料
https://github.com/mqyqingfeng/Blog/issues/6
https://www.cnblogs.com/wangfupeng1988/p/3977924.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值