大部分接觸js應該都是先用了再學,其實大部分學習大部分語言都應該採取這種方式。因爲只看不練,還能入門,而如果先看的話,估計很容易就學不下去了。所以呀,要想欺騙別人,首先得欺騙自己(假裝自己會了,然後開始動手,不會的再去學,然後去騙騙別人,如果覺得騙不過,再老老實實看看視頻和書籍,做做筆記,周而復始,然後就真的可以騙到別人了)。^0^
首先,讀本書讓我瞭解到js的最重要的兩個知識點——閉包還有this指向,其次一點的就是編譯原理和對象原形。
這裏記錄一下閉包的相關知識。瞭解閉包前還需要先理解js編譯原理、變量查詢以及作用域。
1.基礎知識
1.1 編譯原理
儘管通常將 JavaScript 歸類爲“動態”或“解釋執行”語言,但事實上它是一門編譯語言。它不是提前編譯的,比起傳統編譯語言的編譯器,JavaScript 引擎要複雜得多。
對於 JavaScript 來說,大部分情況下編譯發生在代碼執行前的幾微秒的時間內。任何 JavaScript 代碼片段在執行前都要進行編譯,然後再執行。
關於var a = 2;
的編譯過程:
遇到 var a,檢查變量名稱是否存在於同一作用域,存在則忽略,否則聲明新的變量a;
生成運行時所需的代碼,用來處理
a = 2
賦值操作;
執行代碼時,引擎會去查找變量a, 如果查找到,就會進行賦值,否則就會拋出異常。
1.2 關於變量的查找
變量查詢分爲LHS查詢
和RHS查詢
,上面賦值操作將進行LHS查詢
。
當變量出現在賦值操作的左側時進行 LHS 查詢,出現在右側時進行 RHS 查詢。
賦值操作的目標是誰
LHS
以及誰是賦值操作的源頭RHS
。
LHS查詢
是試圖找到變量的容器本身,從而可以對其賦值。RHS查詢
相當於查找某個變量的值,RHS查詢
並不是真正意義上的“賦值操作的右側”,更準確地說是“非左側”。
如console.log( a );
,其中對 a 的引用是一個 RHS 引用,而a = 2;
對 a 的引用則是 LHS 引用,因爲實際上我們並不關心當前的值是什麼。
拓展
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
這裏LHS查詢
有3處,RHS查詢
有4處,foo方法調用也需要一次RHS查詢
, 參數傳遞需要將2
賦值給方法形式參數a
。
1.3 關於作用域
作用域
是根據名稱查找變量的一套規則。通常需要同時顧及幾個作用域
。
當一個塊或函數嵌套在另一個塊或函數中時,就發生了作用域的嵌套
。在當前作用域中無法找到某個變量時,引擎就會在外層嵌套的(上一級)作用域
中繼續查找,直到找到該變量, 或抵達最外層的作用域
(也就是全局作用域
)爲止。
如果RHS查詢
未找到所需的變量,引擎就會拋出ReferenceError
異常。
當引擎執行LHS查詢
時,如果在全局作用域中也無法找到目標變量,全局作用域中就會創建一個具有該名稱的變量,並將其返還給引擎,前提是在非 “嚴格模式”下。
如果RHS查詢
成功,但對變量進行不合理的操作時,就會拋出TypeError
異常。
遮蔽效應
作用域查找會在找到第一個匹配的標識符時停止。
全局變量會自動成爲全局對象(比如瀏覽器中的 window 對象)的屬性,可以通過全局對象訪問該變量:window.a
;但無論如何無法訪問到被遮蔽非全局的變量。
欺騙詞法
function foo(str, a) {
eval( str ); // 欺騙! console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
使用eval
,在foo方法聲明變量b
並賦值,將遮蔽全局變量b
。
在嚴格模式的程序中,eval(..) 在運行時有其自己的詞法作用域,意味着其 中的聲明無法修改所在的作用域。
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 函數的作用域
函數作用域的是指,屬於這個函數的全部變量都可以在整個函數的範圍內(包括嵌套的作用域中)使用及複用。
最小授權或最小暴露原則:在軟件設計中,應該最小限度地暴露必 要內容,而將其他內容都“隱藏”起來,比如某個模塊或對象的API 設計。
作用域的好處:
規避衝突
全局命名空間易與第三方庫發生變量衝突。
利用作用域的規則強制所有標識符都不能注入到共享作用域中,而是保持在私有、無衝突的作用域中,這樣可以有效規避掉所有的意外衝突。
var a = 2;
(function foo(){ // <-- 添加這一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及這一行
console.log( a ); // 2
函數會被當作函數表達式而不是一 個標準的函數聲明來處理。
區分函數聲明和表達式最簡單的方法是看 function 關鍵字出現在聲明中的位 置(不僅僅是一行代碼,而是整個聲明中的位置)。如果 function 是聲明中 的第一個詞,那麼就是一個函數聲明,否則就是一個函數表達式。
函數聲明和函數表達式之間最重要的區別是它們的名稱標識符將會綁定在何處。
匿名函數表達式
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );
函數表達式可以沒有名稱標識符,而函數聲明則不可以省略函數名。
匿名函數表達式有一下幾個缺點:
匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
當函數需要引用自身時只能使用已經過期的arguments.callee引用, 比如在遞歸中。以及在事件觸發後事件監聽器需要解綁自身。
影響代碼可讀性。
推薦具名寫法:
setTimeout( function timeoutHandler() {
console.log( "I waited 1 second!" );
}, 1000 );
立即執行函數表達式(IIFE)
var a = 2;
(function foo(a) {
a += 3;
console.log( a ); // 5
})(a);
console.log( a ); // 2
優點:
- 將外部對象作爲參數,並將變量命名爲任何你覺得合適的名字。有助於改進代碼風格。
- 解決 undefined 標識符的默認值被錯誤覆蓋導致的異常。
undefined = true; // 給其他代碼挖了一個大坑!絕對不要這樣做!
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
- 倒置代碼的運行順序,將需要運行的函數放在第二位。
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 並沒有塊作用域的相關功能。
for (var i=0; i<10; i++) {
console.log( i );
}
這裏i
會被綁定在外部作用域(函數或全局)中。
塊作用域的用處: 變量的聲明應該距離使用的地方越近越好,並最大限度地本地化。
塊作用域是一個用來對之前的最小授權原則進行擴展的工具,將代碼從在函數中隱藏信息 擴展爲在塊中隱藏信息
當使用 var
聲明變量時,它寫在哪裏都是一樣的,因爲它們最終都會屬於外部作用域。
塊作用域的例子:
with
關鍵字的結構就是塊作用域。try/catch
的catch
分句會創建一個塊作用域,其中聲明的變量僅在catch
內部有效。let
關鍵字可以將變量綁定到所在的任意作用域中。其聲明的變量隱式地了所在的塊作用域。const
關鍵字同樣可以用來創建塊作用域變量,但其值是固定的(常量)。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的塊作用域常量
a = 3; // 正常!
b = 4; // 錯誤!
}
console.log( a ); // 3
console.log( b ); // ReferenceError
let
關鍵字的作用:
-
let
進行的聲明不會在塊作用域中進行提升。聲明的代碼被運行之前,聲明並不“存在”。 - 和閉包及回收內存垃圾的回收機制相關。
function process(data) {
// 在這裏做點有趣的事情
}
// 在這個塊中定義的內容可以銷燬了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
-
for循環
頭部的 let 不僅將i
綁定到了for
循環的塊中,事實上它將其重新綁定到了循環的每一個迭代中,確保使用上一個循環迭代結束時的值重新進行賦值。
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;
當你看到
var a = 2;
時,可能會認爲這是一個聲明。但實際上會將其看成兩個聲明:var a;
和a = 2;
。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執行階段。
這個過程就好像變量和函數聲明從它們在代碼中出現的位置被“移動” 到了最上面。這個過程就叫作提升。
函數聲明和變量聲明都會被提升。但是一個值得注意的細節是函數會首先被提升,然後纔是變量。
例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");
}
}
儘管重複的 var 聲明會被忽略掉,但出現在後面的函數聲明還是可以覆蓋前面的。
2.閉包
JavaScript中閉包無處不在,你只需要能夠識別並擁抱它。
閉包是基於詞法作用域書寫代碼時所產生的自然結果,你甚至不需要爲了利用它們而有意 識地創建閉包。
當函數可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用 域之外執行。
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
bar()
對 a
的引用的方法是詞法作用域的查找規則,而這些規則只是閉包的一部分。但根據前面的定義,這並不是閉包。
下面一段代碼,清晰地展示了閉包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 這就是閉包的效果。
函數 bar()
的詞法作用域能夠訪問foo()
的內部作用域。然後我們將bar()
函數本身當作 一個值類型進行傳遞。
理解閉包
在foo()
執行後,通常會期待foo()
的整個內部作用域都被銷燬。事實上內部作用域依然存在(由於bar()
本身在使用),因此沒有被回收。
拜bar()
所聲明的位置所賜,它擁有涵蓋foo()
內部作用域的閉包,使得該作用域能夠一 直存活,以供bar()
在之後任何時間進行引用。
bar()
依然持有對該作用域的引用,而這個引用就叫作閉包。
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
timer
具有涵蓋wait(..)
作用域的閉包,因此還保有對變量message
的引用。
wait(..)
執行 1000 毫秒後,它的內部作用域並不會消失,timer
函數依然保有wait(..)
作用域的閉包。
循環和閉包
例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: 根據作用域的工作原理,儘管循環中的五個函數是在各個迭代中分別定義的, 但是它們都被封閉在一個共享的全局作用域中,因此實際上只有一個i
。由於函數延遲執行,最終循環執行完才調用,得到i
的值爲6。
例2.2:匿名函數有自己的作用域,變量j
用來在每個迭代中儲存i
的值。
例2.3:對例2.2代碼的改進。
在迭代內使用IIFE
會爲每個迭代都生成一個新的作用域,使得延遲函數的回調可以將新的作用域封閉在每個迭代內部,每個迭代中都會含有一個具有正確值的變量供我們訪問。
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
這個模式在 JavaScript 中被稱爲模塊。我們保持內部數據變量是隱 藏且私有的狀態。可以將這個對象類型的返回值看作本質上是模塊的公共 API。
模塊模式的兩個必要條件:
必須有外部的封閉函數,該函數必須至少被調用一次。
封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,並且可以訪問或者修改私有的狀態。
一個從函數調用所返回的,只有數據屬性而沒有閉包函數的對象並不是真正的模塊。
單例模式
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)
。爲了模塊的定義引入了包裝函數(可以傳入任何依賴),並且將返回值,也就是模塊的API,儲存在一個根據名字來管理的模塊列表中。
使用模塊:
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
總結
學而時習之,不亦說乎。看了一遍書籍,然後通過記筆記,又回顧了一遍,一些不懂的知識這次就弄懂了,同時還發現了一些漏過的知識。
很久以前,隔壁班的某某每套卷子都要重複做上6遍,然後每次成績都排列前茅。而他的母親卻是我的班主任,雖然沒有從老師那裏學到這種學習方式,但是老師卻我覺得學習也是一件快樂的事情,我很慶幸遇到這樣一位老師。