這期徹底復刻木易老師的博客,由於寫的很好 我這裏只是本身複習,沒有商用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 。