大部分接觸js應(yīng)該都是先用了再學(xué)渠旁,其實(shí)大部分學(xué)習(xí)大部分語(yǔ)言都應(yīng)該采取這種方式。因?yàn)橹豢床痪毚€能入門(mén)顾腊,而如果先看的話,估計(jì)很容易就學(xué)不下去了傻唾。所以呀投慈,要想欺騙別人承耿,首先得欺騙自己(假裝自己會(huì)了冠骄,然后開(kāi)始動(dòng)手,不會(huì)的再去學(xué)加袋,然后去騙騙別人凛辣,如果覺(jué)得騙不過(guò),再老老實(shí)實(shí)看看視頻和書(shū)籍职烧,做做筆記扁誓,周而復(fù)始,然后就真的可以騙到別人了)蚀之。^0^
首先蝗敢,讀本書(shū)讓我了解到j(luò)s的最重要的兩個(gè)知識(shí)點(diǎn)——閉包還有this指向,其次一點(diǎn)的就是編譯原理和對(duì)象原形足删。
這里記錄一下閉包的相關(guān)知識(shí)寿谴。了解閉包前還需要先理解js編譯原理、變量查詢以及作用域失受。
1.基礎(chǔ)知識(shí)
1.1 編譯原理
盡管通常將 JavaScript 歸類(lèi)為“動(dòng)態(tài)”或“解釋執(zhí)行”語(yǔ)言讶泰,但事實(shí)上它是一門(mén)編譯語(yǔ)言咏瑟。它不是提前編譯的,比起傳統(tǒng)編譯語(yǔ)言的編譯器痪署,JavaScript 引擎要復(fù)雜得多码泞。
對(duì)于 JavaScript 來(lái)說(shuō),大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒的時(shí)間內(nèi)狼犯。任何 JavaScript 代碼片段在執(zhí)行前都要進(jìn)行編譯余寥,然后再執(zhí)行。
關(guān)于var a = 2;
的編譯過(guò)程:
遇到 var a悯森,檢查變量名稱是否存在于同一作用域劈狐,存在則忽略,否則聲明新的變量a呐馆;
生成運(yùn)行時(shí)所需的代碼肥缔,用來(lái)處理
a = 2
賦值操作;
執(zhí)行代碼時(shí)汹来,引擎會(huì)去查找變量a, 如果查找到续膳,就會(huì)進(jìn)行賦值,否則就會(huì)拋出異常收班。
1.2 關(guān)于變量的查找
變量查詢分為LHS查詢
和RHS查詢
坟岔,上面賦值操作將進(jìn)行LHS查詢
。
當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時(shí)進(jìn)行 LHS 查詢摔桦,出現(xiàn)在右側(cè)時(shí)進(jìn)行 RHS 查詢社付。
賦值操作的目標(biāo)是誰(shuí)
LHS
以及誰(shuí)是賦值操作的源頭RHS
。
LHS查詢
是試圖找到變量的容器本身邻耕,從而可以對(duì)其賦值鸥咖。RHS查詢
相當(dāng)于查找某個(gè)變量的值,RHS查詢
并不是真正意義上的“賦值操作的右側(cè)”兄世,更準(zhǔn)確地說(shuō)是“非左側(cè)”啼辣。
如console.log( a );
,其中對(duì) a 的引用是一個(gè) RHS 引用御滩,而a = 2;
對(duì) a 的引用則是 LHS 引用鸥拧,因?yàn)閷?shí)際上我們并不關(guān)心當(dāng)前的值是什么。
拓展
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
這里LHS查詢
有3處削解,RHS查詢
有4處富弦,foo方法調(diào)用也需要一次RHS查詢
, 參數(shù)傳遞需要將2
賦值給方法形式參數(shù)a
。
1.3 關(guān)于作用域
作用域
是根據(jù)名稱查找變量的一套規(guī)則氛驮。通常需要同時(shí)顧及幾個(gè)作用域
腕柜。
當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí),就發(fā)生了作用域的嵌套
。在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí)媳握,引擎就會(huì)在外層嵌套的(上一級(jí))作用域
中繼續(xù)查找碱屁,直到找到該變量, 或抵達(dá)最外層的作用域
(也就是全局作用域
)為止蛾找。
如果RHS查詢
未找到所需的變量娩脾,引擎就會(huì)拋出ReferenceError
異常。
當(dāng)引擎執(zhí)行LHS查詢
時(shí)打毛,如果在全局作用域中也無(wú)法找到目標(biāo)變量柿赊,全局作用域中就會(huì)創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎幻枉,前提是在非 “嚴(yán)格模式”下碰声。
如果RHS查詢
成功,但對(duì)變量進(jìn)行不合理的操作時(shí)熬甫,就會(huì)拋出TypeError
異常胰挑。
遮蔽效應(yīng)
作用域查找會(huì)在找到第一個(gè)匹配的標(biāo)識(shí)符時(shí)停止。
全局變量會(huì)自動(dòng)成為全局對(duì)象(比如瀏覽器中的 window 對(duì)象)的屬性椿肩,可以通過(guò)全局對(duì)象訪問(wèn)該變量:window.a
瞻颂;但無(wú)論如何無(wú)法訪問(wèn)到被遮蔽非全局的變量。
欺騙詞法
function foo(str, a) {
eval( str ); // 欺騙! console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
使用eval
郑象,在foo方法聲明變量b
并賦值贡这,將遮蔽全局變量b
。
在嚴(yán)格模式的程序中厂榛,eval(..) 在運(yùn)行時(shí)有其自己的詞法作用域盖矫,意味著其 中的聲明無(wú)法修改所在的作用域。
with用法
var obj = { a: 1, b: 2 };
foo(obj){
with (obj) {
a = 3;
b = 4;
c = 5;
}
}
foo(obj)
console.log(obj.a) // 3
console.log(obj.c) // undefine
console.log(c) // 5
1.4 函數(shù)的作用域
函數(shù)作用域的是指击奶,屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)(包括嵌套的作用域中)使用及復(fù)用辈双。
最小授權(quán)或最小暴露原則:在軟件設(shè)計(jì)中,應(yīng)該最小限度地暴露必 要內(nèi)容正歼,而將其他內(nèi)容都“隱藏”起來(lái)辐马,比如某個(gè)模塊或?qū)ο蟮腁PI 設(shè)計(jì)拷橘。
作用域的好處:
規(guī)避沖突
全局命名空間易與第三方庫(kù)發(fā)生變量沖突局义。
利用作用域的規(guī)則強(qiáng)制所有標(biāo)識(shí)符都不能注入到共享作用域中,而是保持在私有冗疮、無(wú)沖突的作用域中萄唇,這樣可以有效規(guī)避掉所有的意外沖突。
var a = 2;
(function foo(){ // <-- 添加這一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及這一行
console.log( a ); // 2
函數(shù)會(huì)被當(dāng)作函數(shù)表達(dá)式而不是一 個(gè)標(biāo)準(zhǔn)的函數(shù)聲明來(lái)處理术幔。
區(qū)分函數(shù)聲明和表達(dá)式最簡(jiǎn)單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明中的位 置(不僅僅是一行代碼另萤,而是整個(gè)聲明中的位置)。如果 function 是聲明中 的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明四敞,否則就是一個(gè)函數(shù)表達(dá)式泛源。
函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別是它們的名稱標(biāo)識(shí)符將會(huì)綁定在何處。
匿名函數(shù)表達(dá)式
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );
函數(shù)表達(dá)式可以沒(méi)有名稱標(biāo)識(shí)符忿危,而函數(shù)聲明則不可以省略函數(shù)名达箍。
匿名函數(shù)表達(dá)式有一下幾個(gè)缺點(diǎn):
匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難铺厨。
當(dāng)函數(shù)需要引用自身時(shí)只能使用已經(jīng)過(guò)期的arguments.callee引用缎玫, 比如在遞歸中。以及在事件觸發(fā)后事件監(jiān)聽(tīng)器需要解綁自身解滓。
影響代碼可讀性赃磨。
推薦具名寫(xiě)法:
setTimeout( function timeoutHandler() {
console.log( "I waited 1 second!" );
}, 1000 );
立即執(zhí)行函數(shù)表達(dá)式(IIFE)
var a = 2;
(function foo(a) {
a += 3;
console.log( a ); // 5
})(a);
console.log( a ); // 2
優(yōu)點(diǎn):
- 將外部對(duì)象作為參數(shù),并將變量命名為任何你覺(jué)得合適的名字洼裤。有助于改進(jìn)代碼風(fēng)格邻辉。
- 解決 undefined 標(biāo)識(shí)符的默認(rèn)值被錯(cuò)誤覆蓋導(dǎo)致的異常。
undefined = true; // 給其他代碼挖了一個(gè)大坑!絕對(duì)不要這樣做!
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
- 倒置代碼的運(yùn)行順序腮鞍,將需要運(yùn)行的函數(shù)放在第二位恩沛。
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3 console.log( global.a ); // 2
});
1.5 塊作用域
表面上看 JavaScript 并沒(méi)有塊作用域的相關(guān)功能。
for (var i=0; i<10; i++) {
console.log( i );
}
這里i
會(huì)被綁定在外部作用域(函數(shù)或全局)中缕减。
塊作用域的用處: 變量的聲明應(yīng)該距離使用的地方越近越好雷客,并最大限度地本地化。
塊作用域是一個(gè)用來(lái)對(duì)之前的最小授權(quán)原則進(jìn)行擴(kuò)展的工具桥狡,將代碼從在函數(shù)中隱藏信息 擴(kuò)展為在塊中隱藏信息
當(dāng)使用 var
聲明變量時(shí)搅裙,它寫(xiě)在哪里都是一樣的,因?yàn)樗鼈冏罱K都會(huì)屬于外部作用域裹芝。
塊作用域的例子:
with
關(guān)鍵字的結(jié)構(gòu)就是塊作用域部逮。try/catch
的catch
分句會(huì)創(chuàng)建一個(gè)塊作用域,其中聲明的變量?jī)H在catch
內(nèi)部有效嫂易。let
關(guān)鍵字可以將變量綁定到所在的任意作用域中兄朋。其聲明的變量隱式地了所在的塊作用域。const
關(guān)鍵字同樣可以用來(lái)創(chuàng)建塊作用域變量怜械,但其值是固定的(常量)颅和。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的塊作用域常量
a = 3; // 正常!
b = 4; // 錯(cuò)誤!
}
console.log( a ); // 3
console.log( b ); // ReferenceError
let
關(guān)鍵字的作用:
-
let
進(jìn)行的聲明不會(huì)在塊作用域中進(jìn)行提升。聲明的代碼被運(yùn)行之前缕允,聲明并不“存在”峡扩。 - 和閉包及回收內(nèi)存垃圾的回收機(jī)制相關(guān)。
function process(data) {
// 在這里做點(diǎn)有趣的事情
}
// 在這個(gè)塊中定義的內(nèi)容可以銷(xiāo)毀了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
-
for循環(huán)
頭部的 let 不僅將i
綁定到了for
循環(huán)的塊中障本,事實(shí)上它將其重新綁定到了循環(huán)的每一個(gè)迭代中教届,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值响鹃。
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
1.6 提升
例1.6.1:
a = 2;
var a;
console.log( a ); // 2
例1.6.2:
console.log( a ); // undefine
var a = 2;
當(dāng)你看到
var a = 2;
時(shí),可能會(huì)認(rèn)為這是一個(gè)聲明案训。但實(shí)際上會(huì)將其看成兩個(gè)聲明:var a;
和a = 2;
买置。第一個(gè)定義聲明是在編譯階段進(jìn)行的。第二個(gè)賦值聲明會(huì)被留在原地等待執(zhí)行階段强霎。
這個(gè)過(guò)程就好像變量和函數(shù)聲明從它們?cè)诖a中出現(xiàn)的位置被“移動(dòng)” 到了最上面堕义。這個(gè)過(guò)程就叫作提升。
函數(shù)聲明和變量聲明都會(huì)被提升脆栋。但是一個(gè)值得注意的細(xì)節(jié)是函數(shù)會(huì)首先被提升倦卖,然后才是變量。
例1.6.3:
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
例1.6.4:
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}
例1.6.5:
foo(); // "b"
var a = true; if (a) {
function foo() {
console.log("a"); }
}
else {
function foo() {
console.log("b");
}
}
盡管重復(fù)的 var 聲明會(huì)被忽略掉椿争,但出現(xiàn)在后面的函數(shù)聲明還是可以覆蓋前面的怕膛。
2.閉包
JavaScript中閉包無(wú)處不在,你只需要能夠識(shí)別并擁抱它秦踪。
閉包是基于詞法作用域書(shū)寫(xiě)代碼時(shí)所產(chǎn)生的自然結(jié)果褐捻,你甚至不需要為了利用它們而有意 識(shí)地創(chuàng)建閉包。
當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域時(shí)椅邓,就產(chǎn)生了閉包柠逞,即使函數(shù)是在當(dāng)前詞法作用 域之外執(zhí)行。
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
bar()
對(duì) a
的引用的方法是詞法作用域的查找規(guī)則景馁,而這些規(guī)則只是閉包的一部分板壮。但根據(jù)前面的定義,這并不是閉包合住。
下面一段代碼绰精,清晰地展示了閉包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 這就是閉包的效果。
函數(shù) bar()
的詞法作用域能夠訪問(wèn)foo()
的內(nèi)部作用域透葛。然后我們將bar()
函數(shù)本身當(dāng)作 一個(gè)值類(lèi)型進(jìn)行傳遞笨使。
理解閉包
在foo()
執(zhí)行后,通常會(huì)期待foo()
的整個(gè)內(nèi)部作用域都被銷(xiāo)毀僚害。事實(shí)上內(nèi)部作用域依然存在(由于bar()
本身在使用)硫椰,因此沒(méi)有被回收。
拜bar()
所聲明的位置所賜萨蚕,它擁有涵蓋foo()
內(nèi)部作用域的閉包靶草,使得該作用域能夠一 直存活,以供bar()
在之后任何時(shí)間進(jìn)行引用门岔。
bar()
依然持有對(duì)該作用域的引用爱致,而這個(gè)引用就叫作閉包。
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
timer
具有涵蓋wait(..)
作用域的閉包寒随,因此還保有對(duì)變量message
的引用。
wait(..)
執(zhí)行 1000 毫秒后,它的內(nèi)部作用域并不會(huì)消失妻往,timer
函數(shù)依然保有wait(..)
作用域的閉包互艾。
循環(huán)和閉包
例2.1:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i ); // 6 6 6 6 6
}, i*1000 );
}
例2.2:
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j ); // 1 2 3 4 5 6
}, j*1000 );
})();
}
例2.3:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j ); // 1 2 3 4 5
}, j*1000 );
})( i );
}
例2.1: 根據(jù)作用域的工作原理,盡管循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的讯泣, 但是它們都被封閉在一個(gè)共享的全局作用域中纫普,因此實(shí)際上只有一個(gè)i
。由于函數(shù)延遲執(zhí)行好渠,最終循環(huán)執(zhí)行完才調(diào)用昨稼,得到i
的值為6。
例2.2:匿名函數(shù)有自己的作用域拳锚,變量j
用來(lái)在每個(gè)迭代中儲(chǔ)存i
的值假栓。
例2.3:對(duì)例2.2代碼的改進(jìn)。
在迭代內(nèi)使用IIFE
會(huì)為每個(gè)迭代都生成一個(gè)新的作用域霍掺,使得延遲函數(shù)的回調(diào)可以將新的作用域封閉在每個(gè)迭代內(nèi)部匾荆,每個(gè)迭代中都會(huì)含有一個(gè)具有正確值的變量供我們?cè)L問(wèn)。
2.1 模塊
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
這個(gè)模式在 JavaScript 中被稱為模塊杆烁。我們保持內(nèi)部數(shù)據(jù)變量是隱 藏且私有的狀態(tài)牙丽。可以將這個(gè)對(duì)象類(lèi)型的返回值看作本質(zhì)上是模塊的公共 API兔魂。
模塊模式的兩個(gè)必要條件:
必須有外部的封閉函數(shù)烤芦,該函數(shù)必須至少被調(diào)用一次。
封閉函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù)析校,這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包拍棕,并且可以訪問(wèn)或者修改私有的狀態(tài)。
一個(gè)從函數(shù)調(diào)用所返回的勺良,只有數(shù)據(jù)屬性而沒(méi)有閉包函數(shù)的對(duì)象并不是真正的模塊绰播。
單例模式
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
加載器 / 管理器
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
這段代碼的核心是modules[name] = impl.apply(impl, deps)
。為了模塊的定義引入了包裝函數(shù)(可以傳入任何依賴)尚困,并且將返回值蠢箩,也就是模塊的API,儲(chǔ)存在一個(gè)根據(jù)名字來(lái)管理的模塊列表中事甜。
使用模塊:
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
});
var bar = MyModules.get( "bar" );
console.log(
bar.hello( "hippo" )
); // Let me introduce: hippo
總結(jié)
學(xué)而時(shí)習(xí)之谬泌,不亦說(shuō)乎÷咔看了一遍書(shū)籍掌实,然后通過(guò)記筆記,又回顧了一遍邦马,一些不懂的知識(shí)這次就弄懂了贱鼻,同時(shí)還發(fā)現(xiàn)了一些漏過(guò)的知識(shí)宴卖。
很久以前,隔壁班的某某每套卷子都要重復(fù)做上6遍邻悬,然后每次成績(jī)都排列前茅症昏。而他的母親卻是我的班主任,雖然沒(méi)有從老師那里學(xué)到這種學(xué)習(xí)方式父丰,但是老師卻我覺(jué)得學(xué)習(xí)也是一件快樂(lè)的事情肝谭,我很慶幸遇到這樣一位老師。