JavaScript 的执行上下文

执行上下文

JavaScript 是顺序执行代码程序的。

function foo() {
	console.log('嘿嘿');
}

foo(); // '嘿嘿'

var myName = 'heihei';

myName;  // ‘heihei’

JavaScript 的可执行代码有哪几种?

  1. 全局代码
  2. 函数代码
  3. eval 代码

当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。

调用栈

调用栈就是用来管理函数调用关系的一种数据结构。


function foo() {
	console.log('嘿嘿');
}

foo(); // '嘿嘿'

在执行函数 foo 之前,JavaScript 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量。
在这里插入图片描述
接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?
JavaScript 引擎通过利用栈结构来管理执行上下文。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中。


var a = 'heihei1';
function foo() {
	var b = 'heihei2';
}
function bar() {
	var c = 'heihei3';
	foo();
}
bar();

第一步,创建全局上下文,并将其压入栈底
在这里插入图片描述
接下来,开始执行全局代码。
在这里插入图片描述

执行 foo 函数调用,创建函数上下文,压栈。
在这里插入图片描述
接下来,开始执行 foo 函数代码。
在这里插入图片描述
执行到 foo() ,开始创建 foo 上下文,压栈。
在这里插入图片描述
执行 foo 函数。
在这里插入图片描述
当 foo 函数返回时,该函数的执行上下文就会从栈顶弹出。
在这里插入图片描述
当 bar 函数返回时,该函数的执行上下文就会从栈顶弹出。
在这里插入图片描述
至此,整个 JavaScript 流程执行结束了。调用栈是 JavaScript 引擎追踪函数执行的一个机制


foo();
myName;
var myName = 'heihei';
function foo() {
	console.log('嘿嘿');
}

变量提升

按照我们正常的从上到下执行来说,我们的正常逻辑应该会这样认为:

  1. 执行函数 foo ,找不到 foo 就报错。
  2. 执行 myName ,找不到就报错。

但是确实打印出了 “undefined” 和 “嘿嘿”。

所以我们可以得出一个结论。

  1. 变量定义之前就使用它,该值为 undefined。
  2. 函数定义之前使用它,会正常执行函数。

var myName = ‘heihei’;做了下面的事情。
在这里插入图片描述
function foo() {} 可以看作为:
在这里插入图片描述

变量提升:
是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

JavaScript 代码的执行流程

即使存在提升,但是变量和函数在代码里的位置是不会改变的,而是在 编译阶段 被 JavaScript 引擎放入内存中。

第一部分:变量提升部分的代码。
在这里插入图片描述
第二部分,可执行代码部分
在这里插入图片描述

执行上下文

执行上下文是 JavaScript 执行一段代码时的运行环境。每个执行上下文,都有三个重要的属性:

  • 变量环境。
  • 词法环境。
  • 可执行代码。
  • 作用域链。
  • this。

变量环境。

当进入执行上下文时,这时候还没有执行代码。

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)
    • 由名称和对应值组成的一个变量对象的属性被创建。
    • 没有实参,属性值设为 undefined
  2. 函数声明。
    • 由名称和对应值组成一个变量对象的属性被创建。
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性。
  3. 变量声明
    • 由名称和对应值 (undefined),组成一个变量对象的属性被创建。
    • 如果变量名称根已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

在这里插入图片描述
JavaScript 会把声明以外的代码编译为字节码(可执行的代码)。

foo();
myName = 'heihei';
执行阶段

JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。
在这里插入图片描述
看一个更加完整的变量环境。

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);
在进入执行上下文之后:

在这里插入图片描述

代码执行:

在这里插入图片描述

词法环境

在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。

变量提升所带来的问题

  1. 变量容易在不被察觉的情况下被覆盖掉
  2. 本应销毁的变量没有被销毁

ES6 是如何解决变量提升带来的缺陷,ES6 引入了 let 和 const 关键字。

function foo(){ 
	var a = 1
	let b = 2 
	{
		 let b = 3 
		 var c = 4 
		 let d = 5 
		 console.log(a) 
		 console.log(b) 
	 } 
	 console.log(b) 
	 console.log(c) 
	 console.log(d)
} 
foo()

接下来我们就来一步步分析上面这段代码的执行流程。

第一步是编译并创建执行上下文
在这里插入图片描述
接下来,第二步继续执行代码
在这里插入图片描述
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶,当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。

变量查找过程
在这里插入图片描述
当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出
在这里插入图片描述

作用域链

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


var myName = 'heihei1';
function foo() {
	console.log(myName);
}
function bar() {
	var myName = 'heihei2';
	foo();
}
bar();

上面代码调用栈如下:
在这里插入图片描述

如果按照调用栈的顺序来查找变量,那么打印出来的myName = ‘heihei2’;

但实际情况并非如此,打印出来的应该是 ‘heihei1’;

作用域链

在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。

当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。

比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。

在这里插入图片描述
我们可以看出来,如果在 bar 函数或者 foo 函数中使用了外部变量,JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。

foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?

词法作用域:

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。


var myName = 'heihei1';
function foo() {
	var myName = 'heihei2'
	function bar() {
		var myName = 'heihei3';
	}
}
foo();

在这里插入图片描述

闭包


var myName = 'heihei1'
function foo() {
	var myName = 'heihei2';
	function bar() {
		console.log(myName);
	}
	return bar;
}
var tmp = foo();
tmp();

在这里插入图片描述

我们可以看到 bar 函数访问了 foo 函数的 myName 属性,根据词法作用域的规则,内部函数总是可以访问外部函数的变量的。所以当 foo 函数返回给全局变量 tmp 的 bar 函数时,虽然 foo 函数已经执行结束,但是还是引用着 foo 的变量。
在这里插入图片描述
当我们执行 tmp() 的时候

在这里插入图片描述
JavaScript 引擎会沿着,当前执行上下文 —> foo 函数闭包 ----> 全局执行上下文 来查找 myName ,所以 myName = ‘heihei1’

闭包是怎么回收的

如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值