深入理解JavaScript中的执行环境(execution context)

-------------------------------------华丽的分割线-------------------------------------

执行环境(execution context,也称作‘环境’)定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,,环境中定义的所有变量和函数都保存在这个对象中。

代码执行时,可以划分为以下类型:

  • 全局代码: 默认环境,是最外围的一个执行环境。
  • 函数代码(局部环境):当执行流进入一个函数体的时候
  • Eval代码: 在eval()函数中的代码

根据ECMAScript实现所在宿主环境不同,表示执行环境的对象也不一样。在web浏览器中,全局执行环境被认为是window对象。因此所有全局变量和函数都是作为window对象的属性和方法创建的。一般情况下,某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出: 关闭网页或者浏览器时才会被销毁)。


在这里想说一下js的垃圾回收机制:找出那些不再继续使用的变量,然后释放其占用的内存。

犀牛书上也有讲过:

垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性的执行这一操作。函数中局部变量的正常声明周期: 局部变量只在函数执行过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间。以便存储他们的值。

然后在函数中使用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此就可以释放他们的空间内存以供将来使用。在这种情况下,很容易判断变量是否还有存在的必要,某些情况就不是很好判断(闭包)。垃圾收集器必须跟踪那个变量有用那个变量没用,对于不再有用的变量打上标记,以便将来收回其占用的内存。

一般通过标记清除(现在浏览器大多采用这种)和引用计数(不常用)两种方式。

至于是怎么标记清除的,有兴趣的同学可以深入去研究一下。这里不再详述。


进入正文:

每个函数都有自己的执行环境,在新的函数环境中,会创建一个私有作用域,在这个函数中创建的任何声明都不能被当前函数作用域之外的地方访问。JavaScript解释器在浏览器中是单线程的,这意味着浏览器在同一时间内只执行一个事件,对于其他的事件我们把它们排队在一个称为 执行栈的地方。当执行流进入一个函数时,函数的环境就会被推入一个执行栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境

js中保存变量的堆内存、栈内存,执行栈,栈的概念性区别?

执行栈的几个特点:

  • 单线程
  • 同步执行
  • 拥有一个全局环境
  • 无限的函数环境
  • 函数被调用就会创建一个新的执行环境(甚至调用自己)。

一个函数被调用就会创建一个新的执行环境。然而解释器的内部,每次调用执行环境会有两个阶段:

1.创建阶段(预解释):

  1. 当函数被调用,但是未执行内部代码之前
  2. 创建一个作用域链
  3. 创建变量,函数和参数。
  4. 确定this的值。

2. 代码执行阶段:

  1. 赋值,引用函数,解释/执行代码。

可以把执行环境理解为一个executionContextObj对象,它身上带有三个属性:

executionContextObj = {
    scopeChain: { /* variableObject + all parent execution context's variableObject */ },
    //作用域链:{变量对象+所有父执行环境的变量对象}
    variableObject: { /* function arguments / parameters, inner variable and function declarations */ },
    //变量对象:{函数形参+内部的变量+函数声明(但不包含表达式)}
    this: {}
}

执行环境:

  • 作用域链: 包含变量对象和所有父执行环境的变量对象
  • 变量对象: 函数形参,内部变量和函数声明(环境中定义的所有变量和函数都保存在这个对象中)
  • this: 执行上下文

活动对象,变量对象?怎么区别?

当函数被调用,executionContextObj就被创建,该对象在实际函数执行前就已创建。这就是已知的第一个阶段创建阶段.在第一阶段,解释器创建了executionContextObj对象,通过扫描函数,传递形参,函数声明和局部变量声明。扫描的结果成为了变量对象在executionContextObj中。

执行环境被推入执行栈后,大概发生了以下过程:

  • 代码被调用(推入执行栈)
  • 在执行函数代码前,创建执行环境
  • 进入创建阶段:
    • 初始化作用域链
    • 创建变量对象
    • 创建arguments对象,检查环境中的参数,初始化名和值
    • 扫描环境中的函数声明:
      • 某个函数被发现,在变量对象创建一个属性,它是函数的确切名。它是一个指针在内存中,指向这个函数
      • 如果这个函数名已存在,这个指针的值将会重写。
    • 扫描环境内的变量声明
      • 某个变量声明被发现,在变量对象中创建一个属性,他是变量的名,初始化它的值为undefined
      • 如果变量名在变量对象中已存在,什么也不做,继续扫描
    • 在环境中确定this的值。
  • 激活/代码执行阶段:在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值
function foo(i) {
  var a = 'hello';
  var b = function privateB() {
};
function bar() {}

foo(22);

上面代码在调用foo(22)的时候,按照上面过程,创建阶段:

                fooExecutionContext = {
                    scopeChain: {...},
                    variableObject: {
                        arguments: {
                            0: 22,
                            length: 1
                        },
                        i: 22,
                        bar: pointer to function bar(),
                        a: undefined,
                        b: undefined
                    },
                    this: {...}
                }

创建阶段定义属性的名称,但是并不赋值(除了形参和实参,他们是赋值了的)。

创建阶段完成后,执行流进入函数,开始执行阶段,大致如下:

fooExecutionContext = {
	scopeChain: { ... },
	variableObject: {
		arguments: {
            0: 22,
            length: 1
    	},
		i: 22,
		bar: pointer to function bar()
		a: 'hello',
		b: pointer to function privateB()
	},
	this: { ... }
}

一个demo加深理解:

(function() {

  console.log(typeof foo); // function pointer
  console.log(typeof bar); // undefined

  var foo = 'hello',
    	bar = function() {
    		return 'world';
  		};

  function foo() {
  	return 'hello';
  }

}());

为什么在声明foo之前我们就可以调用?

如果我们按照创建阶段进行,我们知道变量在激活/执行阶段之前已经被创建了。因此,在函数流开始执行,foo已经在活动对象中被定义了。

foo被声明了两次, 为什么foo展现出来的是functiton,而不是undefined或者string?

我们从创建阶段知道,尽管foo被声明了两次,函数在活动对象中是在变量之前被创建的,并且如果属性名在活动对象已经存在,会绕过这个声明。所以,引用函数foo()是在活动对象上第一次被创建的, 当我们解释到 var foo的时候,我们发现属性名foo已经存在,所以代码不会做任何处理,只是继续进行

为什么bar是undefined?

bar确实是一个变量,并且值是一个函数。变量是在创建阶段被创建的,但是它们的值被初始化为undefined。

一个更具体的例子:

var a = 10;
function b () {
    console.log('全局的b函数')
};
function bar(a, b) {
    console.log('1', a, b) 
    var a = 1
    function b() {
        console.log('bar下的b函数')
    }
    console.log('2', a, b) 
}
bar(2, 3)
console.log('3', a, b)
1 2 ƒ b() {
        console.log('bar下的b函数')
    }

2 1 ƒ b() {
        console.log('bar下的b函数')
    }

3 10 ƒ b () {
    console.log('全局的b函数')
}

其代码执行过程如下:

  • 在浏览器内代码进入到全局作用域,进入创建阶段即预解释阶段(形成一个新的执行环境):
    • 声明了a的值,此时为undefined,
    • 声明了函数b,bar。它们的值分别为该函数所在内存地址的引用
  • 当代码执行到bar(2, 3)之前,会进行第二个阶段即函数执行阶段,a的值变为a=10
  • 代码执行到bar(2, 3),就有重复步骤1, 2,会形成一个新的执行环境,并被推入执行栈:
    • 创建arguments对象,并且对形参赋值,此时 a = 2, b = 3
    • 代码块内再次声明了变量a,此时什么都不做(a仍然为2),继续扫描到了函数声明b,值为函数b所在内存地址的引用
  • 代码执行阶段:
    • 执行到console.log(‘1’, a, b) 的时候,a为2, b表示的是一个内存地址的引用(function b() {console.log(‘bar下的b函数’)})
    • 执行到下一行var a = 1的时候,根据上面的规则,会忽略对a的再次声明,只是覆盖原来a的值,此时a = 1, b的值还没改变。
  • bar(2, 3)函数执行完毕,被推出执行栈。此时进入全局作用域的执行栈,到console.log(‘3’, a, b)时,变量a,b的值分别为1, 2步骤下的值,即a = 10, b为一个指针,指向function b () {console.log(‘全局的b函数’)}在内存中的位置;

建议有一定js基础的同学看,初学可能会看的云里雾里

遗留两个小问题:闭包的问题;JS的event loop

如果此文帮到了你,你可以随意赞赏,以示鼓励。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lihchweb/article/details/103715826
今日推荐