JavaScript深入理解系列(3):执行上下文与作用域

定义

每次当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文。执行上下文(简称-EC)是ECMA标准里一个比较抽象的概念,用于同可执行代码(executable code)概念进行区分。

活动的执行上下文组在逻辑上组成一个堆栈。堆栈底部永远都是全局上下文(global context),而顶部就是当前(活动的)执行上下文。堆栈在EC类型进入和退出上下文的时候被修改(推入或弹出)。

简单一句话概括:上下文就是js运行的一个环境。分别是全局上下文、函数上下文、eval上下文(很少用)

在JavaScript引擎创建执行上下文栈(Execution context stack,ECS)的时候,通过堆栈模式管理执行上下文。我们假想一下,我们在文件里面写了很多函数,如果不用这些管理的话,那肯定很乱,让人完全掌握不了代码运行的规律。所以用这种方法来管理是明智的。

那么,我们可以定义执行上下文栈是一个数组:

ECStack = [];

全局代码

当我们把js文件运行的时候,JavaScript引擎首先就是通过<script>标签来解析文件内部的代码,最先运行的就是全局,那么可以先把全局的上下文(globalContext)放进执行上下文栈(ECStack)里面,当全局上下文运行完毕,我们就把上下文栈(ECStack)清空。这个很简单,相当于执行这个数组里面的函数或者变量,程序运行完毕,就自动清空,释放内存。那么用代码怎么表示呢,看下文:

ECStack = [
    globalContext
]

注意:这个全局代码,不包括function体内的代码。

函数代码

代码:

function fn1() {
	fn2();
}

function fn2() {
	fn3();
}

function fn3() {
	console.log('fn3');
}

fn1();

分析:当执行fn1方法的时候,fn1函数体内执行方法fn2,fn2执行的时候,fn2函数体内有fn3,那就继续执行方法fn3。所以执行顺序 fn1 => fn2 => fn3

结论: 当程序执行函数的时候,自动创建一个执行上下文,并且把这个执行上下文push进上下文栈(ECStack)。看下面标识:

// 演示流程
var ECStack = [
    globalContext
]

// 执行fn1,创建的执行上下文push进上下文栈(ECStack)
ECStack.push(<fn1> functionContext)

// 执行fn2
ECStack.push(<fn2> functionContext)

// 执行fn3
ECStack.push(<fn2> functionContext)

// 此时数组,只为演示
ECStack = [
    <fn3> functionContext, // 后进先出,先执行
    <fn2> functionContext,
    <fn1> functionContext,
    globalContext
]

// 好了,调用fn1函数,创建了fn1,fn2,fn3,3个执行上下文
// 执行顺序,基于堆栈数据结构特点,后进先出(Last-In/First-Out)

// fn3执行完毕
ECStack.pop();

// fn2执行完毕
ECStack.pop();

// fn1执行完毕
ECStack.pop();

// globalContext执行,JavaScript里面globalContext永远都是最后执行的。全局上下文
// 程序结束

作用域与作用域链

作用域

根据MDN解释,指当前的执行上下文。分别是全局作用域、局部作用局

直白一点,作用域就是变量与函数的可访问范围,即作用域限制变量和函数的可见性和生命周期,访问范围分为全局作用域(window)、局部作用局(例如:函数内部)。简单说一下,函数和变量在什么时候调用,什么时候销毁,都是跟作用域有关。

全局作用域(Global Scope)

在JavaScript代码里面任何地方都能访问到的对象拥有全局作用域,一般分这几种情况:

  • 1、最外层函数和在最外层函数外面定义的变量拥有全局作用域

  • 2、所有末定义直接赋值的变量自动声明为拥有全局作用域

  • 3、所有window对象的属性拥有全局作用域

一般情况下,window对象的内置属性都拥有全局作用域,例如:window.width, window.height等等。

var name = '程序员米粉';

function fn() {
	console.log(name);
    function bar() {
        console.log(name);
    }
    bar() // '程序员米粉'
}
fn() // '程序员米粉'  name变量定义在全局作用域,任何一个函数内都可以访问

局部作用域(Local Scope)

刚好和全局作用局相反,只能在局部范围内才能访问的环境。最最最常见的就是在函数的内部,一般也称之为函数作用域。或者在ES6里面使用let、const声明变量,作用域的作用域称之为块级作用域,块级作用域由最近的一对包含花括号{}界定。

var name = '222';
function fn() {
    var name = '程序员米粉';
	console.log(name);
}
fn() // '程序员米粉  222'  name变量定义在局部作用域,只能在当前函数运行环境(函数作用域)查找变量,假如当前的找不到,就往上一层作用域查找。

作用域链

看一看下面这个例子(高程三里面的):

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

可以看到上面的代码涉及了3个作用域:全局作用域、changeColor()的局部作用域和swapColors()的局部作用域。全局作用域中有一个变量color和一个函数changeColor()。changeColor()的局部作用域中有一个变量anotherColor和一个函数swapColors(),但在这里可以访问全局作用域中的变量color。swapColors()的局部作用域中有一个变量tempColor,只能在这个作用域中访问到。全局作用域和changeColor()的局部作用域都无法访问到tempColor。而在swapColors()中则可以访问另外两个作用域中的变量,因为它们都是父级作用域。图示:

在这里插入图片描述

从图可以看出来,不同的矩形,表示不同的作用域,内部的作用域可以通过作用域链访问外部作用域的一切,不过一般常见都是访问外部作用域中的函数、变量、等等。上下文之间都是线性连贯的、有序、层次分明的。里面的函数可以访问上一级任何的变量和函数,但是外部的函数访问不了里面函数的任何东西。例如上图。

  • swapColors函数在作用域链里有3个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。swapColors从自身函数里面先寻找自身的变量和对象,寻找不到再往上一级寻常,直到寻找到window上面,假如没有的话就抛出异常。

  • changeColor函数在作用域链里有2个对象:changeColor()的变量对象、全局变量对象。也是先从自身函数里面寻找自身的变量和对象,寻找不到再往上一级寻常,直到寻找到window上面,假如没有的话就抛出异常。

参考

  • 高级程序三

结语

希望看完这篇文章对你有帮助:

  • 执行上下文
  • 作用域与作用域链

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注,后续会输出更好的分享。

欢迎关注公众号:【程序员米粉】
公众号分享开发编程、职场晋升、大厂面试经验

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值