1、执行上下文
执行上下文是js执行一段代码时的运行环境,是编译后生成的
比如当调用一个函数时,就会创建一个该函数的执行上下文;当执行全局代码时,就会编译生成全局执行上下文
执行上下文包括 变量环境 和 词法环境
用var
声明的变量会存储在变量环境
用let
、const
声明的变量会存储在词法环境(全局上下文特殊)
执行上下文可以分为 全局执行上下文 和 函数执行上下文(也称为局部执行上下文),还有一种eval
我们暂不讨论
全局执行上下文
全局执行上下文是最外层的上下文,它的下一级可以是函数执行上下文
全局上下文(全局执行上下文)就是我们常说的window
对象
所有通过var
声明的变量和函数都会成为window
对象的属性和方法
注意:let
和var
声明的变量并不会被定义在全局上下文中,但是仍然可以被作用域链访问到
全局上下文只有在应用程序退出才会被销毁,例如:关闭页面或直接关闭浏览器
函数执行上下文
函数中声明的变量或函数就保存在当前的执行上下文中
与全局上下文不同,函数执行上下文在其所有代码执行完就会被垃圾回收器回收
2、作用域
作用域是指在程序中定义变量的区域
通俗地讲,作用域就是变量和函数的可以访问范围,作用域控制变量和函数的可见性和生命周期
作用域分为全局作用域 和 函数作用域,ES6之后新增了 块级作用域
- 全局作用域:在代码的任何地方都能够被访问,其生命周期伴随着页面的声明周期,只有页面被销毁了,全局作用域的对象才会被回收
- 函数作用域:定义在函数内部的变量和函数,只能在函数内部被访问,在函数执行完后,就会被销毁
执行上下文 和 作用域其实是两个差不多的概念,只不过执行上下文指代的是整体环境,而作用域更关注对象的可见性和生命周期
3、作用域链
在每个执行上下文中的变量环境中,都包含了有一个外部引用**outer
**,用来指向外部的执行上下文
先来看一段代码:
function foo1() {
console.log(namer);
}
function foo2() {
var namer = '张三'
foo1()
}
var namer = '李四'
foo2()
你猜这段代码会输出什么?为什么会输出这个?
输出的当然是我们李四啦,这么可爱
当然,输出李四不是因为他可爱,而是因为他是全局变量,那么为什么会输出全局变量,这就涉及到了作用域链
执行代码的时候,先进行编译
编译完会依次生成 全局执行上下文、foo2
执行上下文、foo1
执行上下文
所以李四住在全局执行上下文的变量环境中(var
声明的保存在变量环境中)
张三自然是住在foo2
执行上下文的变量环境中
当一段代码使用了一个变量时,js 引擎首先会在 ‘当前执行上下文‘ 查找该变量
所以当执行foo1
时,发现它使用了namer
变量,所以就在其执行上下文中查找变量,但是没有找到
这时,js引擎会继续在outer
所指向的执行上下文中查找
发现outer
指向的是全局执行上下文,所以js引擎就会去全局执行上下文中取查找
这个查找的链条我们就称为 作用域链
js引擎发现全局执行上下文中竟然有 namer
变量,所以就返回了的该变量的值李四
现在我们知道了,变量是通过作用域链来进行查找,首先在当前的上下文中查找,若当前的上下文中存在该变量,就直接返回(就近原则);否则,就顺着
outer
指向的外部执行上下文去查找,找到了就返回,找不到就报错。在这些作用域之间进行的查找,查找的这个链条,我们就亲切称之为 作用域链
4、词法作用域
词法作用域又是什么东西?
我猜你会有疑问,上面js引擎沿着作用域链查找变量的时候,外部引用outer
为什么会指向全局上下文,而不是foo2
上下文?
明明foo1
函数还是在foo2
中被调用了,居然去使用全局作用域的变量,这不是吃里扒外嘛??
其实,outer
指向是有规则的,而规则就是由词法作用域定的
词法作用域就是指 作用域是由代码中函数的声明位置来决定的,通过它就能够知道代码在执行过程中是如何查找标识符
看下面这段代码,来辅助我们理解词法作用域:
let namer = '张三'
function foo1() {
let namer = '李四'
function foo2() {
let namer = '王五'
function foo3() {
let namer = '小二'
console.log(namer);
}
foo3()
}
foo2()
}
foo1(); // 小二
整个词法作用域链的执行顺序就如上图一样:foo3
函数作用域 -> foo2
函数作用域 -> foo1
函数作用域 -> 全局作用域
执行foo3
函数时,发现需要用到变量namer
,就在当前的执行上下文中查找,结果找了,就直接引用当前的变量namer
,并且结束查找
如果没有找到,foo3
上下文的变量环境中的outer
引用就会根据词法作用域链指向外部执行上下文foo2
js引擎就会去到该外部执行上下文中去查找变量namer
如果还没有找到,当前的foo2
执行上下文的outer
指针就会指向下一个外部执行上下文继续查找
最外层的执行上下文是全局执行上下文,如果到该层还没有找到所需要的变量,就会报错
你哔哩吧啦说了一推,也不知道对不对,来验证一下就知道了:
let namer = '张三'
function foo1() {
function foo2() {
let namer = '王五'
function foo3() {
console.log(namer);
}
foo3()
}
foo2()
}
foo1(); // 王五
- 当前上下文找不到,就进入外部上下文查找,找到了就返回 王五
let namer = '张三'
function foo1() {
function foo2() {
function foo3() {
console.log(namer);
}
foo3()
}
foo2()
}
foo1(); // 张三
- 前几个执行上下文都找不到,就到全局上下文只找
现在可以来解释一下为什么上面(作用域链那个)的foo1
是去全局上下文中去查找,而不是到foo2
的上下文中去查找
根据词法作用域,foo1
和 foo2
的上级作用域都是全局作用域
因为foo1
只是被foo2
调用,并没有定义在foo2
中
所以他两是同级,可以理解为是 同事关系,不是上下级关系
所以不管是 foo1
还是 foo2
函数,只要它们使用了一个它们没有定义的变量,那么它们都要到全局作用域中去查找
所以说,词法作用域是代码编译阶段就决定好的,和函数怎么调用没有关系
总结
- 作用域分为全局作用域和函数作用域(块级作用域)
- 全局作用域只有整个程序结束的时候才会被销毁;当函数执行完的时候函数作用域就会被立即销毁
- 执行上下文里面包含变量环境和词法环境,全局执行上下文是boss,是最外层的执行上下文
- 当前上下文搜索不到的变量就会到上一级去搜索,不能在同级或者下一级搜索
- 作用域链式由词法作用域决定的