搞懂JS关于作用域和执行上下文

743 阅读6分钟

执行上下文

定义

执行上下文:javascript 代码解析和执行时所在的环境。

我们时刻使用函数,通过函数实现各种逻辑处理,函数之间可以实现调用,当函数调用结束时,程序会通过执行上下文回到函数调用的位置。

执行上下文可以分为全局执行上下文和函数执行上下文,执行上下文都有两个阶段:创建阶段和执行阶段

1.全局执行上下文:

  1. 创建阶段在执行全局代码前创建
  • 创建全局执行对象:浏览器端就是window,node中是global
  • 对全局数据进行预处理:
    • var定义的全局变量==>undefined, 添加为window的属性(变量提升)
    • function声明的全局函数==>赋值(fun), 添加为window的方法 (函数提升)(1.2属于创建全局对象)
    • 确定this指向==>赋值(window)
  1. 执行阶段: 执行全局代码

全局上下文被压入执行栈底(后面介绍),逐行执行代码,执行到函数调用部分进入函数的执行上下文,函数执行完后出栈,以此类推 全局代码执行结束,全局的执行上下文对象销毁

2.函数执行上下文:

函数每次被调用一次,就会创建一个函数执行上下文

函数的数据每次调用都是互相独立,属于各自的执行上下文对象

函数调用结束,执行上下文对象就被销毁了

  1. 创建阶段:在调用函数的时候,执行函数内的语句之前,创建函数的执行上下文

  2. 对函数内的数据进行预处理:

    • 给形参赋值(实参),作为执行上下文对象的属性
    • 给arguments赋值(实参的集合),作为执行上下文对象的属性
    • 使用var 声明的变量,提升(赋值 undefined),作为执行上下文对象的属性
    • 使用 function 声明的函数提升(值),作为执行上下文对象的方法
    • 确定this的指向,this指向调用函数的对象,全局直接调用this指向window
  3. 执行阶段: 执行函数内的语句

3.执行栈

栈存储结构:先进后出

用来存储执行上下文

具体执行流程如下

  • 首先创建全局执行上下文, 压入栈底
  • 每当调用一个函数时,创建函数的函数执行上下文。并且压入栈顶
  • 当函数执行完成后,会从执行上下文栈中弹出,js引擎继续执栈顶的函数。

如以下函数执行时的执行栈变化为:

function fun1(){
    console.log('func1')
    fun2()
}
function fun2(){
    console.log('func2')
}
fun1()  
/*
*                     fun2
*           fun1      fun1       fun1   
* global => global => global => global => global
*/

4.执行上下文生命周期 (对照1.2理解创建阶段

ES5 规范去除了 ES3 中变量对象和活动对象,以词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。

创建阶段:

此阶段执行上下文会执行以下操作

  1. 确定 this 的值,也被称为 This Binding
  2. LexicalEnvironment(词法环境) 组件被创建
  3. VariableEnvironment(变量环境) 组件被创建

执行阶段

  • 变量赋值。函数引用,执行其他代码逻辑
  • 当执行完毕后。执行上下文出栈,等待垃圾回收机制回收

销毁阶段

作用域

作用域概念可以分为全局作用域和块级作用域(包含函数作用域)

1. 全局作用域

变量提升:

  • 使用var关键字声明的变量,会在所有代码执行之前声明
    • a的输出为undefined
  • 但是如果不使用var关键字,变量不会被声明提前
    • 如果用let关键字:报错Cannot access before initialization

函数的声明提前

  • 使用函数声明创建的函数:在所有代码执行之前就被创建 且被存储
  • 使用函数表达式创建的函数:不会被提前创建,函数变量是undefiend但是不能调用

2. 函数作用域

1.定义 ⭐

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是 其声明时所在的作用域 ,与其运行时所在的作用域无关--阮一峰JS

var a = 1;
var x = function () {
  console.log(a);
};

function f() {
  var a = 2;
  x();
}

f() // 1

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。

函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

2.作用域链:

var a=10;
function fun1(){
  var b=20;
  var a="fun1 a"
  function fun2(){
    console.log(a)
  }
  fun2()
}
fun1();

3.省略关键字的情况

var c=33;
function fun(){
  console.log(c);
  c=10
}
fun();
//console.log(c);
  • c=10 输出33,因为没有用var定义 没有变量提升,找到全局的c输出
  • var c=10 输出undefined 变量提升
  • let c =10 报错
  • var c 输出undefined

上面的code中在全局中console.log(c)结果是10 在函数中直接赋值变量都会变成全局变量

4.形参相当于在函数作用域中声明变量

var e =23;
function fun(e){
  console.log(e)
}
fun();

函数的调用结果是undefined,传入形参e相当于在函数作用域中声明 var e;

3.块级作用域

{} 会创建块级作用域,具有块级作用域的变量用 let 或 const 声明

  1. if 语句
  2. switch 语句
  3. for 语句
  4. while 语句
  5. 直接在 {} 中写代码

4.闭包

函数外无法读取函数内的变量(作用域链),当f1嵌套f2时,想要f2读取f1中的变量,要解决这个问题可以将f2作为其返回值,此时f2就是闭包:

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

闭包的用处有3个:

  • 一个是可以读取外层函数内部的变量,
  • 另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
  • 闭包的另一个用处,是封装对象的私有属性和私有方法。 阮一峰JS

3.作用域和执行上下文的关系⭐

  • 函数作用域是在函数声明的时候就已经确认,函数执行上下文是 函数调用时确定
    • 但是函数中的变量是在调用的时候创建

区别:

  1. 创建的时间点不同:
作用域执行上下文
函数:函数定义的时候创建函数作用域函数调用时创建执行上下文
全局:页面打开时创建在全局作用域确定之后,代码执行之前创建
  1. 属性不同:
  • 作用域是静态的,只要函数确定好了就一直存在,而且不会变化
  • 上下文环境是动态的

参考资料: