Javascript 执行机制

执行流程

先编译,再执行。 浏览器

JS代码执行流程

  • 编译阶段:进行变量提高,变量与函数会被存放到变量环境中,变量的默认值被设为 undefined.若存在两个相同的函数,最终存放在变量环境中的是后面那个。若是函数带有参数,编译过程当中,参数会经过参数列表保存在变量环境中。
  • 执行阶段:JS 引擎会从变量环境中去查找自定义的变量和函数。

哪些状况下代码在执行以前会编译并建立执行上下文?

  1. 当 JS 执行全局代码的时候,会编译全局代码并建立全局执行上下文,并且在整个页面生存周期内,全局执行上下文只有一份。
  2. 当调用一个函数的时候,函数体内的代码会被编译,并建立函数执行上下文,通常状况下,函数执行结束以后,建立的函数执行上下文会被销毁。
  3. 当使用 eval 的时候,eval 的代码也会被编译,并建立执行上下文。

调用栈

  • 栈溢出是如何产生的?
    当调用一个函数时,会给他建立一个执行上下文 push 到栈中,执行完毕从栈中 pop。若函数内部又调用了其余函数,内部又调用其余函数...,不断将执行上下文往栈中 push 却没有 pop,超过必定数量就会致使栈溢出报错。闭包

    没有终止条件的递归会一直建立新函数的执行上下文压入栈中,超过栈容量的最大先以后就会报错;app

    能够经过把递归改形成其余形式、加入定时器拆分任务等方法来解决。函数

    调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,经过调用栈就能追踪到哪一个函数正在被执行和各个函数间的调用关系。工具

如何用好调用栈?

  • 利用浏览器查看调用栈信息
    函数调用关系
  • 加入 console.trace() 输出当前函数调用关系
    使用 trace 打印调用栈信息

JS 中 let,const,{} 如何实现块级做用域?

ES6 以前的做用域

  • 全局做用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数做用域就是在函数内部定义的变量或者函数只能在函数内部被访问。函数执行结束以后,函数内部定义的变量被销毁。

变量提高带来的问题

  1. 变量容易被覆盖
chat* myname = "geek time";
void showName() {
  printf("%s \n", myname);  // 'geek time'
  if(0){
    chat* myname = "Hei ha";
  }
}
int main(){
  showName();
  return 0;
}
复制代码

最终打印为 'geek time'ui

var myname = "geek time";
function showName() {
  console.log(myname);
  if (0) {
    var myname = "Hei ha";
  }
}
showName();
复制代码

最终打印为 undefinedthis

  1. 本应销毁的变量没有销毁
function foo() {
  for (var i = 0; i < 7; i++) {}
  console.log(i);
}
foo(); //7
复制代码

输出为 7,变量 i 在 foo 循环结束后并无被销毁,说明在建立执行上下文阶段,变量 i 就已经被提高了。spa

在其余语言中,for,if,while,{},函数块等内部变量执行完后就会被销毁。设计

ES6 如何解决变量提高带来的问题?

经过 var 声明的变量,在编译阶段被放进变量环境,而经过 let,const 声明的被放进词法环境(Lexical Environment);
3d

let声明变量-1
每一个块级做用域内的 let,const 声明又被放进词法环境的一个单独区域中。
let声明变量-2
看成用域块执行结束后,内部定义的变量就会从词法环境的栈顶弹出,从而实现和其余语言同样的变量销毁。
let声明变量-3

做用域链与闭包

做用域链

  • 下面代码输出什么?
function bar() {
  console.log(myName);
}
function foo() {
  var myName = "极客邦";
  bar();
}
var myName = "极客时间";
foo();
复制代码

执行bar时的调用栈
按上面调用栈顺序来分析,那么结果应该是 极客邦; 实际答案是 极客时间
带有外部引用的调用栈
每一个执行上下文的环境中都包含了一个外部引用,用来指向外部执行的上下文,上图中的 outer。

当一段代码使用一个变量时,JS 引擎首先在“当前执行上下文(bar)”中查找该变量,若没有,则在 outer 所指向的执行上下文中查找,这个查找链条就是做用域链

  • 问题:那么 foo 中调用的 bar,为何 bar 的外部引用是全局执行上下文而不是 foo 函数的执行上下文?

由于在 JS 执行过程当中,做用域链是由词法做用域决定的。

词法做用域

词法做用域是由代码中函数声明的位置来决定的,因此词法做用域是静态做用域,经过它能预测代码在执行过程当中如何查找标识符。

词法做用域
上图中,整个词法做用域链的顺序是: foo 函数做用域 -> bar 函数做用域 -> main 函数做用域 -> 全局做用域。

词法做用域是代码阶段就决定好的,和函数怎么调用没有关系。 再看上面的问题,就知道打印的结果为何是“极客时间”了。 若是换成下面的:

function foo() {
  var myName = "极客邦";
  function bar() {
    console.log(myName);
  }
  return bar();
}
var myName = "极客时间";
foo();
复制代码

此时打印的就是“极客邦”了。

块级做用域中的变量查找

function bar() {
  var myName = "浏览器";
  let test1 = 100;
  if (1) {
    let myName = "Chrome 浏览器";
    console.log(test);
  }
}

function foo() {
  var myName = "极客邦";
  let test = 2;
  {
    let test = 3;
    bar();
  }
}

var myName = "极客时间";
let myAge = 10;
let test = 1;
foo();
复制代码

结合上面的做用域链与词法做用域,易得最终输出结果为 1。 查找顺序以下(图中标记的 1,2,3,4,5)

块级做用域的变量查找

闭包

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

执行到 return innerBar 时的调用栈

根据词法做用域的规则易得,内部函数 getNamesetName能够访问 foo 中的 myName 和 test1。因此,当 foo 执行完后,这两个变量成为 foo 闭包的专属变量,除了 setName 和 getName 其余任何地方都没法访问 foo 闭包中的变量。调用栈的状态以下:
闭包的产生过程-1
闭包的产生过程

经过上图能够看出,当执行到 foo 时,闭包就产生了,foo 结束后,getName 与 setName 都引用了clourse(foo) 对象,因此即便 foo 函数结束了,clourse(foo)依然被其内部的 getName 和 setName 引用,调用这两个方法时,建立的执行上下文就包含了 clourse(foo)

  • 站在内存模型角度分析代码的执行流程
  1. 执行 foo 函数,编译、建立执行上下文。
  2. 编译过程当中,遇到 setName,发现其中使用了外部函数的变量myName,因而生成一个闭包环境来存放 myName 变量。
  3. 接着扫描,又遇到 getName 发现函数内部有使用了外部变量,JS 引擎又将 test1 存放到闭包中。
  4. test2 没有被函数内部引用,因此依然保存在执行栈中。
  • 产生闭包的核心两步:
  1. 预扫描内部函数
  2. 把内部函数引用的外部变量保存到堆中

闭包如何使用?

当执行 bar.setName() 方法中的 myName = 'xxx' 时,JS 引擎会沿着“当前执行上下文 -> foo 函数闭包 -> 全局执行上下文”的属性来查找,以下:

执行bar.setName时调用栈状态

Chrome 开发者工具中在 innerBar 的函数中打断点,刷新页面也可查看闭包状态。
开发者工具中闭包展现

经过 Scope便可查看做用域链的状况。

闭包如何回收?

若是引用闭包的函数是全局变量,那么闭包会一直存在到页面关闭;但若是这个闭包之后再也不使用的话,就会形成内存泄漏。

若是引用闭包的函数是局部变量,等函数销毁后,下次 JS 引擎执行垃圾回收时,判断闭包若是已再也不被使用,就会回收这块内存。

综上所述,若闭包一直使用,则做为全局变量,不然为局部变量。

this

this 是和执行上下文绑定的,执行上下文有全局、函数、eval 执行上下文,故对应的 this 也有这三种。

执行上下文中的 this

全局执行上下文中的 this

window

函数执行上下文中的 this

  1. 经过 call,bind,apply 设置
let bar = {
  myName: 'x'
}
function foo() {
  this.myName = 'xxx'
}
foo.call(bar)
复制代码
  1. 经过对象调用方法设置
var myObj = {
  name: 'x',
  showThis() {
    console.log(this)
  }
}
myObj.showThis()  // 等同于 myObj.showThis.call(myObj)
var foo = myObj.shiwThis
foo() // window
复制代码

在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。 经过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象自己。
3. 经过构造函数中设置 new 运算符

this 的设计缺陷

  1. 嵌套函数中的 this 不会从外层继承 this 没有做用域限制,因此嵌套函数不会从调用它的函数中继承。
var myObj = {
  name: 'jk',
  showThis() {
    console.log(this) // myObj
    function bar() {
      console.log(this)
    }
    bar() // window
  }
}
复制代码

解决办法:1. 外层绑定 this 2. 箭头函数
2. 普通函数中的 this 默认指向全局对象 window 严格模式下,默认执行一个函数,这个函数执行上下文中的 this 是 undefined

some question

  1. 第一题
showName();
var showName = function() {
  console.log(2);
};
function showName() {
  console.log(1);
}
复制代码

输出 1,第一个 showName 带 var 通过变量提高后被赋值为 undefined,变量 showName 会被下面同名函数覆盖,再次执行 showName 就为 2,具体过程以下

// 编译
var showName = undefined;
function showName() {
  console.log(1);
}
// 执行
showName(); // 1
showName = function() {
  console.log(2);
};
showName(); // 2
复制代码
  1. 第二题
let myname = "geek time";
{
  console.log(myname);
  let myname = "Hei ha";
}
复制代码

最终的打印结果不是 undefined.
而是:Cannot access 'myname' before initialization 缘由:在块级做用域内,let 变量只是建立被提高,初始化并无被提高,在初始化以前使用变量,会造成一个暂时性死区。

  • var 的建立和初始化被提高,赋值不会被提高。
  • let 的建立被提高,初始化和赋值不会被提高。
  • function 的建立、初始化和赋值均会被提高。
  • 暂时性死区:
    执行函数时才有进行编译,抽象语法树(AST)在进入函数阶段就生成了,而且函数内部做用域已经明确了,因此进入块级做用域不会有编译过程,只不过经过 let 或者 const 声明的变量会在进入块级做用域时才被建立,可是在该变量没有赋值以前,引用该变量 JavaScript 引擎会抛出错误---这就是“暂时性死区”

参考资料

浏览器工做原理与实践

相关文章
相关标签/搜索