【准备面试】-JS - 调用堆栈

这期彻底复刻木易老师的博客,由于写的很好 我这里只是本身复习,没有商用node

木易老师博客地址git

执行上下文

执行上下文总共有三种类型:github

全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。算法

函数执行上下文:存在无数个,只有在函数被调用的时候才会被建立,每次调用函数都会建立一个新的执行上下文。数组

Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,不多用并且不建议使用。浏览器

执行栈

执行栈,也叫调用栈,具备 LIFO(后进先出)结构,用于存储在代码执行期间建立的全部执行上下文。bash

首次运行JS代码时,会建立一个全局执行上下文并Push到当前的执行栈中。每当发生函数调用,引擎都会为该函数建立一个新的函数执行上下文并Push到当前执行栈的栈顶。服务器

根据执行栈LIFO规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文。数据结构

function first() {  
  console.log('Inside first function');  
  second();  
  console.log('Again inside first function');  
}

function second() {  
  console.log('Inside second function');  
}

first();  
console.log('Inside Global Execution Context');

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
复制代码

执行上下文的建立

执行上下文分两个阶段建立:1)建立阶段; 2)执行阶段闭包

建立阶段

一、肯定 this 的值,也被称为 This Binding。

二、LexicalEnvironment(词法环境) 组件被建立。

三、VariableEnvironment(变量环境) 组件被建立。 直接看伪代码可能更加直观

ExecutionContext = {
ThisBinding = , // 肯定this LexicalEnvironment = { ... }, // 词法环境 VariableEnvironment = { ... }, // 变量环境 } This Binding 全局执行上下文中,this 的值指向全局对象,在浏览器中this 的值指向 window 对象,而在nodejs中指向这个文件的module对象。

函数执行上下文中,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数,具体内容会在【this全面解析】部分详解。

词法环境(Lexical Environment) 词法环境有两个组成部分

  • 一、环境记录:存储变量和函数声明的实际位置

  • 二、对外部环境的引用:能够访问其外部词法环境

词法环境有两种类型

  • 一、全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。

  • 二、函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments 对象。对外部环境的引用能够是全局环境,也能够是包含内部函数的外部函数环境。

直接看伪代码可能更加直观

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

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

变量环境 变量环境也是一个词法环境,所以它具备上面定义的词法环境的全部属性。

在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储函数声明和变量( let 和 const )绑定,然后者仅用于存储变量( 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 >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  
   
  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}
复制代码

提高

  • 1.变量提高
console.log(num); //undefined
var num = 1;
复制代码
  • 2.函数提高
foo();  // foo2
var foo = function() {
    console.log('foo1');
}

foo();  // foo1,foo从新赋值

function foo() {
    console.log('foo2');
}

foo(); // foo1
复制代码

执行上下文栈

由于JS引擎建立了不少的执行上下文,因此JS引擎建立了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

有以下两段代码,执行的结果是同样的,可是两段代码究竟有什么不一样?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
复制代码
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
复制代码

答案是 执行上下文栈的变化不同。

第一段代码:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
复制代码

第二段代码:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
复制代码

函数上下文

在函数上下文中,用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象的区别在于

  • 一、变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
  • 二、当进入到一个执行上下文后,这个变量对象才会被激活,因此叫活动对象(AO),这时候活动对象上的各类属性才能被访问。 调用函数时,会为其建立一个Arguments对象,并自动初始化局部变量arguments,指代该Arguments对象。全部做为参数传入的值都会成为Arguments对象的数组元素。

执行过程 执行上下文的代码会分红两个阶段进行处理

  • 一、进入执行上下文

  • 二、代码执行

进入执行上下文 很明显,这个时候尚未执行代码

此时的变量对象会包括(以下顺序初始化):

  • 一、函数的全部形参 (only函数上下文):没有实参,属性值设为undefined。
  • 二、函数声明:若是变量对象已经存在相同名称的属性,则彻底替换这个属性。
  • 三、变量声明:若是变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。 上代码就直观了
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;
}

foo(1);
复制代码

对于上面的代码,这个时候的AO是

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}
复制代码

形参arguments这时候已经有赋值了,可是变量仍是undefined,只是初始化的值

代码执行 这个阶段会顺序执行代码,修改变量对象的值,执行完成后AO以下

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}
复制代码

总结以下:

  • 一、全局上下文的变量对象初始化是全局对象

  • 二、函数上下文的变量对象初始化只包括 Arguments 对象

  • 三、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

  • 四、在代码执行阶段,会再次修改变量对象的属性值

内存

  • 1.基本类型:--> 栈内存(不包含闭包中的变量)
  • 2.引用类型:--> 堆内存

内存回收

JavaScript有自动垃圾收集机制,垃圾收集器会每隔一段时间就执行一次释放操做,找出那些再也不继续使用的值,而后释放其占用的内存。

局部变量和全局变量的销毁

  • 局部变量:局部做用域中,当函数执行完毕,局部变量也就没有存在的必要了,所以垃圾收集器很容易作出判断并回收。
  • 全局变量:全局变量何时须要自动释放内存空间则很难判断,因此在开发中尽可能避免使用全局变量。

以Google的V8引擎为例,V8引擎中全部的JS对象都是经过来进行内存分配的

  • 初始分配:当声明变量并赋值时,V8引擎就会在堆内存中分配给这个变量。
  • 继续申请:当已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止。

V8引擎对堆内存中的JS对象进行分代管理

  • 新生代:存活周期较短的JS对象,如临时变量、字符串等。
  • 老生代:通过屡次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

垃圾回收算法

对垃圾回收算法来讲,核心思想就是如何判断内存已经再也不使用,经常使用垃圾回收算法有下面两种。

  • 1.引用计数(现代浏览器再也不使用)
  • 2.标记清除(经常使用)

引用计数

引用计数算法定义“内存再也不使用”的标准很简单,就是看一个对象是否有指向它的引用。若是没有其余对象指向它了,说明该对象已经再也不须要了。

// 建立一个对象person,他有两个指向属性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};

person.name = null; // 虽然name设置为null,但由于person对象还有指向name的引用,所以name不会回收

var p = person; 
person = 1;         //原来的person对象被赋值为1,但由于有新引用p指向原person对象,所以它不会被回收

p = null;           //原person对象已经没有引用,很快会被回收
复制代码

引用计数有一个致命的问题,那就是循环引用

若是两个对象相互引用,尽管他们已再也不使用,可是垃圾回收器不会进行回收,最终可能会致使内存泄露。

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 

    return "cycle reference!"
}

cycle();
复制代码

cycle函数执行完成以后,对象o1和o2实际上已经再也不须要了,但根据引用计数的原则,他们之间的相互引用依然存在,所以这部份内存不会被回收。因此现代浏览器再也不使用这个算法。

可是IE依旧使用。

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};
复制代码

上面的写法很常见,可是上面的例子就是一个循环引用。

变量div有事件处理函数的引用,同时事件处理函数也有div的引用,由于div变量可在函数内被访问,因此循环引用就出现了。

标记清除(经常使用)

标记清除算法将“再也不使用的对象”定义为“没法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发没法触及到的对象被标记为再也不使用,稍后进行回收。没法触及的对象包含了没有引用的对象这个概念,但反之未必成立。因此上面的例子就能够正确被垃圾回收处理了。

算法由如下几步组成:

  • 一、垃圾回收器建立了一个“roots”列表。roots 一般是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被看成 root 。window 对象老是存在,所以垃圾回收器能够检查它和它的全部子对象是否存在(即不是垃圾);

  • 二、全部的 roots 被检查和标记为激活(即不是垃圾)。全部的子对象也被递归地检查。从 root 开始的全部对象若是是可达的,它就不被看成垃圾。

  • 三、全部未被标记的内存会被当作垃圾,收集器如今能够释放内存,归还给操做系统了。

现代的垃圾回收器改良了算法,可是本质是相同的:可达内存被标记,其他的被看成垃圾回收。

内存泄漏

对于持续运行的服务进程(daemon),必须及时释放再也不用到的内存。不然,内存占用愈来愈高,轻则影响系统性能,重则致使进程崩溃。 对于再也不用到的内存,没有及时释放,就叫作内存泄漏(memory leak) 内存泄漏识别方法

  • 一、浏览器方法 打开开发者工具,选择 Memory 在右侧的Select profiling type字段里面勾选 timeline 点击左上角的录制按钮。 在页面上进行各类操做,模拟用户的使用状况。 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用状况。
  • 二、命令行方法 使用 Node 提供的 process.memoryUsage 方法。
console.log(process.memoryUsage());

// 输出
{ 
  rss: 27709440,		// resident set size,全部内存占用,包括指令区和堆栈
  heapTotal: 5685248,   // "堆"占用的内存,包括用到的和没用到的
  heapUsed: 3449392,	// 用到的堆的部分
  external: 8772 		// V8 引擎内部的 C++ 对象占用的内存
}
复制代码

判断内存泄漏,以heapUsed字段为准。

WeakMap

ES6 新出的两种数据结构:WeakSetWeakMap,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。

const wm = new WeakMap();
const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"
复制代码

先新建一个 Weakmap 实例,而后将一个 DOM 节点做为键名存入该实例,并将一些附加信息做为键值,一块儿存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

四种常见的JS内存泄漏

一、意外的全局变量

未定义的变量会在全局对象建立一个新变量,以下。

function foo(arg) {
    bar = "this is a hidden global variable";
}
函数 foo 内部忘记使用 var ,实际上JS会把bar挂载到全局对象上,意外建立一个全局变量。

function foo(arg) {
    window.bar = "this is an explicit global variable";
}
另外一个意外的全局变量可能由 this 建立。

function foo() {
    this.variable = "potential accidental global";
}

// Foo 调用本身,this 指向了全局对象(window)
// 而不是 undefined
foo();
复制代码

解决方法:在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。若是必须使用全局变量存储大量数据时,确保用完之后把它设置为 null 或者从新定义。

二、被遗忘的计时器或回调函数

计时器setInterval代码很常见

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
复制代码

上面的例子代表,在节点node或者数据再也不须要时,定时器依旧指向这些数据。因此哪怕当node节点被移除后,interval 仍旧存活而且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。

var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}

element.addEventListener('click', onClick);
复制代码

对于上面观察者的例子,一旦它们再也不须要(或者关联的对象变成不可达),明确地移除它们很是重要。老的 IE 6 是没法处理循环引用的。由于老版本的 IE 是没法检测 DOM 节点与 JavaScript 代码之间的循环引用,会致使内存泄漏。 可是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经能够正确检测和处理循环引用了。即回收节点内存时,没必要非要调用removeEventListener了。

三、脱离 DOM 的引用

若是把DOM 存成字典(JSON 键值对)或者数组,此时,一样的 DOM 元素存在两个引用:一个在 DOM树中,另外一个在字典中。那么未来须要把两个引用都清除。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // 更多逻辑
}
function removeButton() {
    // 按钮是 body 的后代元素
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}
复制代码

若是代码中保存了表格某一个<td> 的引用。未来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的<td>之外的其它节点。实际状况并不是如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。因为代码保留了<td> 的引用,致使整个表格仍待在内存中。因此保存 DOM 元素引用的时候,要当心谨慎。

4.闭包

闭包的关键是匿名函数能够访问父级做用域的变量。

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
    
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

setInterval(replaceThing, 1000);
复制代码

每次调用 replaceThing ,theThing 获得一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。someMethod 能够经过 theThing 使用,someMethod 与 unused 分享闭包做用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。

解决方法:在 replaceThing 的最后添加 originalThing = null 。

相关文章
相关标签/搜索