JavaScript 代码的执行

223 阅读4分钟

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」。

JavaScript 代码的执行主要分为以下三步:

  • 分析有没有词法、语法错误
  • 预编译-发生在函数执行的前一刻,生成变量环境、词法环境
  • 解释执行

而 JavaScript 比较有特色的就是在预编译阶段,我们重点看看预编译阶段做了什么事情。

预编译 引擎一开始会创建执行上下文(也叫Activation Object、AO对象),执行上下文主要有如下三种类型:

  • 全局执行上下文:只有一个
  • 函数执行上下文:存在无数个,每个函数被调用就新建一个
  • Eval执行上下文:eval中运行的函数代码,很少用

执行上下文的创建主要分为创建阶段和执行阶段

创建阶段
1.绑定 this 指向

2.创建词法环境

3.生成变量环境

这里解释一下词法环境和变量环境,其实他们两个是差不多相同的组件。词法环境中包含两个部分,一个是存储变量与函数声明的位置,另一个是对外部环境的引用。

伪代码如下:

GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {          // 词法环境
    EnvironmentRecord: {           // 环境记录
      Type: "Object",                 // 全局环境
      // 标识符绑定在这里 
      outer: <null>, // 对外部环境的引用
    }                   
  }  
}

FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: {        // 词法环境
    EnvironmentRecord: {          // 环境记录
      Type: "Declarative",         // 函数环境
      // 标识符绑定在这里               // 对外部环境的引用
      outer: <Global or outer function environment reference>  
  }  
}

变量环境也是一个词法环境,词法环境和变量环境的区别在于:

  • 词法环境存储函数声明和绑定 let 和 const 变量
  • 变量环境仅绑定 var 变量

执行阶段 完成对所有变量的分配,最后执行代码

作用域&作用域链 每个 JavaScript 函数都是一个对象,对象中有的属性可以访问,有的不能,这些属性仅供 JavaScript 引擎存取,如[[scope]] 。

[[scope]]就是函数的作用域,其中存储了执行上下文的集合。

[[scope]]中所存储的执行上下文对象的集合,这个集合呈链式链接,我们称这种链式链接为作用域链。查找变量时,要从作用域链的顶部开始查找。在当前执行上下文中找不到变量时,则到对外部环境的引用中向上查找,故呈现一个链式结构。

作用域与变量声明提升 •在 JavaScript 中,函数声明与变量声明会被 JavaScript 引擎隐式地提升到当前作用域的顶部 •声明语句中的赋值部分并不会被提升,只有名称被提升 •函数声明的优先级高于变量,如果变量名跟函数名相同且未赋值,则函数声明会覆盖变量声明 •如果函数有多个同名参数,那么最后一个参数(即使没有定义)会覆盖前面的同名参数

闭包 当内部函数被保存到外部时,将会生成闭包。生成闭包后,内部函数依旧可以访问其所在的外部函数的变量。 当函数执行时,会创建执行上下文,获取作用域链(存储了函数能够访问的所有执行上下文)。函数每次执行时对应的执行上下文都是独一无二的,当函数执行完毕,函数都会失去对这个作用域链的引用, JS 的垃圾回收机制是采用引用计数策略,如果一块内存不再被引用了那么这块内存就会被释放。

但是,当闭包存在时,即内部函数保留了对外部变量的引用时,这个作用域链就不会被销毁,此时内部函数依旧可以访问其所在的外部函数的变量,这就是闭包。

即闭包逃过了 GC 策略,故滥用会导致内存泄漏,其实本身就是一种内存泄漏?

经典题目

for (var i = 0; i < 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 100)
}
function test() {
   var a = [];
   for (var i = 0; i < 5; i++) {
         a[i] = function () {
            console.log(i);
         }
   }
   return a;
}

var myArr = test();
for(var j=0;j<5;j++)
{
   myArr[j]();
}

以上两个例子都打印5个5,简单解释就是变量 i 记录的是最终跳出循环的值,即5,可以通过立即执行函数或者 let 来解决。因为立即执行函数创建了一个新的执行上下文,可以保存当前循环 i 的值,而let则构建了块级作用域,也可以保存当前循环 i 的值。

for (var i = 0; i < 5; i++) {
   ;(function(i) {
      setTimeout(function timer() {
         console.log(i)
      }, i * 100)
   })(i)
}
function test(){
   var arr=[];
   for(i=0;i<10;i++)
   {
      (function(j){
         arr[j]=function(){
         console.log(j);
         }
      })(i)
   }
   return arr;
}

var myArr=test();
for(j=0;j<10;j++)
{
   myArr[j]();
}
封装私有变量
function Counter() {
   let count = 0;
   this.plus = function () {
      return ++count;
   }
   this.minus = function () {
      return --count;
   }
   this.getCount = function () {
      return count;
   }
}

const counter = new Counter();
counter.puls();
counter.puls();
console.log(counter.getCount())
计数器
实现一个foo函数 可以这么使用:

a = foo();
b = foo();
c = foo();
// a === 1;b === 2;c === 3;
foo.clear();d = foo(); //d === 1;
function myIndex() {
    var index = 1;

    function foo(){
        return index++;
    }

    foo.clear = function() {
        index = 1;
    }
    return foo;
}

var foo = myIndex();