一文彻底搞懂执行上下文、VO、AO、Scope、[[scope]]、作用域链、闭包

0.写在开头


本文将秉承能写代码就不多BB的原则,争取将执行上下文VOAOScope[[scope]]作用域作用域链这些晦涩抽象的概念用伪代码来清晰表述出来,用以强化理解和记忆。

若有写的不对的地方,还请大佬们在评论区批评指正!

那么,让我们开始吧~

1.执行上下文


JS引擎在执行一段代码前,会先创建对应的执行上下文(EC,Execution Context),该执行上下文负责存储VOAOScopethis。同时也创建执行上下文栈(ECStack,Execution Context Stack)来管理执行上下文的推入和弹出

不多BB,来看代码

let a = 1
function fn1(){
  fn2()
}
function fn2(){
  console.log(a)
}
fn1()
// 在执行fn1()时,执行上下文栈操作如下
ECStack.push(globalEC) // globalEC即全局上下文
ECStack.push(fn1EC)
ECStack.push(fn2EC)

// 执行完毕,出栈操作如下
ECStack.pop() // 弹出fn2EC并销毁
ECStack.pop() // 弹出fn1EC并销毁
// globalEC会一直保留,直到程序退出

2.VO


VO即Variable Object 变量对象,定义在全局执行上下文(globalEC)中,存储全局变量函数

来看代码

let a = 1
let arr = [1,2,3]
let obj = {id:107}
function fn(){ ... }

// globalEC
globalEC = {
  VO:{
    a: 1,
    arr: [1,2,3]
    obj: {id:107}
    fn: function fn(){ ... }
  }
}

3.AO


AO即Activation Object 活跃对象,定义在函数执行上下文(fnEC)中(准确来说,在函数开始执行时才创建),存储局部变量子函数以及arguments

不多BB,来看代码

function fn(a,b){
  var c = 3,
  var fn2 = function(){
    let d = 4
    console.log(a+b+c+d)
  }
  fn2()
}
fn(1,2) // 10

// fn函数开始执行前,创建fnEC
fnEC = {
  AO:{
    arguments:{
      '0':1,
      '1':2,
      length:2
	},
	a:1,
	b:2,
	c:undefined, 
	fn2:undefined, 
  }
}
// 将fnEC推入执行上下文栈
ECStack.push(fnEC) // [globalEC,fnEC]

// fn函数执行的过程中慢慢填装AO
fnEC = {
  AO:{
  	arguments:{ ... },
 	a:1,
	b:2,
  	c:3,
  	fn2:function(){ ... }
  }
}
// 执行内部函数fn2,也是如此
fn2EC = {
  AO:{
    arguments:{
      length:0
    },
    d:4
  }
}
// 将fn2EC推入栈
ECStack.push(fn2EC) // [globalEC,fnEC,fn2EC]

// 执行fn2结束
ECStack.pop() // fn2EC销毁
// 执行fn结束
ECStack.pop() // fnEC销毁

4.Scope


Scope定义在执行上下文中,就是所谓的作用域,存储在其中的一个个AOVO按队列顺序链接成了所谓的作用域链
在开始介绍Scope前,先介绍与其关联的[[scope]]

[[scope]]定义在函数中,在函数创建时会保存当前父级函数的[[scope]]以及父级函数执行上下文的AO;若为全局函数,那么保存的是当前全局执行上下文的VO

文字介绍比较晦涩,所以不多bb,写代码就完事了,先来看[[scope]]

let a = 1
function fn(){
  let b = 2
  function fn2(){
    let c = 3
    function fn3(){
      console.log(a,b,c)
	}
  }
  fn2()
}
fn()

// 创建fn函数时,定义fn.[[scope]]
fn.[[scope]] = [globalEC.VO]
// 执行fn函数,在其内部创建fn2函数时,定义fn2.[[scope]]
fn2.[[scope]] = [fnEC.AO, ...fn.[[scope]] ]
// 执行fn2函数,在其内部创建fn3函数时,定义fn3.[[scope]]
fn3.[[scope]] = [fn2EC.AO,...fn2.[[scope]] ] // [fn2EC.AO, fn1EC.AO, globalEC.VO]

Scope定义在执行上下文,[[scope]]定义在函数中,二者关系如下:

fnEC.Scope = [ fnEC.AO, ...fn.[[scope]] ]

不多BB,继续coding,这次串联上前面介绍的ECECStackVOAO[[scope]],只要搞懂了这些,作用域、作用域链、作用域链查询这些概念就是小case

// 执行代码如下
let a = 0
function fn3(){ console.log(a) }
function fn1(b,c,d){
  let a = 1
  fn3()
  function fn2(b){
    let c = 8
    console.log(a,b,c,d)  
  }
  fn2(7)
}
fn1(2,3,4) // 输出: 0  1,7,8,4

// 咳咳... 那么重头戏来了 
// 详细分析如下(建议把上述执行的代码截图,对照着看以下的伪代码分析过程)
ECStack.push(globalEC) // 推入全局执行上下文

globalEC.VO.a = 1
globalEC.VO.fn3 = function(){ ... } // VO存储fn3
fn3.[[scope]] = [globalEC.VO] // fn3创建时定义[[scope]]

globalEC.VO.fn = function(){ ... }
fn.[[scope]] = [globalEC.VO]

fnEC = { // 在执行fn前创建fnEC
  AO:{
	arguments:{'0':2,'1':3,'2':4,length:3},
	b:2,
	c:3,
	d:4,
	a:undefined,
	fn2:function(){ ... }
  }
}
ECStack.push(fnEC) // 推入fnEC [globalEC, fnEC]

fn1.AO.a = 1 // 执行let a = 1才会给AO中的a赋值

fn3EC = { // 执行fn3前创建fn3EC
  AO:{
	arguments:{length:0}	
  },
  Scope:[ fn3EC.AO, ...fn3.[[scope]] ] // [ fn3EC.AO, globalEC.VO ]
}
ECStack.push(fn3EC) // 推入fn3EC [globalEC, fnEC, fn3EC]

// 执行console.log(a),会从当前执行上下文即fn3EC内部的Scope查询,沿着作用域链逐级往上查找a
fn3EC.Scope.forEach(scope=>{
  if(scope.hasOwnProperty('a')){
	console.log(scope['a']) // 找到的是: globalEC.VO.a即0
	break
  }
)

ECSTack.pop() // fn3EC销毁,[globalEC, fnEC] 

fn2.[[scope]] = [fn1EC.AO, ...fn1.[[scope]] ] // 创建fn2时定义[[scope]]
fn2EC = { // 执行fn2前创建执行上下文
  AO:{
	arguments:{ '0':7,length:1},
	b:7,
	c:undefined
  },
  Scope:[ fn2EC.AO, ...fn2.[[scope]] ] // [fn2EC.AO,fn1EC.AO,globalEC.VO]
}
ECStack.push(fn2EC) // [globalEC, fnEC, fn2EC] 
fn2EC.AO.c = 8
// 执行console.log(a,b,c,d),从当前执行上下文即fn2EC的Scope查询
fn2EC.Scope.forEach(scope=>{
  if(scope.hasOwnProperty('a')){
	console.log(scope['a']) // 找到的是: fn1EC.AO.a即1
	break
  }
  // 依此类推
  // 找到 fn2EC.AO.b即7
  // 找到 fn2EC.AO.c即8
  // 找到 fn1.AO.d即4
)

ECStack.pop() // 销毁fn2EC, [globalEC, fnEC] 
ECStack.pop() // 销毁fnEC, [globalEC]

// 直到用户关闭当前页面,globalEC销毁,ECStack也销毁

5.闭包


在搞懂了前面的东西,那么理解闭包就是信手拈来,其本质上就是返回的内部函数的[[scope]]保存了下来

不多bb,来看代码

function fn1(){
  let a = 1
  function fn2(){
  	let b = 2
  	console.log(a,b)
  }
  return fn2
}
let fn = fn1()
fn() // 1,2

// 执行fn1函数,在创建fn2时,会定义fn2.[[scope]]
fn2.[[scope]] = [ fn1EC.AO, ...fn1.[[scope]] ]
// fn1执行完毕,将fn2 return出去,这时候fn = fn2 --> fn.[[scope]] = fn2.[[scope]],故fn2的[[scope]]保留了下来,可以访问fn1EC.AO和fn1.[[scope]]
// 在执行fn时,创建的执行上下文是这样的
fnEC = {
  AO:{
	arguments:{ ... },
	b:2
  }
  Scope:[fnEC.AO, ...fn.[[scope]] ] // [fnEC.AO, fn1EC.AO, globalEC.VO]
}
// 通过fn1EC.AO可以访问fn1函数内部的变量a

再来看一个老掉牙的闭包面试题

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

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

// 我们来分析data[1]
父函数.[[scope]] = [ globalEC.VO ]
父函数EC = { // 注意每次循环父函数都不一样
  AO:{
	arguments:{'0':1,length:1},
	i:1
  },
  Scope:[ 匿名函数EC.AO, globalEC.VO ]
}
返回的函数.[[scope]] = [ 父函数EC.AO, globalEC.VO ] // 保存了父函数的Scope
返回的函数EC={
  AO:{
	arguments:{length:0}
  },
  Scope:[自身AO,父函数EC.AO, globalEC.VO] // 这时i变量从父函数EC.AO中获取
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值