js - 作用域链

1,389 阅读4分钟

作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

当前代码运行的环境,可访问的变量以及作用域链上的变量环境对象。

标识符解释

所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成:

  • 第一个字符必须是一个字母、下划线(_)或美元符号($);
  • 剩下的其他字符可以是字母、下划线、美元符号或数字。

标识符中的字母可以是扩展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符,如 À 和 Æ(但不推荐使用)。

执行上下文

每一个执行上下文对应三个对象,分成两个不同的阶段:

创建

  • this
  • 变量对象 【variable object】

执行

  • this
  • 变量对象 =》活动对象 【activation object】
  • 作用域链 【scope chain】

作用域链

var color = "blue";

function changeColor() {
  let anotherColor = "red";

  function swapColors() {
    let tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
  }
  
  swapColors();
}

changeColor();

全局上下文执行

GlobalContext: {
  ...window,
  color: 'blue',
  changeColor: fn
}

于此同时:changeColor函数的执行上下文就是GlobalContext。

changeColor调用

当前的执行上下文为:

changeColorContext: {
  'activation object': {
      anotherColor: 'blue',
      swapColors: fn
  },
  'scope chain': [changeColorContext => GlobalContext]
}

swapColors调用

当前的执行上下文为:

swapColorsContext: {
  'activation object': {
      tempColor: 'blue'
  },
  'scope chain': [swapColorsContext => changeColorContext => GlobalContext]
}

这里的color不属于当前当前执行上下文的活动对象,而是属于作用域链上GlobalContext的值。anotherColor则属于changeColorContext的。

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符


一道面试题

比较下面两段代码,试述两段代码的不同之处
// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
      debugger
        return scope;
    }
    return f();
}
checkscope();

// B---------------------------
var scope = "global scope";

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

A的ECS

  1. 主流程:【GlobalContext】
  2. checkscope执行:【checkscopeContext => GlobalContext】
  3. 第9行代码执行:【fContext => checkscopeContext => GlobalContext】
  4. 第9行return语句:【checkscopeContext => GlobalContext】
  5. checkscope执行完:【GlobalContext】

B的ECS

  1. 主流程:【GlobalContext】
  2. checkscope执行:【checkscopeContext => GlobalContext】
  3. 第9行return语句:【checkscopeContext => GlobalContext】
  4. checkscope执行方法调用:【fContext => GlobalContext】
  5. checkscope执行完:【GlobalContext】


但是这里为什么是“local scope”
所以这里上面两种写法返回的代码结果都是:"local scope",因为js是静态作用域,因为f函数创建的时候变量对象内部的scope就是"local scope"。

dmitrysoshnikov.com/ecmascript/…

in the further explanation we’ll mainly use the concept of an environment, rather than scope, 

通过环境的概念来代替scope。那scope如何和环境变量对应。个人理解如下:

scope这里指的是 outer指向父环境的变量,一直到global。执行栈ECS和当前执行上下文没有必然的联系,唯一有关系的是this指向

image.png


环境概念代替scope下的执行上下文

执行上下文【具体会在下个文章学习

  1. this 值的决定,即我们所熟知的 This 绑定。
  2. 创建词法环境组件。
  3. 创建变量环境组件。

作用域链增强

  • try/catch 语句的 catch 块
  • with 语句


这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。

let name = 'rod';
let name1 = 'rod';

function testWith() {
  let withObject = {
    name: 'rodchen',
    age: '15'
  }

  with(withObject) {
    console.log(name)
    console.log(name1)
  }
}

testWith()

image.png

with语句使用var创建变量,则归属于所属函数。使用let / const,则属于block作用域。

let name = 'rod';
var name1 = 'rod';

function testWith() {
	let withObject = {
    name: 'rodchen',
    age: '15'
  }

  with(withObject) {
    let test = 'test'
    console.log(name)
    console.log(name1)
  }

  console.log(test)
}

testWith()

image.png


块级作用域

image.png

这里所说的分别是:

  • 块级作用域
  • 是否可以在未声明之前访问
  • 是否会创建global 属性
  • 是否可以重新指向
  • 是否可以重新声明


挡在函数当中定义const / let的时候,这两种类型的变量是存储在当前函数的执行上下文的。如果是函数内部的{}内部创建的,则会在为当前的{}创建新的块级的变量对象。

let name = 'rod';
var name1 = 'rod';

function testWith() {
  let withObject = {
    name: 'rodchen',
    age: '15'
  }

  if(1) {
    let a = 1
    console.log(a)
  }

  console.log(withObject)
}

testWith()


chorme
image.png

firefox
image.png

图示

这是一个函数内部没有{}里创建let / const
image.png


 这是一个函数内部存在{}里创建let / constimage.png



总结:

什么是作用域链?

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