JavaScript作用域和执行上下文

92 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

来看看两段代码:

 for(var i = 0; i < 10; i++){
     //……
 }
 ​
 console.log(i);//10

代码正常运行,i 的值为10。

 for(let i = 0; i < 10; i++){
     //……
 }
 ​
 console.log(i);//ReferenceError: i is not defined

代码报错,ReferenceError: i is not defined。

为什么使用var声明的i,在for外面就可以访问,而使用let声明的却不能访问到呢?了解完JavaScript的作用域,就能够很好的解释了。

作用域

作用域是在程序运行时代码中的某些特定部分中变量、函数和对象的可访问性,例如文章最开始的两个代码,一个可以访问到i,一个不可以访问到i,说明两个i的作用域是不一样的。

而在 ES6 之前,Javascript只有函数作用域全局作用域,直到ES6出现之后,才有了块级作用域

全局作用域

也就是定义在最外层的变量或者函数,可以在任何地方访问到它们。

 let a = 1;
 function sayHi(){
     console.log('Hello World!');
 }

函数作用域

用函数形式以function(){……}类似的代码包起来的(省略号……)区域,即函数作用域

 function func(){
     var a = 1;//函数作用域
 }
 ​
 console.log(a);//ReferenceError: a is not defined

可以看到,全局作用域无法访问函数作用域中的变量。

块级作用域

ES6规定,在某个花括号对{ }的内部let关键字生声明的变量和函数拥有块级作用域 ,这些变量和函数它们只能被花括号对{ }的内部的语句使用,外部不可访问。在你写下代码的时候,变量和函数的块级作用域就已经确定下来。 块级作用域和函数作用域也可以统称为局部作用域。

两个关键条件:

  • 在花括号{}
  • 使用let关键字声明
 {
   let func = function (){
     return 1;
   }
 }
 console.log(func());//>> func is not defined

ifswitch 条件语句, forwhile 循环语句,不同于函数,它们不会创建一个新的作用域,所以就会出现文章开头的问题,那么在for中使用var声明的变量,就会污染全局环境,所以使用let来声明i,形成块级作用域。

作用域链

多个作用域对应的变量对象串联起来组成的链表就是作用域链,这个链表是以引用的形式保持对变量对象的访问。作用域链保证了当前执行上下文对符合访问权限的变量和函数的有序访问。

 let name = 'window';
 ​
 function sayHi(){
     let name = "function SayHi";
     {
         let name = "Block Name";
         console.log(name);
     }
 }
 ​
 sayHi();//window

依次从内向外注释name的声明,则打印分别是Block Name,function SayHi,window,所以可以得出节流,内部作用域可以访问外部作用域中的变量,如果当前作用域中没找到引用的变量,那么就去外部作用域找,直到全局作用域。

image.png

执行上下文

执行上下文就是当前 JavaScript 代码被解析和执行时所在的环境,也叫作执行环境。

JavaScript 中运行任何的代码都是在执行上下文中运行,在该执行上下文的创建阶段,变量对象、作用域链、this指向会分别被确定。

类型

全局执行上下文

这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:

  1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象;
  2. 将 this 指针指向这个全局对象(window或者global)。一个程序中只能存在一个全局执行上下文。

函数执行上下文

每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。

eval执行上下文

运行在 eval 函数中的代码也获得了自己的执行上下文,ES6 之后不再推荐使用 eval 函数。

生命周期

函数执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段,重点介绍创建阶段。

1、创建阶段

  • 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明(变量的声明提前有赖于var关键字)
  • 创建作用域链:在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
  • 确定this的指向

2、执行阶段

创建完成之后,就会开始执行代码,在这个阶段,会完成变量赋值、函数引用、以及执行其他代码。

3、回收阶段

函数调用完毕后,函数出栈,对应的执行上下文也出栈,等待垃圾回收器回收执行上下文。

执行上下文栈

 var a = "coffe"; //1.进入全局执行上下文
 function out() {
     var b = "18";
     function inner() {
         var c = "91";
         console.log(a+b+c);
     }
     inner(); //3.进入inner函数的执行上下文
 }
 out(); //2.进入out函数的执行上下文

在代码开始执行时,首先会产生一个全局执行上下文,调用函数时,会产生函数执行上下文,函数调用完成后,它的执行上下文以及其中的数据都会被销毁,重新回到全局执行环境,网页关闭后全局执行环境也会销毁。

其实这是一个入栈出栈的过程,全局上下文永远在栈底,而当前正在执行的函数执行上下文在栈顶

img

变量对象

使用var声明变量:

 console.log(a);//undefined
 console.log(func);//[Function: func]
 ​
 var a = 1;
 ​
 function func(){
     //……
 }

使用let声明变量:

 console.log(a);//ReferenceError: Cannot access 'a' before initialization
 ​
 let a = 1;

变量对象(VO)是一个类似于容器的对象,与作用域链、执行上下文息息相关。

变量对象的创建规则:

1、建立arguments对象。检查当前执行上下文中的参数,建立该对象下的属性与属性值。

2、检查当前执行上下文的函数声明,也就是function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果该属性之前已经存在,那么该属性将会被新的引用所覆盖。

3、检查当前执行上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

这也就是为什么使用var声明一个变量,会存在变量提升的现象。

可以用以下伪代码来表示变量对象:

 VO={
     Arguments:{},//实参
     Param_Variable:具体值,//形参
     Function:<function reference>,//函数的引用
     Variable:undefined//其他变量
 }

当执行上下文进入执行阶段后,变量对象会变为活动对象(Active Object,AO)。此时原先声明的变量会被赋值变量对象和活动对象都是指同一个对象,只是处于执行上下文的不同阶段

那么你能解释一下下面的输出吗?

 console.log(a);//[Function: a]
 ​
 var a = 1;
 ​
 function a(){
     //……
 }
 ​
 console.log(a);//1

解释:

1、首先检查当前上下文的函数声明,发现了function a,那么变量对象添加属性a,且指向函数的地址,再检查变量声明,发现了var a,但是a已经存在,所以跳过,那么检测阶段就完成了。

2、接下来进入执行阶段,第一次输出,很显然是[Function: a],接着运行到了赋值语句var a = 1;,此时a的值已经被改变了,所以下一次再输出a,就是重新赋值以后的1了。