執行上下文主要有兩種狀況:javascript
全局代碼: 一段<script>
標籤裏,有一個全局的執行上下文。所作的事情是:變量定義、函數聲明html
函數代碼:每一個函數裏有一個上下文。所作的事情是:變量定義、函數聲明、this、argumentsjava
PS:注意「函數聲明」和「函數表達式」的區別。chrome
在執行全局代碼前將window肯定爲全局執行上下文。數組
(1)對全局數據進行預處理:(並無賦值)瀏覽器
var定義的全局變量==>undefined, 添加爲window的屬性閉包
function聲明的全局函數==>賦值(fun), 添加爲window的方法app
this==>賦值(window)函數
(2)開始執行全局代碼ui
在調用函數, 準備執行函數體以前, 建立對應的函數執行上下文對象(虛擬的, 存在於棧中)。
(1)對局部數據進行預處理:
形參變量==>賦值(實參)==>添加爲執行上下文的屬性
arguments==>賦值(實參列表), 添加爲執行上下文的屬性
var定義的局部變量==>undefined, 添加爲執行上下文的屬性
function聲明的函數 ==>賦值(fun), 添加爲執行上下文的方法
this==>賦值(調用函數的對象)
(2)開始執行函數體代碼
1.在全局代碼執行前, JS引擎就會建立一個棧來存儲管理全部的執行上下文對象
2.在全局執行上下文(window)肯定後, 將其添加到棧中(壓棧)
3.在函數執行上下文建立後, 將其添加到棧中(壓棧)
4.在當前函數執行完後,將棧頂的對象移除(出棧)
5.當全部的代碼執行完後, 棧中只剩下window
this指的是,調用函數的那個對象。this永遠指向函數運行時所在的對象。
解析器在調用函數每次都會向函數內部傳遞進一個隱含的參數,這個隱含的參數就是this。
根據函數的調用方式的不一樣,this會指向不一樣的對象:【重要】
1.以函數的形式調用時,this永遠都是window。好比fun();
至關於window.fun();
2.以方法的形式調用時,this是調用方法的那個對象
3.以構造函數的形式調用時,this是新建立的那個對象
4.使用call和apply調用時,this是指定的那個對象
須要特別提醒的是:this的指向在函數定義時沒法確認,只有函數執行時才能肯定。
this的幾種場景:
例如:
function Foo(name) { //this = {}; this.name = name; //return this; } var foo = new Foo();
var obj = { name: 'A', printName: function () { console.log(this.name); } } obj.printName();
function fn() { console.log(this); //this === window } fn();
做用域指一個變量的做用範圍。它是靜態的(相對於上下文對象), 在編寫代碼時就肯定了。
做用:隔離變量,不一樣做用域下同名變量不會有衝突。
做用域的分類:
全局做用域
函數做用域
沒有塊做用域(ES6有了)
if (true) { var name = 'smyhvae'; } console.log(name);
上方代碼中,並不會報錯,由於:雖然 name 是在塊裏面定義的,可是 name 是全局變量。
直接編寫在script標籤中的JS代碼,都在全局做用域。
在全局做用域中:
在全局做用域中有一個全局對象window,它表明的是一個瀏覽器的窗口,它由瀏覽器建立咱們能夠直接使用。
建立的變量都會做爲window對象的屬性保存。
建立的函數都會做爲window對象的方法保存。
全局做用域中的變量都是全局變量,在頁面的任意的部分均可以訪問到。
變量的聲明提早:
使用var關鍵字聲明的變量( 好比 var a = 1
),會在全部的代碼執行以前被聲明(可是不會賦值),可是若是聲明變量時不是用var關鍵字(好比直接寫a = 1
),則變量不會被聲明提早。
舉例1:
console.log(a); var a = 123;
打印結果:undefined
舉例2:
console.log(a); a = 123; //此時a至關於window.a
程序會報錯:
函數的聲明提早:
函數聲明
的形式建立的函數function foo(){}
,會被聲明提早。也就是說,它會在全部的代碼執行以前就被建立,因此咱們能夠在函數聲明以前,調用函數。
函數表達式
建立的函數var foo = function(){}
,不會被聲明提早,因此不能在聲明前調用。很好理解,由於此時foo被聲明瞭,且爲undefined,並無給其賦值function(){}
。
因此說,下面的例子,會報錯:
調用函數時建立函數做用域,函數執行完畢之後,函數做用域銷燬。
每調用一次函數就會建立一個新的函數做用域,他們之間是互相獨立的。
在函數做用域中能夠訪問到全局做用域的變量,在全局做用域中沒法訪問到函數做用域的變量。
在函數中要訪問全局變量可使用window對象。(好比說,全局做用域和函數做用域都定義了變量a,若是想訪問全局變量,可使用window.a
)
提醒1:
在函數做用域也有聲明提早的特性:
使用var關鍵字聲明的變量,會在函數中全部的代碼執行以前被聲明
函數聲明也會在函數中全部的代碼執行以前執行
所以,在函數中,沒有var聲明的變量都會成爲全局變量,並且並不會提早聲明。
舉例1:
var a = 1; function foo() { console.log(a); a = 2; // 此處的a至關於window.a } foo(); console.log(a); //打印結果是2
上方代碼中,foo()的打印結果是1
。若是去掉第一行代碼,打印結果是Uncaught ReferenceError: a is not defined
提醒2:定義形參就至關於在函數做用域中聲明瞭變量。
function fun6(e) { console.log(e); } fun6(); //打印結果爲 undefined fun6(123);//打印結果爲123
區別1:
全局做用域以外,每一個函數都會建立本身的做用域,做用域在函數定義時就已經肯定了。而不是在函數調用時
全局執行上下文環境是在全局做用域肯定以後, js代碼立刻執行以前建立
函數執行上下文是在調用函數時, 函數體代碼執行以前建立
區別2:
做用域是靜態的, 只要函數定義好了就一直存在, 且不會再變化
執行上下文是動態的, 調用函數時建立, 函數調用結束時就會自動釋放
聯繫:
執行上下文(對象)是從屬於所在的做用域
全局上下文環境==>全局做用域
函數上下文環境==>對應的函數使用域
當在函數做用域操做一個變量時,它會先在自身做用域中尋找,若是有就直接使用(就近原則)。若是沒有則向上一級做用域中尋找,直到找到全局做用域;若是全局做用域中依然沒有找到,則會報錯ReferenceError。
外部函數定義的變量能夠被內部函數所使用,反之則不行。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script> //只要是函數就能夠創造做用域 //函數中又能夠再建立函數 //函數內部的做用域能夠訪問函數外部的做用域 //若是有多個函數嵌套,那麼就會構成一個鏈式訪問結構,這就是做用域鏈 //f1--->全局 function f1(){ //f2--->f1--->全局 function f2(){ //f3---->f2--->f1--->全局 function f3(){ } //f4--->f2--->f1---->全局 function f4(){ } } //f5--->f1---->全局 function f5(){ } } </script> </head> <body> </body> </html>
理解:
多個上下級關係的做用域造成的鏈, 它的方向是從下向上的(從內到外)
查找變量時就是沿着做用域鏈來查找的
查找一個變量的查找規則:
var a = 1 function fn1() { var b = 2 function fn2() { var c = 3 console.log(c) console.log(b) console.log(a) console.log(d) } fn2() } fn1()
在當前做用域下的執行上下文中查找對應的屬性, 若是有直接返回, 不然進入2
在上一級做用域的執行上下文中查找對應的屬性, 若是有直接返回, 不然進入3
再次執行2的相同操做, 直到全局做用域, 若是還找不到就拋出找不到的異常
閉包就是可以讀取其餘函數內部數據(變量/函數)的函數。
只有函數內部的子函數才能讀取局部變量,所以能夠把閉包簡單理解成"定義在一個函數內部的函數"。
上面這兩句話,是阮一峯的文章裏的,你不必定能理解,來看下面的講解和舉例。
當一個嵌套的內部(子)函數引用了嵌套的外部(父)函數的變量或函數時, 就產生了閉包。
使用chrome調試查看
理解一: 閉包是嵌套的內部函數(絕大部分人)
理解二: 包含被引用變量 or 函數的對象(極少數人)
注意: 閉包存在於嵌套的內部函數中。
1.函數嵌套
2.內部函數引用了外部函數的數據(變量/函數)。
來看看條件2:
function fn1() { function fn2() { } return fn2; } fn1();
上面的代碼不會產生閉包,由於內部函數fn2並無引用外部函數fn1的變量。
PS:還有一個條件是外部函數被調用,內部函數被聲明。好比:
function fn1() { var a = 2 var b = 'abc' function fn2() { //fn2內部函數被提早聲明,就會產生閉包(不用調用內部函數) console.log(a) } } fn1(); function fn3() { var a = 3 var fun4 = function () { //fun4採用的是「函數表達式」建立的函數,此時內部函數的聲明並無提早 console.log(a) } } fn3();
function fn1() { var a = 2 function fn2() { a++ console.log(a) } return fn2 } var f = fn1(); //執行外部函數fn1,返回的是內部函數fn2 f() // 3 //執行fn2 f() // 4 //再次執行fn2
當f()第二次執行的時候,a加1了,也就說明了:閉包裏的數據沒有消失,而是保存在了內存中。若是沒有閉包,代碼執行完倒數第三行後,變量a就消失了。
上面的代碼中,雖然調用了內部函數兩次,可是,閉包對象只建立了一個。
也就是說,要看閉包對象建立了一個,就看:外部函數執行了幾回(與內部函數執行幾回無關)。
function showDelay(msg, time) { setTimeout(function() { //這個function是閉包,由於是嵌套的子函數,並且引用了外部函數的變量msg alert(msg) }, time) } showDelay('atguigu', 2000)
上面的代碼中,閉包是裏面的funciton,由於它是嵌套的子函數,並且引用了外部函數的變量msg。
做用1. 使用函數內部的變量在函數執行完後, 仍然存活在內存中(延長了局部變量的生命週期)
做用2. 讓函數外部能夠操做(讀寫)到函數內部的數據(變量/函數)
咱們讓然拿這段代碼來分析:
function fn1() { var a = 2 function fn2() { a++ console.log(a) } return fn2 } var f = fn1(); //執行外部函數fn1,返回的是內部函數fn2 f() // 3 //執行fn2 f() // 4 //再次執行fn2
做用1分析:
上方代碼中,外部函數fn1執行完畢後,變量a並無當即消失,而是保存在內存當中。
做用2分析:
函數fn1中的變量a,是在fn1這個函數做用域內,所以外部沒法訪問。可是經過閉包,外部就能夠操做到變量a。
達到的效果是:外界看不到變量a,但能夠操做a。
好比上面達到的效果是:我看不到變量a,可是每次執行函數後,讓a加1。固然,若是我真想看到a,我能夠在fn2中將a返回便可。
回答幾個問題:
答案:通常是不存在, 存在於閉中的變量纔可能存在。
閉包可以一直存在的根本緣由是f
,由於f
接收了fn1()
,這個是閉包,閉包裏有a。注意,此時,fn2並不存在了,可是裏面的對象(即閉包)依然存在,由於用f
接收了。
不能,但咱們能夠經過閉包讓外部操做它。
產生: 嵌套內部函數fn2被聲明時就產生了(不是在調用)
死亡: 嵌套的內部函數成爲垃圾對象時。(好比f = null,就可讓f成爲垃圾對象。意思是,此時f再也不引用閉包這個對象了)
將全部的數據和功能都封裝在一個函數內部(私有的),只向外暴露一個包含n個方法的對象或函數。
模塊的使用者, 只須要經過模塊暴露的對象調用方法來實現對應的功能。
(1)myModule.js:(定義一個模塊,向外暴露多個函數,供外界調用)
function myModule() { //私有數據 var msg = 'Smyhvae Haha' //操做私有數據的函數 function doSomething() { console.log('doSomething() ' + msg.toUpperCase()); //字符串大寫 } function doOtherthing() { console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小寫 } //經過【對象字面量】的形式進行包裹,向外暴露多個函數 return { doSomething1: doSomething, doOtherthing2: doOtherthing } }
上方代碼中,外界能夠經過doSomething1和doOtherthing2來操做裏面的數據,但不讓外界看到。
(2)index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>05_閉包的應用_自定義JS模塊</title> </head> <body> <!-- 閉包的應用 : 定義JS模塊 * 具備特定功能的js文件 * 將全部的數據和功能都封裝在一個函數內部(私有的) * 【重要】只向外暴露一個包含n個方法的對象或函數 * 模塊的使用者, 只須要經過模塊暴露的對象調用方法來實現對應的功能 --> <script type="text/javascript" src="myModule.js"></script> <script type="text/javascript"> var module = myModule(); module.doSomething1(); module.doOtherthing2(); </script> </body> </html>
一樣是實現方式一種的功能,這裏咱們採起另一種方式。
(1)myModule2.js:(是一個當即執行的匿名函數)
(function () { //私有數據 var msg = 'Smyhvae Haha' //操做私有數據的函數 function doSomething() { console.log('doSomething() ' + msg.toUpperCase()) } function doOtherthing() { console.log('doOtherthing() ' + msg.toLowerCase()) } //外部函數是即便運行的匿名函數,咱們能夠把兩個方法直接傳給window對象 window.myModule = { doSomething1: doSomething, doOtherthing2: doOtherthing } })()
(2)index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>05_閉包的應用_自定義JS模塊2</title> </head> <body> <!-- 閉包的應用2 : 定義JS模塊 * 具備特定功能的js文件 * 將全部的數據和功能都封裝在一個函數內部(私有的) * 只向外暴露一個包信n個方法的對象或函數 * 模塊的使用者, 只須要經過模塊暴露的對象調用方法來實現對應的功能 --> <!--引入myModule文件--> <script type="text/javascript" src="myModule2.js"></script> <script type="text/javascript"> myModule.doSomething1() myModule.doOtherthing2() </script> </body> </html>
上方兩個文件中,咱們在myModule2.js
裏直接把兩個方法直接傳遞給window對象了。因而,在index.html中引入這個js文件後,會當即執行裏面的匿名函數。在index.html中把myModule直接拿來用便可。
總結:
固然,方式一和方式二對比後,咱們更建議採用方式二,由於很方便。
但不管如何,兩種方式都採用了閉包。
缺點:函數執行完後, 函數內的局部變量沒有釋放,佔用內存時間會變長,容易形成內存泄露。
解決:能不用閉包就不用,及時釋放。好比:
f = null; // 讓內部函數成爲垃圾對象 -->回收閉包
總而言之,你須要它,就是優勢;你不須要它,就成了缺點。
內存泄漏:佔用的內存沒有及時釋放。內存泄露積累多了就容易致使內存溢出。
常見的內存泄露:
1.意外的全局變量
2.沒有及時清理的計時器或回調函數
3.閉包
狀況1舉例:
// 意外的全局變量 function fn() { a = new Array(10000000); console.log(a); } fn();
狀況2舉例:
// 沒有及時清理的計時器或回調函數 var intervalId = setInterval(function () { //啓動循環定時器後不清理 console.log('----') }, 1000) // clearInterval(intervalId); //清理定時器
狀況3舉例:
<script type="text/javascript"> function fn1() { var arr = new Array[100000]; //這個數組佔用了很大的內存空間 function fn2() { console.log(arr.length) } return fn2 } var f = fn1() f() f = null //讓內部函數成爲垃圾對象-->回收閉包 </script>
內存溢出:當程序運行須要的內存超過了剩餘的內存時,就出拋出內存溢出的錯誤。
//內存溢出 var obj = {} for (var i = 0; i < 10000; i++) { obj[i] = new Array(10000000); //把全部的數組內容都放到obj裏保存,致使obj佔用了很大的內存空間 console.log('-----') }