JS笔记《作用域与执行上下文(ES3)》

67 阅读7分钟

作用域

  • 作用域指的是变量存在的范围区域,最大的用处就是隔离变量,各个作用域之间的变量是相互独立的,互不影响。即使同名变量,只要在不同的作用域内,它们就不会有冲突。
  • 在ES5中只有全局作用域函数作用域,ES6新增了块级作用域

全局作用域

  • 代码最外层的区域就是全局作用域,声明的变量就是全局变量,函数内可以读取。

函数作用域

  • 只要创建了函数,就创建了函数作用域,无论是否调用。在函数内声明的变量是局部变量,只在函数内可以使用,外部无法读取。

块级作用域

  • 使用{}花括号包着的区域就是块级作用域,ES6之前没有,一般使用立即执行函数替代。ES6之后,使用letconst定义的变量,就可将变量的作用域限制到当前代码块中。

执行上下文(ES3)

  • 执行上下文是指代码被执行时所在的环境,也可称之为执行环境。JS中运行任何的代码都是在执行上下文中运行。

全局执行上下文

  • 默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。一个程序只能存在一个全局执行上下文

函数执行上下文

  • 每次调用函数时都会为该函数创建一个新的执行上下文,叫做函数执行上下文。每个函数都拥有自己的执行上下文,但只有函数在被调用时才会被创建,函数执行完会被销毁。

执行栈

  • 也叫调用栈,是用来存储执行上下文的一种先入后出的结构,入栈和出栈对应内存的申请和释放。

执行流程

  1. 当JS代码运行的时候,会首先创建一个全局执行上下文(创建阶段)。
  2. 将全局执行上下文压入当前执行栈中(入栈执行阶段)。
  3. 当发生函数调用时,JS会为该函数创建一个函数执行上下文(创建阶段)。
  4. 将函数执行上下文压入当前栈中(入栈执行阶段)。
  5. 函数执行完成后,其对应的函数执行上下文会被移出当前栈(出栈回收阶段)。
  6. 当浏览器关闭时,会将全局执行上下文移出当前栈(出栈回收阶段)。

执行上下文存储结构

  1. 创建变量对象
  2. 创建作用域链
  3. 确定this值

变量对象和活动对象

变量对象(VO)

  • 变量对象是执行上下文的一部分,在它之中保存着当前执行上下文中的变量和函数声明。该对象是隐式的,无法访问。对于浏览器中的全局执行上下文来说,变量对象就是window对象。

活动对象(AO)

  • 对于函数来讲,如果未被调用则不会产生执行上下文,也就没有变量对象。但是当函数被调用时,此时变量对象被激活了,所以我们称之为活动对象。活动对象包含arguments属性、形参以及函数内声明的变量和函数。在函数执行上下文中,活动对象就是变量对象。

全局执行上下文生命周期

  1. 创建一个全局执行上下文
  2. 变量声明,将变量名作为VO属性名,值为undefined
  3. 函数声明,将函数名作为VO属性名,值为函数体。如VO中已存在对应属性名则直接覆盖。
  4. 将全局执行上下文压入到执行栈中,执行具体代码(被提升的变量声明函数声明将不再参与执行)。
  5. 除非关闭浏览器,否则全局执行上下文一直不出栈。
console.log(a); 
var a = 20;
console.log(a); 
function a() {}
console.log(a); 


// 1.创建全局执行上下文:
// 全局执行上下文{
//    变量对象(VO),
//    作用域链,
//    this
// }

// 2. 找变量声明,将变量名作为VO的属性名,值为 undefined
// VO{
//  a: undefined
// }

// 3. 找函数声明,将函数名作为VO的属性名,值为函数体,如VO中已存在对应属性名则直接覆盖
// VO{
//  a: function(){}
// }

// 4. 压入执行栈中,执行代码(被提升的变量和函数不参与执行)
// console.log(a);  // function a(){}
// a = 20;          // 变量声明已被提升,不参与执行
// console.log(a);  // 20
//                  函数已被提升,不参与执行
// console.log(a);  // 20 

函数执行上下文生命周期

  1. 函数被调用。
  2. 创建一个函数执行上下文。
  3. 初始化作用域链。
  4. 形参变量声明,将形参名变量名作为AO的属性名,值为undefined
  5. 实参值赋值给AO中的形参属性。
  6. 在函数体里面找函数声明,将函数名作为AO属性名,值为函数体。如AO中已存在对应属性名则直接覆盖。
  7. 确定this值。
  8. 将函数执行上下文压入到执行栈中,执行具体代码(被提升的变量声明函数声明将不再参与执行)。
  9. 函数执行完毕,函数执行上下文出栈,等待被垃圾回收将其销毁。
function fn(a){
    console.log(a);     
    var a = 123;        
    console.log(a);     
    function a(){}      
    console.log(a);    
    var b = function(){}
    console.log(b);
    function d(){}
}
fn(1);  // 1. 函数被调用

// 2. 创建函数执行上下文:
// 函数执行上下文{
//    活动对象(AO),
//    作用域链,
//    this
// }

// 3. 初始化作用域链
// ...

// 4. 找形参和变量声明作为 AO属性名,值为undefined
AO{
    a : undefined,
    b : undefined
}

// 5. 将实参值和形参统一
AO{
    a : 1,
    b : undefined
}

// 6. 在函数体里面找函数声明,值赋予函数体,将函数名作为 AO属性名
AO{
    a : function a(){},   
    b : undefined,        
    d : function d(){}    
}

// 7. 确定this值
// ...

// 8. 压入执行栈中,执行代码(被提升的变量和函数不参与执行)
console.log(a);   // function a(){}  
a = 123;          // 变量声明已被提升,不参与执行
console.log(a);   // 123  
//                函数a已被提升,不参与执行
console.log(a);   // 123 
var b = function(){}  // AO中的 b 赋值为 function(){}
console.log(b);   // function(){}
//                函数b已被提升,不参与执行

// 9. 函数执行完毕,函数执行上下文出栈,等待被垃圾回收将其销毁。
// 函数执行上下文 = null;

作用域链

  • 执行JS代码时,会创建一个全局执行上下文,压入栈顶。里面的变量对象包含了全局的变量和函数。当函数被调用时,会创建一个函数执行上下文,压入到栈顶。里面的活动对象包含了函数中的变量和函数。如果函数里面还有一个函数,里面的函数在执行时又会创建一个函数执行上下文,包含了函数中的变量和函数,压入到栈顶。这时栈中已经有了三层执行上下文。
  • 此时里面的函数如果访问一个变量,就会在栈顶中寻找该变量,如果找不到再沿着执行栈去上一个执行上下文中寻找,直到找到该变量或已经到达栈底。这种对变量和函数的访问规则呈链式,所以叫做作用域链
var a = 100;
function f(){
    function f2(){
      console.log(a); 
    }
    f2();
}
f();
// 当执行到f2时,执行栈为[f2的AO{}, f的AO{}, GO{a: 100}]
// 打印变量 a:先找执行栈[0],没有再查找执行栈[1],还是没有查找执行[2],找到了,打印!

暗示全局变量

  • 如果未经声明就直接赋值的变量也是全局变量,准确叫做暗示全局变量,会放到VO对象中。
  • 全局变量都是window的属性,只不过window可以忽略不写。
var a = b = 123;    // b是未经声明就赋值的变量,是暗示全局变量
// 1. 123赋值给 b: b = 123;
// 2. 声明变量 a:  var a;
// 3. 将 b赋值给 a:a = b;

console.log(window.a);         // 123  
console.log(window.b);         // 123  
console.log(window.a === a)    // true
console.log(window.b === b)    // true