空梦博客

JavaScript语言编程核心(三)— 执行上下文与作用域

执行上下文(EC)

执行上下文:可以抽象为一个简单的对象,在一个函数或者eval( )被执行时创建,对象里面包含着一些代码运行所需的属性,统称为 上下文状态。一个上下文状态由三个结构组成:

  • 变量对象(variable Object);
  • 作用域链(scope chain);
  • this指向(thisValue);

执行上下文堆栈

一般可以认为有三种执行上下文:

  • 全局执行上下文;
  • 函数执行上下文;
  • eval执行上下文;

一个执行上下文可以触发另一个上下文,以栈的形式实现,称为执行上下文堆栈

触发其它上下文的叫做 caller,而被触发的上下文叫做 callee

1.当代码执行的时候,首先生成一个全局执行上下文(global EC)。

2.当执行到函数作用域时,全局执行上下文(caller)会触发该函数执行上下文(callee),将控制权传递给callee,而此时caller则暂缓执行。callee被push到栈的最顶层,拥有控制权,成为当前运行的执行上下文(active EC)。

3.当callee执行结束后,控制权会返还给callercallee从栈顶层被pop除,caller继续往下执行。

变量对象(VO)

变量对象是执行上下文中的一个数据作用域,存储着上下文中所定义的变量和函数声明(不包括函数表达式)。

全局上下文的变量对象就全局对象本身。

活动对象(AO)

当函数被caller触发时,该函数的变量对象(VO)则会被激活成为活动对象(AO),里面加入了形参和arguments对象。

作用域

作用域:通俗点可以理解为是一段代码或者一个变量的作用范围,是程序源代码中定义这个变量的区域,可以简单的理解为一个对象,其实,变量对象便是作用域的实体。比如,一个全局变量,就拥有全局的作用域,在JavaScript代码的所有地方都有定义,都可以进行访问,理解为一个全局对象中定义着这个全局属性。

在许多编程语言中,每个花括号中的代码都具有各自的作用域,而在花括号的外部,是不可见的,这称为 块级作用域(block scope)。但在JavaScript中,虽然语法上也是使用花括号,但并不存在块级作用域,在花括号内定义的变量,在外部都是可见的。取而代之的是函数作用域(function scope):在一个函数中声明的变量,在该函数,包括其内部嵌套函数中,都是有定义,可见的。但在函数的外部是不可见的。

因此,这里就引出了另一个概念– 声明提前
根据函数作用域的定义,一个声明在函数体内的任何地方都是始终可见的,甚至在声明之前。因此,就算在声明一个变量之前,也是可以访问到这个变量的(但不涉及到赋值)。

1
2
3
4
5
6
7
var scope = "global";
(function(){
// 并不是输出global,这是因为声明提前,等于在该语句前加了一句,var scope。但并不涉及赋值;
console.log(scope); // undefined;
var scope = "local";
console.log(scope); // local;
})();

作用域链

在定义一个函数作用域时,会随之产生一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表,列表内包含所有父级变量对象和自身的活动对象,另外,通过with语句catch语句也可动态的延长作用域链。

作用域链其实是由两部分组成的:函数内部属性 [[scope]] + 自身活动对象(AO);
[[scope]]属性中,保存着父级函数的作用域链。

当在函数内访问一个变量时,首先会从该函数自身的活动对象中查找,如果不存在,则会沿着作用域链往上一级一级查找父级变量对象。

闭包

由于在JavaScript中,函数属于第一级对象,即可以当成参数传入一个函数,也可以从函数中返回出一个函数。此时,便会出现一个问题,当一个父函数执行后返回另一个子函数后,便会被摧毁,而此时,返回的子元素的[[scope]]属性中仍然保存着父函数的作用域链,以此来解决访问父函数中变量的问题。

这种类型的作用域,成为静态(词法)作用域。

1
2
3
4
5
6
7
8
var afun = (function(){
var a = 1;
return function afun(){
alert(a);
}
})();
var a = 2;
afun(); // 1

静态作用域是闭包存在的一个必需条件,因此

闭包可以定义为:

一个函数,和以静态方式/词法方式进行存储的所有父作用域的一个集合体。所以,通过这些存储的作用域,函数可以很容易的找到自由变量。

Tips:从理论上讲,每一个函数都可以称为闭包,因为都包含了[[scopte]]属性。

当在一个父函数中定义了两个子函数时,此时两个子函数拥有相同的[[scope]]属性,而且是共享的。也就是说,改变一个闭包中的变量,会影响到另一个闭包中的变量。这也就是我们最常见的闭包问题的根源所在:

1
2
3
4
5
6
7
8
9
10
var a = [];
for(var i = 0;i<10;i++){
a[i] = function(){
console.log(i);
}
}
// 而不是预期中的 0~9,其原因就是这9个函数共享一样的[[scope]]属性;
a[0](); // 10;
a[1](); // 10;
a[9](); // 10;

可以通过将参数i以参数的形式传入,让其不需要从[[scope]]属性中进行查找。

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for(var i = 0;i<10;i++){
a[i] = (function(x){
return function(){
console.log(x);
}
})(i);
}
a[0](); // 0;
a[1](); // 1;
a[9](); // 9;

Y(・∀・)Y 蟹,请我喝瓶可乐吧!
  • QQ
  • AliPay
  • WeChat


本文作者: Mr. 空梦
发布时间: 2019-12-05
最后更新: 2020-01-14
本文标题: JavaScript语言编程核心(三)--- 执行上下文与作用域
本文链接: https://kmbk0.top/2019/12/05/JavaScript语言编程核心(三)—执行上下文与作用域/
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可。转载请注明出处!

 评论