这期彻底复刻木易老师的博客,由于写的很好 我这里只是本身复习,没有商用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>
}
}
复制代码
console.log(num); //undefined
var num = 1;
复制代码
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)来表示变量对象。
活动对象和变量对象的区别在于
执行过程 执行上下文的代码会分红两个阶段进行处理
一、进入执行上下文
二、代码执行
进入执行上下文 很明显,这个时候尚未执行代码
此时的变量对象会包括(以下顺序初始化):
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 对象
三、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
四、在代码执行阶段,会再次修改变量对象的属性值
内存回收
JavaScript有自动垃圾收集机制,垃圾收集器会每隔一段时间就执行一次释放操做,找出那些再也不继续使用的值,而后释放其占用的内存。
局部变量和全局变量的销毁
以Google的V8引擎为例,V8引擎中全部的JS对象都是经过堆来进行内存分配的
V8引擎对堆内存中的JS对象进行分代管理
垃圾回收算法
对垃圾回收算法来讲,核心思想就是如何判断内存已经再也不使用,经常使用垃圾回收算法有下面两种。
引用计数
引用计数算法定义“内存再也不使用”的标准很简单,就是看一个对象是否有指向它的引用。若是没有其余对象指向它了,说明该对象已经再也不须要了。
// 建立一个对象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) 内存泄漏识别方法
console.log(process.memoryUsage());
// 输出
{
rss: 27709440, // resident set size,全部内存占用,包括指令区和堆栈
heapTotal: 5685248, // "堆"占用的内存,包括用到的和没用到的
heapUsed: 3449392, // 用到的堆的部分
external: 8772 // V8 引擎内部的 C++ 对象占用的内存
}
复制代码
判断内存泄漏,以heapUsed字段为准。
WeakMap
ES6 新出的两种数据结构:WeakSet
和 WeakMap
,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。
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 。