作用域和閉包的通俗理解
執行上下文
執行上下文主要有兩種情況:
全域性程式碼: 一段
<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、作為物件的屬性執行
var obj = {
name: 'A',
printName: function () {
console.log(this.name);
}
}
obj.printName();
- 3、作為普通函式執行
function fn() {
console.log(this); //this === window
}
fn();
- 4、call apply bind
作用域
作用域指一個變數的作用範圍。它是靜態的(相對於上下文物件), 在編寫程式碼時就確定了。
作用:隔離變數,不同作用域下同名變數不會有衝突。
作用域的分類:
全域性作用域
函式作用域
沒有塊作用域(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();
常見的閉包
- 將一個函式作為另一個函式的返回值
- 將函式作為實參傳遞給另一個函式呼叫。
閉包1:將一個函式作為另一個函式的返回值
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就消失了。
上面的程式碼中,雖然呼叫了內部函式兩次,但是,閉包物件只建立了一個。
也就是說,要看閉包物件建立了一個,就看:外部函式執行了幾次(與內部函式執行幾次無關)。
閉包2. 將函式作為實參傳遞給另一個函式呼叫
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返回即可。
回答幾個問題:
- 問題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('-----')
}