浏览器中的JavaScript执行机制

174 阅读6分钟

1.变量提升:

demo1:

  showName();
      console.log(hisName);
      var hisName = "大飞老师";
      function showName() {
        console.log("执行showName函数");
      }

在声明之前使用函数与变量的结果 : image.png demo2:

showName();
      console.log(hisName);
      function showName() {
        console.log("执行showName函数");
      }

使用了未定义代码报错: image.png 从上面两段代码的执行结果我们可以得出三个结论:

  1. 在执行过程中,若使用了未声明的变量,那么JS引擎执行会报错
  2. 在一个被var声明的变量定义之前使用它,不会出错,但值为undefined
  3. 在一个函数定义之前使用它,不会出错,且函数能正确的执行。

对于第二第三个结论,要解释这两个问题就需要了解下什么是变量提升,在此之前我们先了解下js代码的执行流程

js代码的执行流程:

  1. 编译阶段:JS代码经过编译后会生成两部分内容:执行上下文和可执行代码。 执行上下文是一段JS代码的运行环境,比如调用一个函数,就会进入该函数的执行上下文,确定该函数在执行期间的诸如:this、变量、对象、以及函数、作用域链等。执行上下文存在变量环境和词法环境,(var声明的变量以及函数声明)变量提升的内容都保存在这个变量环境中,let,const声明的变量则被保存在词法环境中。 代码可以分为声明部分与其他部分,声明部分会被保存到变量环境中,而其他部分会被编译为字节码,也就是可执行代码。

    我们可以简单理解变量环境对象的结构类似这样:

VariableEnviroment:
    myName :undefined,
    showName:function(){...}
  1. 执行阶段:JS引擎开始按照顺序一行一行执行可执行代码。
  • 执行代码过程中,遇到变量赋值、访问变量或者函数调用等,会先去当前执行上下文的词法环境中查找、返回给JS引擎;如果没有找到再查找变量环境,如果还找不到,则会去查找其外层的父作用域,直到找到标识符或者查找到全局作用域为止。

image.png

变量提升:

所谓的变量提升,就是在代码编译阶段,根据代码声明部分,创建变量,并将其放入到执行上下文的词法环境(let、const声明的变量)或者变量环境中,当我们执行代码遇到变量赋值、访问、函数调用时,会先去相应的词法环境再去变量环境中去查找对应标识符,然后再进行相应操作。

注意:声明一个变量可以分为创建、初始化、赋值这三个步骤。

  • var的创建与初始化会被提升,赋值不会被提升。
  • let的创建被提升,初始化与赋值不会被提升。
  • function的创建、初始化、和赋值均会被提升(函数提升优先级高于变量提升)。 demo3:
 let myName = "mumu";
      {
        console.log(myName);
        let myName = "nunu";
      }

当访问一个变量时,首先从当前执行上下文的词法环境中查找标识符,没找到则到执行上下文的的变量环境对象中接着查找...在当前代码块中打印myName变量时,报如下错误,则说明,let变量也存在变量提升,只是let声明的变量的创建被提升,初始化并未提升,所以访问就会报未初始化错误。只有等到变量赋值时才可以访问该变量,这也就是所谓的暂时性死区

image.png

2.调用栈:

JS引擎是通过栈结构来管理执行上下文的。在执行上下文创建好后,JS引擎会将其压入栈中,这种管理执行上下文的栈称为调用栈,也叫执行上下文栈。程序初始化时js引擎创建全局执行上下文,然后将其压入到栈中,在函数调用时创建函数执行上下文并压入到栈中,到函数执行完毕,对应的函数执行上下文弹出栈。

在开发中如何利用好调用栈?

当一次有多个函数被调用时,通过调用栈(执行上下文栈)就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。 调试代码时,可以通过浏览器的开发者工具或者在函数插入console.trace()来查看当前函数的调用栈信息。在分析复杂代码结构或者检查bug时,调用栈都非常有用。

栈溢出:

调用栈是有大小的,当入栈的执行上下文到达一定数目时,JS引擎就会抛出的错误,这种错误我们称为栈溢出。

解决栈溢出的方法:

  • 把递归形式转换为迭代形式 。
  • 使用定时器来处理递归函数,将大任务拆分为小任务。
  • 注意设置递归函数的终止条件。

3.作用域、块级作用域:

作用域:

定义:

作用域是指在程序中定义变量的区域,该位置决定了变量和函数的生命周期与可见性(也就是变量所属范围)。

分类:

  • 全局作用域:全局作用域中的变量在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域:在函数内部定义的变量或者函数,只能在函数内部访问,函数执行结束之后,函数内部定义的变量会被销毁。
  • 块级作用域:ES6之前是不支持块级作用域的,为什么ES6会提出块级作用域这个概念呢?因为变量提升,带来了一定的问题。

变量提升带来的问题:

变量容易在不被察觉的情况下被覆盖:

  var myname = "mumu"
function showName(){
  console.log(myname);//undefined
  if(0){
   var myname = "nunu"//变量提升导致全局变量被覆盖
  }
  console.log(myname);//undefined
}
showName()

本应销毁的变量未销毁:

function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); // 7
}
foo()

所以ES6引入了let 、 const ,从而使JavaScript也像其他语言一样有了块级作用域。 注意:词法环境内部维护了一个栈,维护代码块之间的关系。栈底保存的是最外层的变量,进入一个代码块将其变量压入栈顶,一个代码块执行完毕就弹出栈。

4.作用域链、词法作用域和闭包:

作用域链:

每个执行上下文都有一个外部引用指向其外部的执行上下文环境,我们把这个外部引用称为outer。当一段代码使用一个变量时,JS引擎首先会在当前执行上下文中查找该变量,没查找到在outer所指向的执行的执行上下文中查找。直到查到到或者找到全局执行上下文为止,这个查找的链条就被称为作用域链

词法作用域:

词法作用域就是指作用域是由代码中函数的声明位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测到代码在执行过程中是如何查找变量、函数等标识符的。词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没关系。

闭包:

在一个嵌套函数中,内部函数使用外部函数的局部变量,同时调用这个外部函数会返回内部函数,即使外部函数已经执行结束,其执行上下文弹出栈,但那些被内部函数所引用的变量依旧会保存在内存中,我们把这些变量的集合称为这个外部函数的闭包

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

我们可以在作用域链中查看Closure对象,里面保存的就是这些外部变量: image.png

闭包经典使用场景

  1. 循环赋值(循环绑定事件索引问题)
  //循环赋值;
      for (var i = 0; i < 10; i++) {
        (function (j) {
          let i = j;
          setTimeout(function () {
            console.log(i);
          }, 1000);
        })(i);
      }

执行了十次匿名函数,创建十个不同的闭包,及时函数执行上下文弹出栈了,在内存中销毁了,它们依然存在于内存中,并且彼此之间互不干扰。

image.png

  1. 节流防抖
  // 节流
      function throttle(fn, timeout) {
        let timer = null;
        return function (...arg) {
          if (timer) return;
          timer = setTimeout(() => {
            fn.apply(this, arg);
            timer = null;
          }, timeout);
        };
      }

      // 防抖
      function debounce(fn, timeout) {
        let timer = null;
        return function (...arg) {
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.apply(this, arg);
          }, timeout);
        };
      }
  1. 柯里化实现
  function curry(fn, len = fn.length) {
        return _curry(fn, len);
      }

      function _curry(fn, len, ...arg) {
        return function (...params) {
          let _arg = [...arg, ...params];
          if (_arg.length >= len) {
            return fn.apply(this, _arg);
          } else {
            return _curry.call(this, fn, len, ..._arg);
          }
        };
      }

      let fn = curry(function (a, b, c, d, e) {
        console.log(a + b + c + d + e);
      });

      fn(1, 2, 3, 4, 5); // 15
      fn(1, 2)(3, 4, 5);
      fn(1, 2)(3)(4)(5);
      fn(1)(2)(3)(4)(5);

5.this:

JS中的this:每一个执行上下文中都有一个this。this的指向是在代码执行时确定,取决于函数的使用方式;而不是在哪里定义。

  1. 以函数形式调用时,this 永远都是 window (未开启严格模式)如果开启严格模式则为undefined。
  2. 以方法的形式调用时,this 是调用方法的对象
  3. 以构造函数的形式调用时,this 是新创建的那个实例对象
  4. 使用 call 和 apply 调用时,this 是指定的那个对象
  5. 箭头函数:箭头函数本身没有执行上下文,它的this取决于定义箭头函数时所处的执行上下文环境。也就是看它的外层函数。如果没有外层函数,就是 window。

image.png