前言
本章说一下JavaScript的执行上下文和作用域
一、执行上下文是什么?
执行上下文是评估和执行JavaScript代码环境的抽象概念。
1.执行上下文的种类
执行上下文一共包含以下三种:
- 全局执行上下文:任何不在函数内部的代码均在全局上下文中。
注意: 一个程序里面有且仅有一个全局执行上下文。 - 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个函数上下文。
注意: 函数上下文可以有任意多个。 - Eval函数执行上下文:执行在eval函数内部的代码也会有属于自己的执行上下文。(由于此类函数在日常开发中很少见,我们只谈全局执行上下文和函数执行上下文)。
2.执行栈
执行栈: 是一种LIFO(后进先出)的数据结构栈,用来存储代码运行时创建的所有执行上下文。当JavaScript引擎第一次遇到脚本时,他会创建一个全局的执行上下文并压入当前的执行栈。每当引擎遇到一个函数的调用,他会为该函数创建一个执行上下文并压入执行栈的顶部。引擎会执行那些执行上下文位于栈顶的函数,当函数执行完成时,执行上下文从栈顶弹出,控制流程到达当前栈中的下一个执行上下文。
3.创建执行上下文
创建执行上下文分为两个阶段:创建阶段 和 执行阶段
创建阶段又分为三部分:
- this的绑定
- 创建词法环境组件
- 创建变量环境组件
this的绑定: 在全局上下文中,this的值指向全局对象(在浏览器里面,this指向window对象);在函数执行上下文中,this的指向取决于函数是如何被调用的。如果函数被一个引用对象调用,那么this指向那个对象,否则this的值被设置为全局对象或是undefined(在严格模式下)
词法环境: 是一种规范类型,基于ECMAScript代码的词法结构来定义标识符和具体变量以及函数的关联。一个词法环境由环境记录器和一个可能引用外部词法环境的空值组成。
其中,环境记录器指的是:存储变量和函数声明的实际位置;外部环境的引用:意味着它可以访问其父级词法环境。
词法环境分为两种:
1.全局环境(在全局执行上下文中) 没有外部环境引用的词法环境。全局环境的外部引用是null。
它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且this的值指向全局对象。
2.函数环境: 函数内部用户自定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器分为两种:
1.声明式环境记录器: 存储变量、函数以及参数
2.对象环境记录器: 定义出现在全局上下文中的变量和函数的关系
变量环境: 同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
注意: 在ES6中,词法环境组件和变量环境组件的一个不同:词法环境组件被用来存储函数声明和变量(let和const)声明,而变量环境只用来存储var声明的变量
执行阶段: 在此阶段,完成对所有这些变量的分配,最后执行代码
执行上下文案例一:
function first() {
console.log('第一行');
second();
console.log('第二行');
}
function second() {
console.log('第三行');
}
first();
console.log('第四行');
// 输出:
第一行
第三行
第二行
第四行
解释: 首先JavaScript引擎创建一个全局执行上下文,将全局执行上下文压入栈顶,遇到first函数执行,将first函数压入栈顶,随后输出“第一行”,紧接着遇到second函数执行,将其压入栈顶,随后输出“第三行”,second函数从栈顶弹出,然后first函数继续执行,输出“第二行”,随后从栈顶弹出。紧接着,全局执行上下文里面的console输出“第四行”,随后执行完毕。JavaScript引擎将全局执行上下文弹出。
执行上下文案例二:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// 输出 local scope
解释:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
执行上下文案例三:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
// 输出 local scope
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
理解this的绑定案例一:
let foo = {
baz: function () {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为没有指定引用对象
二、作用域与作用域链
1.作用域种类
作用域决定了这些变量的可访问性(可见性)
作用域分为:函数作用域、全局作用域、块级作用域
作用域: 即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
函数作用域: 每个函数创建一个新的作用域,在创建函数的时候,函数的作用域就已经确定了
全局作用域: 整个script
标签包含的区域
块级作用域: 在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量
函数内部定义的变量从函数外部是不可访问的(不可见的)
注意: JavaScript里面的作用域均属于静态作用域或词法作用域:变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了
2.作用域链
变量对象: 与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明
作用域链: 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
作用域案例一:
var a = 1;
console.log(window.a); //1
this.window.b = 2;
console.log(this.b); //2
解释: 浏览器JavaScript中,全局对象有window属性指向自身
作用域案例二:
function myFoo() {
console.log(a1);
a1 = 1;
}
myFoo();
// 报错 Uncaught ReferenceError: a1 is not defined
解释: a1未用let或const或var定义。
作用域案例三:
console.log(foo);// 打印函数
function foo(){
console.log("foo");
}
var foo = 1;
解释: 在进入执行上下文时,首先会处理函数声明,其次会处理变量声明
作用域案例四:
var sex = '男';
function person() {
var name = '张三';
function student() {
var age = 18;
console.log(name); // 张三
console.log(sex); // 男
}
student();
console.log(age); // Uncaught ReferenceError: age is not defined
}
person();
解释:
student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出“张三”
student内部输出cat时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出“男”
在person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到则报错