作用域/执行上下文

225 阅读11分钟

1. 解析与执行

一般js代码执行需要先经过解析,生成计算机能解析的语言再执行

1.1解析

var a = 100;
  1. 词法分析:分析token,把语句解析成记号,如 var , a , = , 100 , ;
  2. 语法/解析分析:将token数组 根据一定的语法规则 转化成AST(抽象语法树)
  3. 代码生成:生成浏览器js引擎能解析的底层代码。

1.2执行

在js引擎中执行代码分为:分析阶段执行阶段

 1.2.1分析阶段

根据分析创建对应的信息

  1. VO(Variable Object)对象:该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。

  2. 作用域链:VO(当前作用域) + ParentScope(父级作用域)

  3. this的指向: 视情况而定。

    注意:变量作用域通过词法作用域确定,也就是定义的时候已经确定一些变量的值。
    

1.2.2执行阶段

根据对应的作用域访问变量,同时创建执行的上下文。

  • 引擎询问作用域,作用域中是否有这个叫X的变量
  • 如果作用域有X变量,引擎会使用这个变量
  • 如果作用域中没有,引擎会继续寻找(向上层作用域),如果到了最后都没有找到这个变量,引擎会抛出错误。

执行阶段的核心就是找,使用LHS查询RHS查询

var a = 2; // LHS 查询

LHS (Left-hand Side)(=)的左侧,变量赋值或写入内存。想象为将文本文件保存到硬盘中。

RHS (Right-hand Side)(=)的右侧,变量查找或从内存中读取。想象为从硬盘打开文本文件。

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );

直接看引擎在作用域这个过程: LSH(写入内存):

c=, a=2(隐式变量分配), b=

RHS(读取内存)

读foo(2), = a, a ,b
(return a + b 时需要查找ab)

2.作用域

2.1定义

  • 程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。限制谁能否访问该作用域的变量。
  • 也可以理解为:作用域就是对象,对应的变量就是对象属性。

作用:

防止不同范围的变量互相干扰。

2.2分类

2.2.1 全局作用域:

  • 程序的最外层作用域
  • 不在任何函数内容
  • 会随容器一直存在。浏览器上就是window对象,所有的变量和函数都在window上创建。

全局变量 : 保存在全局的变量

优点

  • 可以反复使用,访问方便

缺点

  • 会造成全局污染
  • 占用空间,页面不关闭,不被释放

2.2.2 函数/局部作用域:

只在函数被定义时才会创建,不同函数之间互相独立。函数结束就销毁。

//全局作用域
var a = 10

function myFun1() {
//这里是函数myFun1作用域,当方法调用的时候会动态创建,是独立的作用域
 var b = 20
 console.log(b) //访问函数myFun1作用域的 b 这里是20 
 var b2 = 200
}

function myFun2() { 
//这里是函数myFun2作用域
 var b = 30 //这里与myFun1中的b 不冲突,由于互相独立,所以可以同时定义
 console.log(b) //访问函数myFun2作用域的 b,成功, 这里是30 
 console.log(b2) //访问函数myFun1作用域的 b2,失败, 函数的作用域互相独立。
}

console.log(a) //访问全局作用域的 a 成功

局部变量 : 保存在函数作用域下的变量,包括传入的形参

function myfun(a,b){
    var c = 10; 
}  
//这里的 内部参数 c 和 形参 a,b 都是局部变量

优点:

  • 数据隔离,不会被污染

缺点:

  • 无法反复使用
注意

只有函数的 {} 包裹的区域才是局部作用域,其他都不是 (箭头函数也没有作用域和this概念)

var obj = {
      a:"1",
      b:"2"
}

这里 {} 就不是一个作用域, a也不是作用域变量

2.3 es6的块级作用域

js的块级: 花括号内 {...} 内的代码片段。如 if,for,switch,while的语句

js实际是没有块级作用域,会导致变量的内容超出{}的范围,影响到外部。

console.log(a)
if(false) {  //即使条件为false ,内容的定义的 var a 依然能影响到外部,做声明提前。
    var a = 10
}
console.log(a)
//这里正常打印 
undefined
10

在ES6引进了 let和const声明 实现块级作用域,但是底层还是匿名函数自调

for (let i =0; i<10;i++){
    console.log(i)
}
//等价于
for (let i =0; i<10;i++){
    function(i) {
        console.log(i)
    }(i);  // 定义 匿名函数+执行
}

2.3.1 不会变量提升

console.log(bar);//抛出`ReferenceError`异常: 某变量 `is not defined`,这里不存在变量提升
let bar=2;
for (let i =0; i<10;i++){
    console.log(i)
}
console.log(i);//抛出`ReferenceError`异常: 某变量 `is not defined` 这里访问不了块作用域的i
 

2.3.2 不能重复声明

// var
function test(){
    var name = 'aaaa';
    var name = 'bbbb';
    console.log(name); // bbbb
}

// let || const
function test2(){
    var name ='aaaa';
    let name= 'bbbb'; 
    // Uncaught SyntaxError: Identifier 'count' has already been declared
}
 

2.3作用域链(scope chain)

函数作用域优先访问自己内部的变量,没有再往上继续查找,由此形成了作用域链。

  注意只有往上查找,没有往下的逻辑

访问的这个变量也叫:自由变量

//全局
var a = 10

function myFun1() {
 var b = 20
}

function myFun2() { 
 var c = 30
 console.log(a) //访问全局作用域的 a 成功 通过作用域链一直往上找,自由变量a
 console.log(b) //访问函数作用域的 b 报错
}

console.log(a) //访问全局作用域的 a 成功
console.log(b) //访问函数myFun1作用域的 b 报错,不能父级往子访问。
console.log(c) //访问函数myFun2作用域的 c 报错,不能父级往子访问。
 

嵌套的列子


//这里是全局作用域
var a = 10
function myFun1(p1) {
    //这里是myFun1函数作用域
     var b = 20
     function myFun2(p2) {  
         //这里是myFun2函数作用域
         console.log(a) //访问全局作用域的 a 成功 通过作用域链一直往上找
         console.log(b) //访问函数myFun1作用域的 b 成功
    }
     myFun2()
}
myFun1() //

嵌套的关系
全局作用域 -> myFun1作用域 -> myFun2作用域

  1. 全局作用域 :标识符包含 a 和 myFun1

  2. myFun1作用域 :标识符包含 p1 , b , myFun2

  3. myFun2作用域 :标识符包含 p2

从未声明变量赋值 会提升到全局,不会报错

function a() {
    function b(){
        age = 10; //这里会被提升到全局 ,不会报错
    }
     b()
} 
a()
console.log(age) //这里输出10

如果是访问,则还是会报错

function a() {
    function b(){
       console.log(age); //age is not defined
    }
    b()
} 
a()

2.4词法作用域

定义:函数被定义的时候,它的作用域就已经确定了,跟执行没有关系,也可以叫 “静态作用域”。

JS使用的是词法作用域

var a = 1;
function fun1() {
  console.log(a);
}
function fun2() {
  var a = 2;
  fun1();
}
fun2();
// 结果是 1 访问的是全局的变量 1 

这里的fun1在定义的时候,已经根据定义时的关系,把a分析出来,自由变量a取得是全局的a = 1,已经预设好了,所以在执行的过程中即使在fun2里面执行了 var a = 2, 也不影响

例子

1.全局和参数命名一样

var a = 10
function myfun(a){
    a++ //这里从作用域链查找,优先找到的是参数a,而不是全局的a
    console.log(a) //这样也是就近原则
}
myfun(a)//输出11
console.log(a)//输出10

js里面 参数是都是拷贝一份,因为a是基本数据类型,赋值多一份10在方法里操作,方法结束后,方法参数a也会销毁

作用域是对象

var age = 10;
function fun() {
    var age = 100;
    age++;
    console.log(age);
}
fun();
console.log(age);
  1. 代码执行前 scope只包含全局Global(Window) ,里面有一个age全局变量

image.png

image.png

  1. 当代码执行到 函数内的 console.log(age);会看到scope 一个数组 里面有两个scope image.png

作用域链式对象的集合数组 scope = [Local,Global]

  • Local 函数作用域
  • Global(Window) 全局作用域

各自存储了自己的作用域 image.png

window里包含fun的 等价于 Local image.png

当函数fun调用结束后,作用域链变回scope只包含全局Global(Window)

image.png

2.7 函数执行的三个步骤

  1. 备料:创建作用域对象+局部变量
  2. 执行代码
  3. 垃圾回收

函数的作用域链

一个是自己,另外一个是外部

2.6其他知识

2.5.1声明提前 hoisting

在任意代码执行前处理的,会把未声明的变量做提升处理,放置到当前作用域的最前面。

  • var命令会发生“声明提前”现象,即变量可以在声明之前使用,值为undefined
  • let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

2.5.1.1 未声明

var tmp = new Date();
function f() {
    console.log(tmp);
    if(false) {
        var tmp='hello';
    }
}
f()//输出undefined

结果为undefined

因为js是词法作用域,所以在静态解析时候会忽略false判断,然后把var tmp 提升到console.log(tmp);前声明 var tmp;

2.5.1.2 重复声明-变量

当提升的变量已经存在声明,提升的动作会被忽略,只做赋值操作。即按顺序至上而下,第一个声明了后面都忽略。

var name = 'aaa';
console.log(name); //  aaa
if(true){
    var name = 'bbb';
    console.log(name); // bbb
}
console.log(name); //  bbb

2.5.1.3 重复声明-变量+函数

当出现变量和函数命名一样的情况,声明函数最优先提升(并且包含声明的内容)。函数表达式则跟变量效果一样。

console.log(name)
var name = "aaa"
function name() { //声明函数方式
    console.log("bbb")
}
//输出name
//undefined

console.log(name)
var name = "aaa"
var name = function() { //函数表达式
    console.log("bbb")
}
//输出函数
//ƒ name() {
//    console.log("bbb")
//}

2.5.2作用域冲突

全局与局部重复变量名冲突

解决办法

使用立即执行函数IIFE(Immediately Invoked Function Expression

其实所有的es6的块作用域最终还是通过IIFE实现,并没真正意义上的块级作用域,只是通过语法糖模拟。

// module1.js
(function () {
    var a = 1;
    console.log(a);
  })();
  
  // module2.js
  (function () {
    var a = 2;
    console.log(a);
  })(); 

2.5.3 创建作用域的方式

  1. 函数
    function foo () {
      
    } 

2.使用 letconst

   for (let i = 0; i < 5; i++) {
     console.log(i);
   } 
   console.log(i); // ReferenceError
  1. try catch 创建作用域(不推荐)
   try {
    undefined(); // 强制产生异常
   }
   catch (err) {
    console.log( err ); // TypeError: undefined is not a function
   }

   console.log( err ); // ReferenceError: `err` not found 
  1. 使用 eval
   function foo(str, a) {
    eval( str );
    console.log( a, b );
   }
   var b = 2;
   foo( "var b = 3;", 1 ); // 1 3
  1. 使用 with
   function foo(obj) {
    with (obj) {
      a = 2;
    }
   }

   var o1 = {
    a: 3
   };

   var o2 = {
    b: 3
   };

   foo( o1 );
   console.log( o1.a ); // 2

   foo( o2 );
   console.log( o2.a ); // undefined
   console.log( a ); // 2 

例子

阿里 作用域-面试题

 function fn(a, c) {
    console.log(a) // function a
    var a = 123
    console.log(a) // 123
    console.log(c) // function c 
    function a() {}
    if (false) {
        var d = 678 //这里会被申请提前
    }
    console.log(d) // undefined
    console.log(b) // undefined
    var b = function () {} //这是表达式 ,不算是函数声明,所以不在ao创建逻辑里
    console.log(b) // function b
    function c() {}
    console.log(c) // function c
}
 fn(1, 2)

输出结果

[Function: a]
123
[Function: c]
undefined
undefined
[Function: b]
[Function: c]

AO创建与初始化的过程

  1. 创建ao对象
  2. 找形参和变量声明 并当做 AO对象的属性名 值为undefined
  3. 实参和形参相统一
  4. 在函数体里面找函数声明 值赋予函数体
// 第一步 创建
ao : {

}
// 第二步 确定属性
 ao : {
    a: undefined
    c: undefined
    d: undefined
    b: undefined 
 }

 // 第三步 实参赋值
 ao {
    a: 1
    c: 2
    d: undefined
    b: undefined 
 }
 
 // 第四步 函数声明赋值
 ao {
    a: 1
    c: function c() {}
    d: undefined
    b: function c() {} 
 }

3.执行上下文

3.1定义

执行上下文(Execution context)

遇到函数执行的时候,就会创建一个执行上下文。执行上下文是当前 js 代码被解析和执行时所在环境的抽象概念。

3.2分类

  1. 全局执行上下文:这是默认的上下文,任何不在函数内部的代码都在全局上下文中。由浏览器主动调用根函数,包含全局的window对象和this指向这个window ,一个程序中只会有一个全局执行上下文。
  2. 函数执行上下文:在函数执行的时候,都会动态创建的执行上下文,跟函数作用域类似。
  3. Eval 函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

3.3执行3个阶段

3.3.1创建阶段

ExecutionContext = {                 // 执行上下文
    ThisBinding = <this value>,      // this绑定
    LexicalEnvironment = { ... },    // 词法环境
    VariableEnvironment = { ... },   // 变量环境
} 
  1. this绑定:在全局上下文,this指向window,函数调用this指向调用的对象,否则会指到全局或undefined
  2. 词法环境
  3. 回收阶段

3.3.2执行阶段

执行变量赋值、代码执行。

3.3.3回收阶段

执行上下文出栈等待虚拟机回收执行上下文,在执行阶段

3.4执行上下文栈

3.4.1定义

执行上下文栈 (Execution context stack),调用栈,执行栈。 用来维护执行代码时候创建的上下文,是栈的数据结构,后进先出。

3.4.2执行流程

  1. 在执行script代码时,由浏览器主动调用根函数G,创建默认全局执行上下文G,并然后押入栈顶。
  2. 根函数里,调用方法a,创建的函数执行上下文a,并把a押入栈顶,在G的上面。
  3. a函数里,调用方法b,创建的函数执行上下文b,并把b押入栈顶,在a的上面。
  4. 方法a调用完毕,同时执行上下文a也出栈。
  5. 方法b调用完毕,同时执行上下文b也出栈。
  6. 根函数G也执行完毕,全局上下文G也出栈。
//根路径默认会创建一个全局的上下执行栈
function a (){
    b() //函数调用时,创建b的上下文
}
function b (){ 

}
a()  //函数调用时,创建a的上下文

image.png

3.5词法环境

3.5.1定义

相应代码块内标识符与变量值、函数值之间的关联关系的一种体现

3.5.2环境类型

  1. 全局环境
  2. 函数环境

3.5.3环境的两个组件

3.5.3.1 环境记录器:

是存储变量和函数声明的实际位置。不同的环境对应不同的结构

  • 全局环境 使用Type: "Object"对象环境记录器,描述 变量和函数的关系。
  • 函数环境 使用Type: "Declarative"声明式环境记录器,描述 存储变量、函数和参数。
//伪代码
// GlobalExectionContext  // 全局执行上下文
 EnvironmentRecord: {     // 环境记录器:存储变量和函数声明的实际位置
    Type: "Object",      
    // 在这里绑定标识符  
 }

//FunctionExectionContext    // 函数执行上下文
 EnvironmentRecord: {
    Type: "Declarative",
    // 在这里绑定标识符
 }

3.5.3.2. 外部环境的引用:

用于关联外面的环境,形成嵌套关系

//伪代码
GlobalExectionContext = {        // 全局执行上下文
    LexicalEnvironment: {        // 词法环境
        EnvironmentRecord: {     // 环境记录器:存储变量和函数声明的实际位置
            Type: "Object",      
            // 在这里绑定标识符  
        }
        outer: <null>           // 对外部环境的引用:可以访问其父级词法环境
    }
}

FunctionExectionContext = {     // 函数执行上下文
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative",
            // 在这里绑定标识符
        }
        outer: <Global or outer function environment reference>
    }
} 

3.6变量环境

跟词法环境结构一致,区别在于

  • 词法环境 记录的是 let 和 const绑定
  • 变量环境 记录的是 var的绑定 在es5之前只有一个词法环境处理var ,到了es6为了区分let 和const ,多出一个变量环境。把原来词法环境调整为 let 和const ,变量环境处理var。
  1. 词法环境:处理let 和 const
  2. 是变量环境:处理var
let a = 20; 
const b = 30; 
var c; 
function multiply(e, f) {
    var g = 20; 
    return e * f * g; 
} 
c = multiply(20, 30);

对应的 环境伪代码是


GlobalExectionContext = {
    ThisBinding: <Global Object>,
    LexicalEnvironment: {       // 词法环境
      EnvironmentRecord: {
        Type: "Object",
        // 在这里绑定标识符
        a: < uninitialized >,   // let、const声明的变量
        b: < uninitialized >,   // let、const声明的变量
        multiply: < func >      // 函数声明
      }
      outer: <null>
    },
    VariableEnvironment: {     // 变量环境
      EnvironmentRecord: {     
        Type: "Object",
        // 在这里绑定标识符
        c: undefined,         // var声明的变量
      }
      outer: <null>
    }
  }
  
  FunctionExectionContext = {
    ThisBinding: <Global Object>,
    LexicalEnvironment: {         // 词法环境
      EnvironmentRecord: {   
        Type: "Declarative",
        // 在这里绑定标识符
        Arguments: {0: 20, 1: 30, length: 2},   // arguments对象
      },
      outer: <GlobalLexicalEnvironment>
    },
    VariableEnvironment: {        // 变量环境
       EnvironmentRecord: {
         Type: "Declarative",
         // 在这里绑定标识符
         g: undefined            // var声明的变量
       },
       outer: <GlobalLexicalEnvironment>
    }
  }
  • 可以看出 在词法环境中的 a: < uninitialized >, 被定义为未初始化。
  • 而在变量环境 g: undefined 被声明并赋值为undefined。

4.执行上下文与作用域的区别

  • 作用域是编译阶段就已经确定的,因为使用的是词法作用域,不受运行时影响。

  • 但是执行上下文,是代码执行阶段动态分配的。调用了函数就创建,没有则重来没发生,而且执行过程可被修改。

      注 在ES5当中把`variable object`改为了`Lexical Environments`