JS JavaScript閉包和作用域
閉包
JavaScript高級程序設(shè)計(jì)中對閉包的定義:閉包是指有權(quán)訪問另外一個(gè)函數(shù)作用域中變量的函數(shù)。
從概念上,閉包有兩個(gè)特點(diǎn):
- 函數(shù)
- 能訪問另外一個(gè)函數(shù)的作用域中的變量
在ES6
之前,JavaScript
只有函數(shù)作用域的概念,沒有塊級作用域(但catch
捕獲的異常,只能在catch
中訪問)的概念。每個(gè)函數(shù)都是封閉的,外部訪問不到函數(shù)作用域中的變量。
function getName() {
var name = "LHH";
console.log(name); //"LHH"
}
function displayName() {
console.log(name); //報(bào)錯(cuò)
}
把代碼改成以下:
function getName() {
var name = "LHH";
function displayName() {
console.log(name);
}
return displayName;
}
var getLHH = getName();
getLHH() //"LHH"
函數(shù)是一個(gè)閉包,外部就可以訪問函數(shù)中的變量
對于閉包有下面三個(gè)特性:
1.閉包可以訪問當(dāng)前函數(shù)以外的變量
function getOuter(){
var date = '815';
function getDate(str){
console.log(str + date); //訪問外部的date
}
return getDate('今天是:'); //"今天是:815"
}
getOuter();
getData
是一個(gè)閉包,該函數(shù)執(zhí)行時(shí),會(huì)形成一個(gè)作用域A
,A
中并沒有定義變量data
,但它能從父一級作用域中找到該變量的定義。
2.即使外部函數(shù)已經(jīng)返回,閉包仍能訪問外部函數(shù)定義的變量
function getOuter(){
var date = '815';
function getDate(str){
console.log(str + date); //訪問外部的date
}
return getDate; //外部函數(shù)返回
}
var today = getOuter();
today('今天是:'); //"今天是:815"
today('明天不是:'); //"明天不是:815"
3.閉包可以更新外部變量的值
function updateCount(){
var count = 0;
function getCount(val){
count = val;
console.log(count);
}
return getCount; //外部函數(shù)返回
}
var count = updateCount();
count(815); //815
count(816); //816
作用域鏈
JavaScript
中有一個(gè)執(zhí)行環(huán)境(execution context
)的概念,它定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了他們各自的行為。每個(gè)執(zhí)行環(huán)境都有一個(gè)與之關(guān)聯(lián)的變量對象,環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對象中。可以修改它的屬性,但不能引用它。
變量對象也是有父作用域的。當(dāng)訪問一個(gè)變量時(shí),解釋器會(huì)首先在當(dāng)前作用域查找標(biāo)識符,如果沒有找到,就去父作用域找,直到找到該變量的標(biāo)識符或者不再存在父作用域鏈了,這就是作用域鏈。
作用域鏈和原型繼承有點(diǎn)類似:如果去查找一個(gè)普通對象的屬性時(shí),在當(dāng)前對象和其原型中都找不到時(shí),會(huì)返回undefined
,但查找的屬性在作用域中不存在的話就會(huì)拋出ReferenceError
。
作用域頂端是全局對象。對于全局環(huán)境中的代碼,作用域中只包含一個(gè)元素:全局對象。所以,在全局環(huán)境中定義變量的時(shí)候,它們會(huì)被定義到全局對象中。當(dāng)函數(shù)被調(diào)用的時(shí)候,作用域鏈就會(huì)包含多個(gè)作用域?qū)ο蟆?/p>
1.全局環(huán)境
看一個(gè)例子:
// my_script.js
"use strict";
var foo = 1;
var bar = 2;
在全局環(huán)境中,創(chuàng)建了兩個(gè)簡單地變量。此時(shí)變量對象是全局對象:
執(zhí)行上述代碼,my_script.js
本身會(huì)形成一個(gè)執(zhí)行環(huán)境,以及它所引用的變量對象。
2.無嵌套函數(shù)(Non-nested functions)
"use strict";
var foo = 1;
var bar = 2;
function myFunc() {
//-- define local-to-function variables
var a = 1;
var b = 2;
var foo = 3;
console.log("inside myFunc");
}
console.log("outside");
//-- and then, call it:
myFunc();
當(dāng)myFunc
被定義的時(shí)候,myFunc
的標(biāo)識符(identifier
)就被加到了當(dāng)前的作用域?qū)ο笾校ㄔ谶@里就是全局對象),并且這個(gè)標(biāo)識符所引用的是一個(gè)函數(shù)對象(function object
)。函數(shù)對象中所包含的是函數(shù)的源代碼以及其他的屬性。內(nèi)部屬性[[scope]]
指向的就是當(dāng)前的作用域?qū)ο蟆R簿褪侵傅木褪呛瘮?shù)的標(biāo)識符被創(chuàng)建的時(shí)候,我們所能夠直接訪問的那個(gè)作用域?qū)ο螅ㄔ谶@里就是全局對象)。
myFune
所引用的函數(shù)對象,其本身不僅僅含有函數(shù)的代碼,并且還含有指向其創(chuàng)建的時(shí)候的作用域?qū)ο蟆?/p>
當(dāng)myFunc
函數(shù)被調(diào)用的時(shí)候,一個(gè)新的作用域?qū)ο蟮谋粍?chuàng)建了。新的作用域?qū)ο笾邪?code>myFunc函數(shù)所定義的的本地變量,以及其參數(shù)(arguments
)。這個(gè)新的作用域?qū)ο蟮母缸饔糜驅(qū)ο缶褪窃谶\(yùn)行myFunc
時(shí)我們所能直接訪問的那個(gè)作用域?qū)ο螅慈謱ο螅?/p>
所以,當(dāng)myFunc
被執(zhí)行的時(shí)候,對象之間的關(guān)系如下圖:
3.有嵌套的函數(shù)(Nested functions)
當(dāng)函數(shù)返回沒有被引用的時(shí)候,就會(huì)被垃圾回收器回收。但是對于閉包(函數(shù)嵌套是形成閉包的一種形式),即使外部函數(shù)返回了,函數(shù)對象仍會(huì)引用它被創(chuàng)建時(shí)的作用域?qū)ο蟆?/p>
"use strict";
function createCounter(initial) {
var counter = initial;
function increment(value) {
counter += value;
}
function get() {
return counter;
}
return {
increment: increment,
get: get
};
}
var myCounter = createCounter(100);
console.log(myCounter.get()); // 返回 100
myCounter.increment(5);
console.log(myCounter.get()); // 返回 105
當(dāng)調(diào)用createCounter(100)
時(shí),對象之間的關(guān)系如下圖所示:
內(nèi)嵌函數(shù)increment
和get
都有指向createCounter(100) scope
的應(yīng)用。如果createCounter(100)
沒有任何返回值,那么createCounter(100) scope
不再被引用,于是就可以被垃圾回收。但是因?yàn)?code>createCounter(100)實(shí)際上是有返回值的,并且返回值被存儲(chǔ)在了myCounter
中,所以對象之間的引用關(guān)系變成了如下圖所示:
即使createCounter(100)
已經(jīng)返回,但是其作用域仍在,并能且只能被內(nèi)聯(lián)函數(shù)訪問。可以通過調(diào)用myCounter.increment()
或 myCounter.get()
來直接訪問createCounter(100)
的作用域。
當(dāng)myCounter.increment()
或 myCounter.get()
被調(diào)用時(shí),新的作用域?qū)ο髸?huì)被創(chuàng)建,并且該作用域?qū)ο蟮母缸饔糜驅(qū)ο髸?huì)是當(dāng)前可以直接訪問的作用域?qū)ο蟆4藭r(shí),引用關(guān)系如下:
當(dāng)執(zhí)行到return counter;
時(shí),在get()
所在的作用域并沒有找到對應(yīng)的標(biāo)示符,就會(huì)沿著作用域鏈往上找,直到找到變量counter
,然后返回該變量。
當(dāng)單獨(dú)調(diào)用increment(5)
時(shí),參數(shù)value
會(huì)存貯在當(dāng)前的作用域?qū)ο蟆:瘮?shù)要訪問value
,能馬上在當(dāng)前作用域找到該變量。但是當(dāng)函數(shù)要訪問counter
時(shí),并沒有找到,于是沿著作用域鏈向上查找,在createCounter(100)
的作用域找到了對應(yīng)的標(biāo)示符,increment()
就會(huì)修改counter
的值。除此之外,沒有其他方式來修改這個(gè)變量。閉包的強(qiáng)大也在于此,能夠存貯私有數(shù)據(jù)。
相同的函數(shù),不同的作用域
//myScript.js
"use strict";
function createCounter(initial) {
/* ... see the code from previous example ... */
}
//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);
myCounter1
和myCounter2
創(chuàng)建之后,關(guān)系圖如下:
在上面的例子中,myCounter1.increment
和myCounter2.increment
的函數(shù)對象擁有著一樣的代碼以及一樣的屬性值(name
,length
等等),但是它們的[[scope]]
指向的是不一樣的作用域?qū)ο蟆?/p>
這才有了下面的結(jié)果:
var a, b;
a = myCounter1.get(); // a 等于 100
b = myCounter2.get(); // b 等于 200
myCounter1.increment(1);
myCounter1.increment(2);
myCounter2.increment(5);
a = myCounter1.get(); // a 等于 103
b = myCounter2.get(); // b 等于 205