javascript执行上下文

执行上下文(Excution Context)

Js代码在引擎中是以“一段一段”的方式来执行的,而非一行一行来分析执行的。而这“一段一段”的可执行代码无非三种:Global code, Function code, Eval code。这些可执行代码在执行的时候会创建一个一个的执行上下文。例如,当执行到一个函数的时候,js引擎会做一些“准备工作”,而这个“准备工作”,我们称其为执行上下文。那么随着我们执行上下文数量的增加,js引擎又如何管理这些执行上下文呢?这时便有了执行上下文栈。

这里我们用一段贯穿全文的例子来讲解执行上下文的执行过程

var scope = 'global scope'
function checkScope(s) {
  var scope = 'local scope'
  function f() {
    return scope
  }
  return f()
}
checkScope('scope')

当js引擎去解析代码的时候,最先碰到的就是global code,所以一开始初始化的时候便奖全局上下文推入执行上下文栈,并且只有整个应用程序执行完毕的时候,全局上下文才会推出执行上下文栈。

这里我们用ECS(Excution Context Stack)来模拟执行上下文栈,用glocalContext来表示全局执行上下文栈

// 1.在执行函数之前只有全局上下文
ECS = {
  globalContext
}

// 2.当代码执行fn函数的时候,会创建fn函数的执行上下文,并将其压入执行上下文栈
ECS = {
  fnContext,
  globalContext
}

// 3.当代码执行f函数时,会创建f函数的执行上下文,并将其压入执行上下文栈
ECS  = {
  fContext,
  fnContext,
  globalContext
}

// 4.f函数执行完毕后,f函数的执行上下文出栈,随后fn函数执行完毕,fn函数的执行上下文出栈
ECS = {
  // fContext 出栈
  fnContext,
  globalContext
}

ECS = {
  // fnContext 出栈
  globalContext
}

执行上下文的三个重要属性

  • 变量对象(VO)
  • 作用域链(scope chain)
  • this

变量对象

变量对象是与执行上下文相关的数据作用域,储存了在上下文中定义的变量和函数声明。并且不同的执行上下文也有着不同的变量对象,这里分为全局上下文中的变量对象和函数执行上下文中的变量对象。

全局上下文中的变量对象

全局上下文中的变量对象其实就是全局对象。我们可以通过this来访问全局对象,并且在浏览器环境中this=window,在node环境中this=global。

函数上下文中的变量对象

在函数上下文中的变量对象,我们用活动对象来表示(activation object,这里简称ao),为什么称其为活动对象呢,因为只有进入一个执行上下文中,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问。

在函数执行之前,会为当前函数创建上下文,并且在此时,会创建变量对象:

  • 根据函数arguments属性初始化arguments对象;
  • 根据函数声明生成对应的属性,其值为一个指向内存中函数的引用指针。如果函数名称已经存在,则覆盖;
  • 根据变量声明生成对应的属性,此时初始值为undefined。如果变量名已声明,则忽略该变量声明;

还是以刚才的代码为例:

var scope = 'global scope'
function checkScope(s) {
  var scope = 'local scope'
  function f() {
    return scope
  }
  return f()
}
checkScope('scope')

在执行checkscope函数之前,会为其创建执行上下文,并初始化变量对象,此时的变量对象为:

VO = {
  arguments: {
    0: 'scope',
    length: 1
  },
  s: 'scope',
  f: pointer to function f,
  scope: undefined
}

随着checkscope函数的执行,变量对象被激活,变量对象内的属性随着代码的执行而改变:

VO = {
  arguments: {
    0: 'scope',
    length: 1
  },
  s: 'scope',
  f: pointer to function f,
  scope: 'local scope'
}

作用域链

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

下面还是用上面的例子来讲解作用域链:

首先在checkscope函数声明的时候,内部会绑定一个[[scope]]的内部属性:

checkscope.[[scope]] = [
  globalContext.VO
]

接着在checkscope函数执行之前,创建checkscopeContext,并推入执行上下文栈:

  • 复制函数的[[scope]]属性初始化作用域链
  • 创建变量对象
  • 将变量对象压入作用域链的最顶端
checkscopeContext = {
  scope: checkscope.[[scope]],
  VO: {
    arguments: {
      0: 'scope',
      length: 1
    },
    s: 'scope',
    f: pointer to function f,
    scope: undefined
  },
  this: globalContext.VO
}

// 将函数的变量对象压入作用域链的最顶端
checkscopeContext = {
  scope: [VO, checkscope.[[scope]]],
  VO: {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope',
    f: pointer to function f,
    scope: undefined
  },
  this: globalContext.VO
}

接着,随着函数的执行,修改变量对象:

checkscopeContext = {
  scope: [VO, checkscope.[[scope]]],
  VO: {
    arguments: {
      0: 'scope',
      length: 1
    },
    s: 'scope',
    f: pointer to function f,
    scope: 'local scope'
  },
  this: globalContext.VO
}

与此同时遇到f函数声明,f函数绑定[[scope]]属性:

f.[[scope]] = [
  checkscopeContext.VO, // f函数的作用域还包括checkscope的变量对象
  globalContext.VO
]

之后f函数的步骤同checkscope函数。

再来一个经典的例子:

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

结果很简单,data的所有元素执行结果都是6。在这里我们用分析一下他的作用域链:

在data函数执行前,此时全局上下文的变量对象为:

globalContext.VO = {
  data: [pointer to function (),...],
  i: 6
}

每一个data的匿名函数的执行上下文大致如下:

data[n]context = {
  scope: [VO, globalContext.VO],
  VO: {
    arguments: {
      length: 0
    }
  },
  this: globalContext.VO
}

那么在函数执行的时候,会先去自己匿名函数的变量对象中找i,发现未找到后沿着作用域链向上查找,找到了全局执行上下文的变量对象,而此时全局上下文变量对象中的i是6,所以每次打印都是6.

词法作用域

javascript这门语言是基于词法作用域来创建作用域的,也就是说一个函数的作用域在函数声明的时候就已经确定了,而不是函数执行的时候。

改一下之前的例子:

var scope = 'global scope'
function f () {
  console.log(scope)
}
function checkscope() {
  var scope = 'localscope'
  f()
}
checkscope()

因为javascript是机遇词法作用域创建作用域的,所以打印的结果是global scope而不是local scope。我们结合上面的作用域链分析一下:

首先遇到f函数声明,此时为其绑定[[scope]]属性:

f.[[scope]] = [
  globalContext.VO
]

f函数执行之前的执行上下文是:

fContext = {
  scope: [VO, f.[[scope]]],
  VO: {
    arguments: {
      length: 0
    }
  },
  this: globalContext.VO
}

f函数的执行过程,先在f函数的执行上下文的变量对象中查找,未发现后到全局上下文的变量对象中查找,此时scope的值为global scope。

this

在这里this绑定也分为全局执行上下文和函数执行上下文

  • 在全局执行上下文中,this的值指向全局对象。(浏览器中this指向window,node中指向global)
  • 在函数执行上下文中,this的值取决于该函数如何被调用的。如果它被一个对象调用,那么this会被设置成这个对象,否则this的值被设置为全局对象或者undefined(在严格模式下)

总结起来就是谁调用,this指向谁。

实例分析

例子1-普通对象this分析

var name = 'window'
var obj1 = {
  name: 'obj1',
  fn1: function() {
    console.log(this.name)
  },
  fn2: () => {
    console.log(this.name)
  },
  fn3: function() {
    return function() {
      console.log(this.name)
    }
  },
  fn4: function() {
    return () => {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}

obj1.fn1() // obj1
// 执行过程上下文为
//fn1Context = {
//  scope: [VO, globalContext.VO],
//  VO: {
//    arguments: {
//      length: 1
//    }
//  },
//  this: obj1
//}
// 由上下文可知this指向obj1,所以结果为obj1.name = 'obj1'


obj1.fn1.call(obj2) // obj2
// fn1Context = {
//   scope: [VO, globalContext.VO],
//   VO: {
//     arguments: {
//       length: 1
//     }
//   },
//   this: obj2
// }
// 可知this指向obj2,所以结果为obj2.name = 'obj2'

obj1.fn2() // window
obj1.fn2.call(obj2) // window
// 因为箭头函数没有自己的this,他的this永远指向父级执行上下文的this,那为什么上层执行上下文是globalContext.VO呢?有上下文可知javascript中的上下文分为全局执行上下文,函数执行上下文域eval执行上下文。而不管是全局执行上下文或函数执行上下文,大致都包含创建VO,确认作用域链,确认this指向三步。也就是说,this属于上下文中的一部分,很明显对象obj1并不是一个函数,它并没有权利创建自己的上下文,所以没有自己的this,那么他的外层是谁呢?当然是全局window了,所以这里的this指向window。箭头函数的this由外部环境决定,且一旦绑定无法通过call,apply或者bind再次改变箭头函数的this。所以这里虽然使用了call方法依旧无法修改,指向window
// fn2Context = {
//   scope: [VO, globalContext.VO],
//   VO: {
//     arguments: {
//       length: 1
//     }
//   },
//   this: globalContext.VO
// }

obj1.fn3()() // window
obj1.fn3().call(obj2) // obj2
obj1.fn3.call(obj2)() // window
// fn3返回一个闭包, 而它的this指向它的调用者,即obj1.fn3()返回的函数的调用者
// fn3returnFnContext = {
//   scope: [VO, fn3Context.VO, globalContext.VO],
//   VO: {
//     arguments: {
//       length: 1
//     }
//   },
//   this: 返回函数的调用者
// }

obj1.fn4()() // obj1
obj1.fn4().call(obj2) // obj1
obj1.fn4.call(obj2)() // obj2
// fn4返回一个闭包,只是这个闭包是一个箭头函数,而箭头函数没有自己的this,继承于父级this,所以返回函数的this指向fn4的this,即fn4的调用者
// fn4ReturnFnContext = {
//   scope: [VO, fn4Context.VO, globalContext.VO],
//   VO: {
//     arguments: {
//       length: 1
//     }
//   },
//   this: fn4的调用者
// }

总结:创建上下文只有三中方式,全局上下文/函数上下文/eval上下文。对象不是函数,不具备创建上下文的能力

例子2-继承对象this分析

var name = 'window'
function Person(name) {
  this.name = name
  this.fn1 = function() {
    console.log('fn1:', name)
    console.log(this.name)
  }
  this.fn2 = () => {
    console.log('fn2:', name)
    console.log(this.name)
  }
  this.fn3 = function() {
    return function() {
      console.log('fn3:', name)
      console.log(this.name)
    }
  }
  this.fn4 = function() {
    return () => {
      console.log('fn4:', name)
      console.log(this.name)
    }
  }
}

var obj1 = new Person('obj1')
var obj2 = new Person('obj2')

obj1.fn1() // 'wondow'- 隐式绑定,this指向new出来的对象
obj1.fn1.call(obj2) // 'obj2' - 显式绑定,this指向绑定对象

obj1.fn2() // 'obj1' - 箭头函数没有自己的this,用的是上层上下文环境中的this
obj1.fn2.call(obj2) // 'obj1' - 箭头函数的this永远指向上层执行上下文中的this

obj1.fn3()() // 'window' - 返回闭包本质上被window调用,this被修改
obj1.fn3().call(obj2) // 'obj2' - 返回闭包后利用call方法显示改变this
obj1.fn3.call(obj2)() // 'window' - 返回闭包还是被window调用

obj1.fn4()() // 'obj1' - 返回闭包是箭头函数,this同样会指向obj1,虽然返回也是被window调用,但箭头函数无法被之间修改,还是指向obj1
obj1.fn4.call(obj2)() // 'obj2' - 箭头函数可通过修改外层执行上下文中的this指向来达到间接修改的目的
obj1.fn4().call(obj2) // 'obj1' - 返回的箭头函数无法被直接修改

作用域与执行上下文

  • javascript使用的是词法作用域。对于函数来说,此法作用域是定义时就确定了的,与函数是否被调用无关。通过作用域可以知道作用域范围内的变量和函数有哪些,却不知道变量的值是什么,所以是静态作用域。
  • 对于函数来说,执行环境是在调用时确定的,执行环境包含作用域内所有变量和函数的值。在同一作用域下,不同的调用会产生不同的执行环境,从而产生不同的变量的值。所以执行环境是动态的。
  • 一个作用域可能包含多个执行上下文(闭包),也可能一个执行上下文都没有(函数执行完了,上下文环境销毁,或者没有调用函数)
  • 由定义可知,作用域是在函数声明的时候就确定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的集合体。也就是说作用域定义了执行上下文的变量访问规则,执行上下文是在作用域规则的前提下执行代码的。
  • 15
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s-alone

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值