JavaScript 作用域链简述

简介: JavaScript 作用域链简述

前言


继上一篇文章JavaScript 脚本编译与执行过程简述,再来介绍一下 JavaScript 中的作用域链(Scope Chain)。


函数的 [[scope]] 也是与闭包直接相关。并推荐专题:


正文


作用域链的形成


作用域链(以下简称 Scope)与执行上下文相关。

全局上下文:
  其 Scope 就是 GlobalContext.VO,即 window 对象。
函数上下文:
  函数被调用时,函数上下文的 Scope 被创建,包括 AO 和这个函数内部的 [[scope]] 属性。


因此,我们可以大致当前上下文的作用域链:Scope = AO + function.[[scope]]


函数内部 [[scope]] 属性的形成


当函数被创建的时候,属性 [[scope]] 会保存所有的父级变量对象。

举个例子:

function foo() {
  // some statements...
}


上述例子,函数 foo 处于全局上下文。而全局上下文中所声明的函数,它们的 [[scope]]GlobalContext.VO,即 window 对象。因此 foo.[[scope]] = [ GlobalContext.VO ]


再看:

function foo() {
  function bar() {
    // some statements...
  }
}


同样地,函数 foo[[scope]] 属性为 GlobalContext.VO。然后调用 foo 函数,进入 foo 函数上下文并进行初始化,包括以下过程:

  1. 以当前函数 foo.[[scope]] 为基础初始化函数上下文的 Scope
  2. 初始化上下文的 AO 对象,包括 Arguments、形参、函数声明、变量声明。
    该过程若有函数声明,对应函数的 [[scope]] 也将会被确定,其值就是 Scope
  3. AO 初始化完成后,将 AO 插入上下文的 Scope 中。

因此有两个结论:


  • 当前上下文的作用域链 Scope = AO + foo.[[scope]]
  • 当前上下文内定义的函数 bar,其 [[scope]] 属性值为上下文的 Scope,即 AO + foo.[[scope]]


注意,即便是函数表达式,它在代码执行的时候,才会确定其 [[scope]],由于执行过程中 AO 也会跟着更新,且它们是引用关系,因此总能确保,当前作用域内的函数(函数声明或函数表达式)的 [[scope]] 总是 AO + 各父级上下文的 AO/VO

但是使用 Function 构造器来创建一个新的函数,该函数的 [[scope]] 只有 GlobalContext.VO。下面的示例中,执行 bar 函数会去作用域链上查找 a 变量,可它的作用域链只含全局对象,导致找不到 a 变量而抛出 ReferenceError

function foo() {
  var a = 1
  var bar = new Function('console.log(a)')
  bar() // ReferenceError: a is not defined
}
foo()


因此,尽量不要使用构造函数的方式来创建函数。


影响作用域链的一些例子


一般情况下,一个作用域链 Scope 包括父级变量对象、函数上下文的活动对象 AO,并从当前上下文逐级往上查询。

提醒一下:当我们从对象上查询某个属性,首先从对象自身属性上查找,当找不到的时候,才会往原型上查找......直至原型链的顶端 Object.prototype 再查询不到就返回 undefined


其实作用域链的原理跟原型链很类似,当前如果这个变量在自己的作用域中没有,那么它会往父级查找,直至最顶层(全局对象),再查找不到就会抛出 ReferenceError


前面讲过,当前上下文(作用域)内声明的变量或函数,是以属性的形式,放到一个变量对象(Variable Object)上的。但由于 VO 是无法通过代码访问的,因此在函数调用的时候 VO 被激活形成一个活动对象(Activation Object),它是可以被访问到的(可以简单的理解为 AOVO 浅拷贝的一个引用)。


但是,AO 是没有原型的。假设我们在当前作用域下查找一个变量 a,相当于从 AO 上查找 a 属性。假设 AO 本身没有该属性,自然会往 AO 原型上查找,但很遗憾 AO 没有原型,即当前作用域下查找不到该变量(或称为属性)。然后往作用域链的上一级 AO 中查找......查找规律同理......直到全局作用域(其 VO 就是 window 对象)下的 window 对象查找。由于 window 对象是有原型的,如果自身找不到 a 属性,就会往 window 的原型上查找,查到就返回,查不到就抛出 ReferenceError

说那么多,还不如看个例子更清晰:

Object.prototype.a = 'proto'
function foo() {
  console.log(a)
}
foo() // "proto"


从例子可以看出 foo 函数上下文下并没有声明 a 变量,于是往上一级查找(即全局上下文),那么从 window 自身查找,是没有的。但是 window 是基于 Object 创建的(window instanceof Object 结果为 true),于是从 Object.prototype 上查找,并找到 a 属性,属性值为 "proto"


如何证明 AO 是没有原型的?

Object.prototype.a = 'proto'
function foo() {
  var a = 'inner'
  function bar() {
    console.log(a)
  }
  bar()
}
foo() // "inner"

过程就不在赘述了,假设 AO 是有原型的,那么 bar 函数上下文中查找 a 变量是,应该会取到 AO 对象原型上的 a 属性 "proto",但实际情况 a 取到的结果是 "inner"。因此可以证明:活动对象 AO 是没有原型的。


全局和 eval 上下文中的作用域链


全局上下文的作用域链仅包含全局对象。而 eval 上下文与当前的调用上下文(calling context)拥有同样的作用域链。

GlobalContext.Scope = [ window ]
EvalContext.Scope === CallingContext.Scope;


代码执行时对作用域链的影响


有些情况下也会包含其他对象,例如执行期间,动态加入作用域链中的,例如 with 语句或者 catch 语句。此时作用域链如下:

Scope = (withObject | catchObject)  +  (AO | VO)  +  [[Scope]]
withObject
  表示 with 语句产生的临时作用域对象。如 with({ name }) 中的 { name } 对象;
catchObject
  表示 catch 从句产生的异常对象。如 catch(e) 中的 e 对象。


举个例子:

var foo = { x: 1, y: 2 }
with (foo) {
  console.log(x) // 1
  console.log(y) // 2
}


它的作用域链变成了:Scope = foo + (AO | VO) + [[Scope]]。上面这个例子可能没有体现出来,我们修改一下:

var x = 1, y = 2
var foo = { x: 2 }
with (foo) {
  var x = 3, y = 4
  console.log(x) // 3
  console.log(y) // 4
}
console.log(x) // 1
console.log(y) // 4
console.log(foo) // { x: 3 }


我们来分析一下:


  1. 进入全局上下文的时候,会创建声明 xyfoo 变量。
  2. 执行到 with 语句,会将 foo 对象添加至作用域链顶端。
  3. with 内部的 xy 前面已被解析添加,因此它只是一个赋值语句,并不会重新赋值语句。
  4. 关键在于 with 内部,给 x、y 赋值,究竟是对应哪个变量。前面提到遇到 with 语句会往作用域链顶端插入该对象 foo(注意不会创建一个全新的作用域上下文,只是修改了作用域链而已)。
  5. 因此,当 console.log(x) 查找 x 变量时,从 foo 对象上查找 x 属性,并找到,因此 foo.x 被修改为 3
  6. 接着,查找 y 变量,而 foo 对象上没有(其原型也没有),因此往上一级作用域查找(即全局作用域),因此全局作用域下的 y 被修改为  4
  7. 因此 with 内部的 xy 分别打印出:34
  8. with 执行完,作用域链上的 foo 对象会被移除。即作用域链上只剩下 window 对象。
  9. 后面查找 xyfoo 变量都是从全局作用域下查找的,因此会分别打印出 14
  10. 最后我们也可以看到 foo 对象是更新变为:{ x: 3 }


结合前面的原型的内容,假设将 foo 对象的原型上添加 y 属性,那么 y = 4 被修改的是 foo.__proto__ 上的属性,而不是全局作用域下的 y 变量。(有兴趣的自行尝试一下)


The end.


目录
相关文章
|
17天前
|
JavaScript 前端开发
js变量的作用域、作用域链、数据类型和转换应用案例
【4月更文挑战第27天】JavaScript 中变量有全局和局部作用域,全局变量在所有地方可访问,局部变量只限其定义的代码块。作用域链允许变量在当前块未定义时向上搜索父级作用域。语言支持多种数据类型,如字符串、数字、布尔值,可通过 `typeof` 检查类型。转换数据类型用 `parseInt` 或 `parseFloat`,将字符串转为数值。
18 1
|
1月前
|
存储 JavaScript 前端开发
解释 JavaScript 中的作用域和作用域链的概念。
【4月更文挑战第4天】JavaScript作用域定义了变量和函数的可见范围,静态决定于编码时。每个函数作为对象拥有`scope`属性,关联运行期上下文集合。执行上下文在函数执行时创建,定义执行环境,每次调用函数都会生成独特上下文。作用域链是按层级组织的作用域集合,自内向外查找变量。变量查找遵循从当前执行上下文到全局上下文的顺序,若找不到则抛出异常。
22 6
|
7月前
|
自然语言处理 JavaScript 前端开发
带你读《现代Javascript高级教程》一、作用域和作用域链(2)
带你读《现代Javascript高级教程》一、作用域和作用域链(2)
|
2月前
|
自然语言处理 前端开发 JavaScript
深入理解JavaScript中的闭包与作用域链
在JavaScript编程中,闭包和作用域链是两个非常重要的概念,它们对于理解代码的执行过程和解决一些特定问题至关重要。本文将深入探讨JavaScript中闭包和作用域链的原理和应用,帮助读者更好地理解这些概念并能够在实际项目中灵活运用。
|
2月前
|
JavaScript 前端开发
JS作用域与作用域链
JS作用域与作用域链
|
3月前
|
缓存 前端开发 JavaScript
深入理解JavaScript闭包:解锁神秘的作用域链
深入理解JavaScript闭包:解锁神秘的作用域链
39 0
|
4月前
|
JavaScript 前端开发 开发者
深入理解JavaScript作用域与作用域链
深入理解JavaScript作用域与作用域链
53 0
|
4月前
|
自然语言处理 JavaScript 前端开发
JavaScript中的闭包和作用域链
本文将介绍JavaScript中的闭包和作用域链的概念及其在编程中的应用。闭包是指函数与其相关的词法环境的组合,通过闭包我们可以实现变量的私有化和封装性,提高代码的安全性和可维护性。作用域链则是指在嵌套函数中查找变量的一种机制,它决定了变量的可见性和访问顺序。深入理解闭包和作用域链对于提升JavaScript编程技巧至关重要。
|
4月前
|
JavaScript 前端开发 Shell
深入学习JavaScript系列——作用域和作用域链
深入学习JavaScript系列——作用域和作用域链
|
5月前
|
自然语言处理 前端开发 JavaScript
【前端|Javascript第3篇】探秘JavaScript的作用域与作用域链:小白也能轻松搞懂!
【前端|Javascript第3篇】探秘JavaScript的作用域与作用域链:小白也能轻松搞懂!