JavaScript中的执行上下文、作用域链和闭包详解

1 执行上下文

在我的另一篇博客《JavaScript变量提升原理详解和实例》中讲到过JavaScript代码在编译阶段,会为其创建上下文执行环境,而该执行环境所用到的变量信息都存在执行上下文环境中的环境变量对象中,这里就来详细讲解下JavaScript中有关执行上下文相关知识,还会涉及一个重要的概念—调用栈

执行上下文是JavaScript执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,然后确定该函数在执行期间用到的变量:this、变量、函数等。

1.1 什么情况下会创建执行上下文

在JavaScript代码编译阶段,以下三种情况会创建执行上下文:

  • 开始编译JavaScript代码的时候,会创建一个全局执行上下文,存在所有的全局变量和函数,并且在整个JavaScript执行周期都存在,直到网页或者浏览器关闭。
  • 当调用一个函数的时候,会编译函数体内的代码并创建该函数的执行上下文。函数调用结束,该执行上下文便会被销毁。
  • 使用eval函数的时候,eval代码会被编译并且创建执行上下文。

在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。

也就是说,当一个函数被调用的时候,会创建该函数的执行上下文,那么JavaScript引擎是如何管理全局执行上下文和各函数的执行上下文的呢?

1.2 调用栈

数据结构栈是一种先进后出的线性结构,函数调用的时候,会执行入栈操作,函数执行结束会执行出栈操作。对于JavaScript来说,当代码开始编译的时候,全局执行上下文会被入栈,当一个函数被调用的时候,其执行上下文会被入栈,执行结束后出栈。这个用来管理执行上下文的栈就被称为调用栈

下面通过一个例子讲解全局执行上下文和函数执行上下文在调用栈中的入栈、出栈,以及执行上下文中的环境变量对象的变化。

var a = 10;
function add(b,c){
	return b+c;
} 
function sum(b,c){
	var d = 2;
	var result = add(b,c);
	return a+result+d;
} 
sum(3,4);

(1)创建全局执行上下文,压入调用栈栈底
(2)执行赋值操作:a=10
(3)调用sum函数,将sum函数的执行上下文入栈
在这里插入图片描述
(4)执行赋值操作:d=2
(5)在sum函数中调用add函数,将add函数的执行上下文入栈
在这里插入图片描述
(6)add函数调用结束,返回结果7赋值给result
在这里插入图片描述
(7)sum函数调用结束
(8)整个js代码执行结束

2 作用域链

当在一个作用域中访问某个变量或者调用某个函数的时候,会先在自己的执行上下文变量对象中查找这个变量,如果没有找到则继续沿着一个链条指向的下一个执行环境中查找,直到找到这个变量或者到达了全局执行环境。这个链条就叫做作用域链。

当代码在一个环境中执行时,会创建变量对象的一个作用域链

作用域链保证了在一个作用域中对执行环境有权访问的所有变量和函数的有序访问。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。这个指针列表的第0个位置始终都是当前执行的代码所在环境的变量对象,下一个位置指向包含环境,即假如该函数的外层还定义了一个函数,则这个位置指向这个外层函数的变量对象,在下一个位置指向下一个包含环境,这样一直延续到作用域的的最后一个位置——全局执行环境。标识符的解析就是通过这个作用域链一级一级查找标识符的过程,所以最开始说作用域链的作用就是保证对执行环境有权访问的所有变量和函数的有序访问

下面通过几个例子来加深我们对作用域链的理解。

例1:

var color = "blue";
function changeColor(){
	var anotherColor = "red";
	function swapColors(){
		var tempColor = anotherColor;
		anotherColor = color;
		color = tempColor;
		//这里可以访问tempColor、anotherColor和tempColor
	}
	//这里可以访问color和anotherColor,但不能访问tempColor
	swapColors();
}
//这里只能访问color
changeColor();

例2:

var a = 1;
function b(){
    var a = 2;
    function c(){
        var a = 3;
        console.log(a);  //3
    }
    c();
}
b();

在console.log(a); 的时候,先在函数c的作用域中找变量a,找到了就打印出a的值,否则一直往上级作用域查找(实际就是先取作用域链指针列表的0号位置指向的变量列表,再取下一个位置),直到找到全局执行环境。

例3:

function bar() {
	console.log(myname);
} 
function foo() {
	var myname = "a";
	bar();
} 
var myname = "b";
foo();

这个例子需要好好分析下,一眼看上去,foo函数调用bar函数,那么在bar函数中取myname的值应该访问的是foo函数中的myname变量,但实际上在bar函数中访问的是全局执行上下文中的myname变量,作用域链如下图所示:
在这里插入图片描述
要解释这个原因,还需要了解JavaScript中一个重要的概念——词法作用域

2.1 词法作用域

词法作⽤域是指作⽤域是由代码中函数声明的位置来决定的,而不是由函数调用的位置决定的。

就像上面那个例子,bar函数的声明位置是在全局执行环境中,因此它的作用域链只包含两个指向:一个指向自己的环境变量,一个指向全局环境变量。

词法作用域是根据代码的位置来决定的,JavaScript作用域链是由词法作用域决定的。

如果把例3的代码改成下面这样,则bar函数的作用域链的1号位置指向foo函数的执行环境,2号位置指向全局执行环境,在bar函数中取myname字段的时候,从作用域链的0号位置开始搜索,在1号位置取到myname的值a。

function foo() {
    var myname= "a";
    function bar() {
        console.log(myName); //a
    }
    bar();
}
var myName = "b";
foo();

3 闭包

先通过一个例子来引入闭包的概念:

例1:

function foo() {
    var myname = "a";
	let test1 = 1;
	const test2 = 2;
    var innerBar = {
        getName:function(){
            console.log(test1);
            return myname
        },
        setName:function(newName){
            myname = newName
        }
    };
    return innerBar
}

var bar = foo();
bar.setName("b");
bar.getName();
console.log(bar.getName())

结果:1 1 b

当代码执行到return innerBar的时候,调用栈如下图:
在这里插入图片描述
根据第2节作用域链的知识讲解,内部函数getName和setName总是可以访问其外部函数foo中的变量,因此即使当foo函数执行结束后,getName和setName函数依旧可以访问foo函数中的test1变量和myname变量。foo函数执行结束后的调用栈如下图:
在这里插入图片描述
也就是说,即使foo函数的执行环境上下文已经出栈了,但是由于getName和setName函数使用了foo函数中的test1变量和myname变量,因此这两个变量依旧保存在内存中。此外,当foo函数执行结束后,其中的test1变量和myname变量只能由getName和setName函数访问,这就很像是getName和setName函数的一个专属变量,这些变量的集合就称为闭包。

闭包的概念
在JavaScript中,根据作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。例如外部函数是foo,那么这些变量的集合就称为foo函数的闭包。

这里的内部函数是指在内部定义的函数,之前词法作用域中说过,函数的作用域取决于函数的定义处而不是调用处,因此只有当B函数定义在A函数中时,才符合闭包的概念,而当在A函数返回一个在其他地方定义的函数时,不符合闭包的概念,因为在其他地方定义的函数的作用域链中不会有指向A函数执行环境的指针。

因此,通过bar变量调用setName函数和getName函数的时候,在函数中查找test1变量和myname变量的顺序为:当前执行上下文(调用setName函数或getName函数时产生的上下文)->foo函数的闭包->全局执行上下文。当调用setName函数的时候,调用栈如图:
在这里插入图片描述
例2:

//createComparisonFunction函数的作用是通过传入的对象的不同的属性名从而比较该属性值的大小。
var o1 = {
	a:1,
	b:2
};

var o2 = {
	a:1,
	b:2
};

function createComparisonFunction(propertyName) {
	return function(object1, object2){
		var value1 = object1[propertyName];
		var value2 = object2[propertyName];
			if (value1 < value2){
				return -1;
			} else if (value1 > value2){
				return 1;
			} else {
				return 0;
			}
	};
}
var fun = createComparisonFunction('a');
console.log(fun(o1, o2));

带有闭包的作用域链:
这里写图片描述
可以通过浏览器的调试窗口Sources查看闭包
当执行到createComparisonFunction函数内部时:
在这里插入图片描述
当执行到内部函数时,可以看到createComparisonFunction的闭包:
在这里插入图片描述
例3:

	function createFunction(){
		var result=new Array();
		
		for(var i=0;i<10;i++){
			result[i]=function(){
				return i;
			}
		}
		return result;
	}
	
	var res=createFunction();
	for(var i=0;i<10;i++){
		console.log(res[i]());
	}

这段代码我们预期将0到9放入result数组中,但解决却打印出来十个10,这是为什么?因为闭包只能取得包含函数中任何变量的最后一个值,即res[i]的作用域链中有个指针指向包含函数createFunction的活动对象,而此时该活动对象中i的值已经为10。
修改以上代码如下即可得到预期结果:

function createFunction(){
	var result=new Array();
	
	for(var i=0;i<10;i++){
		result[i]=function(num){
			return function(){
				return num;
			}
		}(i); 
	}
	return result;
}
var res=createFunction();
for(var i=0;i<10;i++){
	console.log(res[i]());
}

例4:

var bar = {
    myName:"c",
    printName: function () {
        console.log(myName);
    }
}
function foo() {
    let myName = "a";
    return bar.printName;
}
let myName = "b";
let _printName = foo();
_printName();
bar.printName();

分析:
(1)在let myName = "b";上打断点,可以看到此时已经有了全局执行上下文变量环境中的bar变量,两个属性的值已经赋值了
在这里插入图片描述
(2)当执行到foo函数的return语句时,红色区域是foo函数的执行上下文,绿色区域是全局执行上下文的词法环境
在这里插入图片描述
(3)当foo函数执行结束后,全局执行上下文的词法环境多个一个变量_printName
在这里插入图片描述
看到这里你就明白了,_printName是属于全局执行上下文的,因此调用该函数的时候,会在全局执行上下文中查找myName变量。
(4)当执行bar.printName();的时候,再第一步的图中就圈出了bar变量,它是属于全局执行执行上下文环境变量的,因此打印的还是全局执行上下文中的myName变量。

这个例子很好地用到了前面所说的词法作用域和闭包的概念:

  1. 函数的作用域取决于它的定义处,而不是调用处。printName函数定义的地方在全局执行上下文环境中,因此该函数中访问的变量只能是全局执行环境中的变量。
  2. 虽然在foo函数中返回值是一个函数,但是这个函数并不是在foo函数中定义的,因此不符合闭包的概念,调用_printName的时候自热也就不会产生foo函数的闭包。

4 从数据存储的角度看闭包原理

第3节只是从原理上讲解了下闭包的作用和概念,这里再从数据存储的角度看闭包原理。
我在《JavaScript数据类型及其存储方式》一文中讲解了JavaScript中,简单类型的变量就存储在栈空间中,引用类型的变量存储在堆空间中。
第3节中说foo函数的闭包存储在内存中,即使foo函数执行结束,通过bar.setName或者bar.getName也能访问到foo函数中的变量,那么这个闭包具体是如何存储的呢?
这里从数据存储的角度分析:
当JavaScript引擎遇到内部函数的时候,会对内部函数做一次词法分析,发现内部函数使用了外部函数中的myname变量和test1变量,于是,在堆空间中创建foo函数的闭包,保存myname变量和test1变量,而test2变量没有被内部函数用到,因此继续保留在栈中。
当foo函数执行到return innerBar;的时候,调用栈和堆空间如下图:
在这里插入图片描述

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值