作用域和闭包的通俗理解
執行上下文
執行上下文主要有兩種情況:
全局代碼: 一段<script>標簽里,有一個全局的執行上下文。所做的事情是:變量定義、函數聲明
函數代碼:每個函數里有一個上下文。所做的事情是:變量定義、函數聲明、this、arguments
PS:注意“函數聲明”和“函數表達式”的區別。
全局執行上下文
在執行全局代碼前將window確定為全局執行上下文。
(1)對全局數據進行預處理:(并沒有賦值)
var定義的全局變量==>undefined, 添加為window的屬性
function聲明的全局函數==>賦值(fun), 添加為window的方法
this==>賦值(window)
(2)開始執行全局代碼
函數執行上下文
在調用函數, 準備執行函數體之前, 創建對應的函數執行上下文對象(虛擬的, 存在于棧中)。
(1)對局部數據進行預處理:
形參變量==>賦值(實參)==>添加為執行上下文的屬性
arguments==>賦值(實參列表), 添加為執行上下文的屬性
var定義的局部變量==>undefined, 添加為執行上下文的屬性
function聲明的函數 ==>賦值(fun), 添加為執行上下文的方法
this==>賦值(調用函數的對象)
(2)開始執行函數體代碼
執行上下文棧
1.在全局代碼執行前, JS引擎就會創建一個棧來存儲管理所有的執行上下文對象
2.在全局執行上下文(window)確定后, 將其添加到棧中(壓棧)
3.在函數執行上下文創建后, 將其添加到棧中(壓棧)
4.在當前函數執行完后,將棧頂的對象移除(出棧)
5.當所有的代碼執行完后, 棧中只剩下window
this
this指的是,調用函數的那個對象。this永遠指向函數運行時所在的對象。
解析器在調用函數每次都會向函數內部傳遞進一個隱含的參數,這個隱含的參數就是this。
根據函數的調用方式的不同,this會指向不同的對象:【重要】
1.以函數的形式調用時,this永遠都是window。比如fun();相當于window.fun();
2.以方法的形式調用時,this是調用方法的那個對象
3.以構造函數的形式調用時,this是新創建的那個對象
4.使用call和apply調用時,this是指定的那個對象
需要特別提醒的是:this的指向在函數定義時無法確認,只有函數執行時才能確定。
this的幾種場景:
- 1、作為構造函數執行
例如:
function Foo(name) {//this = {};this.name = name;//return this;}var foo = new Foo();- 2、作為對象的屬性執行
- 3、作為普通函數執行
- 4、call apply bind
作用域
作用域指一個變量的作用范圍。它是靜態的(相對于上下文對象), 在編寫代碼時就確定了。
作用:隔離變量,不同作用域下同名變量不會有沖突。
作用域的分類:
全局作用域
函數作用域
沒有塊作用域(ES6有了)
上方代碼中,并不會報錯,因為:雖然 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(); //打印結果為 undefinedfun6(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 = 1function fn1() {var b = 2function fn2() {var c = 3console.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 = 2var b = 'abc'function fn2() { //fn2內部函數被提前聲明,就會產生閉包(不用調用內部函數)console.log(a)}}fn1();function fn3() {var a = 3var fun4 = function () { //fun4采用的是“函數表達式”創建的函數,此時內部函數的聲明并沒有提前console.log(a)}}fn3();常見的閉包
- 將一個函數作為另一個函數的返回值
- 將函數作為實參傳遞給另一個函數調用。
閉包1:將一個函數作為另一個函數的返回值
function fn1() {var a = 2function fn2() {a++console.log(a)}return fn2}var f = fn1(); //執行外部函數fn1,返回的是內部函數fn2f() // 3 //執行fn2f() // 4 //再次執行fn2當f()第二次執行的時候,a加1了,也就說明了:閉包里的數據沒有消失,而是保存在了內存中。如果沒有閉包,代碼執行完倒數第三行后,變量a就消失了。
上面的代碼中,雖然調用了內部函數兩次,但是,閉包對象只創建了一個。
也就是說,要看閉包對象創建了一個,就看:外部函數執行了幾次(與內部函數執行幾次無關)。
閉包2. 將函數作為實參傳遞給另一個函數調用
function showDelay(msg, time) {setTimeout(function() { //這個function是閉包,因為是嵌套的子函數,而且引用了外部函數的變量msgalert(msg)}, time)}showDelay('atguigu', 2000)上面的代碼中,閉包是里面的funciton,因為它是嵌套的子函數,而且引用了外部函數的變量msg。
閉包的作用
作用1. 使用函數內部的變量在函數執行完后, 仍然存活在內存中(延長了局部變量的生命周期)
作用2. 讓函數外部可以操作(讀寫)到函數內部的數據(變量/函數)
我們讓然拿這段代碼來分析:
function fn1() {var a = 2function fn2() {a++console.log(a)}return fn2}var f = fn1(); //執行外部函數fn1,返回的是內部函數fn2f() // 3 //執行fn2f() // 4 //再次執行fn2作用1分析:
上方代碼中,外部函數fn1執行完畢后,變量a并沒有立即消失,而是保存在內存當中。
作用2分析:
函數fn1中的變量a,是在fn1這個函數作用域內,因此外部無法訪問。但是通過閉包,外部就可以操作到變量a。
達到的效果是:外界看不到變量a,但可以操作a。
比如上面達到的效果是:我看不到變量a,但是每次執行函數后,讓a加1。當然,如果我真想看到a,我可以在fn2中將a返回即可。
回答幾個問題:
- 問題1. 函數執行完后, 函數內部聲明的局部變量是否還存在?
答案:一般是不存在, 存在于閉中的變量才可能存在。
閉包能夠一直存在的根本原因是f,因為f接收了fn1(),這個是閉包,閉包里有a。注意,此時,fn2并不存在了,但是里面的對象(即閉包)依然存在,因為用f接收了。
- 問題2. 在函數外部能直接訪問函數內部的局部變量嗎?
不能,但我們可以通過閉包讓外部操作它。
閉包的生命周期
產生: 嵌套內部函數fn2被聲明時就產生了(不是在調用)
死亡: 嵌套的內部函數成為垃圾對象時。(比如f = null,就可以讓f成為垃圾對象。意思是,此時f不再引用閉包這個對象了)
閉包的應用:定義具有特定功能的js模塊
將所有的數據和功能都封裝在一個函數內部(私有的),只向外暴露一個包含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('-----')}總結
以上是生活随笔為你收集整理的作用域和闭包的通俗理解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中国联通与阿里宣布相互开放云计算资源
- 下一篇: Jenkins+docker+maven