作用域:
分為函數(shù)作用域,和塊級作用域;
函數(shù)作用域
函數(shù)作用域外面的無法訪問函數(shù)作用域內(nèi)部的變量和函數(shù),這樣就可以將一些變量和函數(shù)隱藏起來;
隱藏起來的好處是
- 形成命名空間,避免各個函數(shù)里面的變量沖突
- 實現(xiàn)模塊管理
內(nèi)部可以訪問外部的;
function foo(a) { var b = 2;
// 一些代碼
function bar() {
// ...
}
// 更多的代碼 var c = 3;
}
bar(); // 失敗
console.log( a, b, c ); // 三個全都失敗
此時,foo里面就是一個函數(shù)作用域,可以bar里面又是一個作用域;最外面當然就是全局作用域;
可以把函數(shù)看著一個可以單向向外訪問的圈子;
函數(shù)表達式 vs 函數(shù)聲明
這里需要重點區(qū)分一下:
- 函數(shù)聲明: function 是聲明中 的第一個詞,那么就是一個函數(shù)聲明;
- 函數(shù)表達式:除此之外就是函數(shù)表達式;
- 函數(shù)表達式可以是匿名的,函數(shù)聲明不可以;
函數(shù)聲明和函數(shù)表達式之間最重要的區(qū)別是它們的名稱標識符將會綁定在何處;
例子
var a = 2;
(function foo(){ // <-- 添加這一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及這一行
console.log( a ); // 2
這里的
(function foo(){ .. })是一個函數(shù)表達式;而不是一 個標準的函數(shù)聲明;
所以,foo 被綁定在函數(shù)表達式自身的函數(shù)中而不是所在作用域中。換句話說,
(function foo(){ .. })作為函數(shù)表達式意味著foo只能在..所代表的位置中被訪問,外部作用域則不行。foo 變量名被隱藏在自身中意味著不會非必要地污染外部作 用域。立即執(zhí)行函數(shù)表達式:由于
foo被包含在一對( )括號內(nèi)部,因此成為了一個表達式;通過在末尾加上另外一個 ( ) 可以立即執(zhí)行這個函數(shù);即,(function foo(){ .. })()。第一個 ( ) 將函數(shù)變成表 達式,第二個 ( ) 執(zhí)行了這個函數(shù)。還可以穿參數(shù)
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
這樣就可以訪問外面的a了,因為訪問變量a的時候就近原則,就得到了3;
塊級作用域
1. { }
var 定義的變量和函數(shù),和在當前塊級所處作用域定義沒有什么區(qū)別;
let,const 在塊級作用域外面就訪問不到;
簡單的說{ }這個就形成了一個塊級作用域;
例子:
{
var a = 44;
let b = 22;
const c = 33
}
a // 44;
b // Uncaught ReferenceError: b is not defined;找不到引用,報錯;
c // Uncaught ReferenceError: c is not defined;找不到引用,報錯;
2. with
用 with 從對象中創(chuàng)建出的作用域僅在 with 聲明中而非外 部作用域中有效。
3. try/catch
JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會創(chuàng)建一個塊作
用域,其中聲明的變量僅在 catch 內(nèi)部有效。
閉包
無論論通過何種手段將內(nèi)部函數(shù)傳遞到所在的作用域以外,它都會持有對原始定義作用域的引用,無論在何處執(zhí)行這個函數(shù)都會使用閉包。
個人所理解的閉包就是一個作用域,及作用域內(nèi)的變量和函數(shù)的緩存;不會被釋放了,以供在今后訪問;不得被垃圾回收掉
不知道大家是否還記得JavaScript的垃圾回收機制;
垃圾收集:就是執(zhí)行完后,對沒有引用的變量進行釋放;常見手動釋放就是設置成null;
例子1
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果。
分析:
- 函數(shù) bar() 的作用域能夠訪問 foo() 的內(nèi)部作用域。
bar() 顯然可以被正常執(zhí)行。并且,它能在自己定義的作用域以外的地方 執(zhí)行。 - 在 foo() 執(zhí)行后,通常會期待foo()的整個內(nèi)部作用域都被銷毀,因為我們知道引擎有垃圾回收器用來釋放不再使用的內(nèi)存空間。由于看上去 foo() 的內(nèi)容不會再被使用,所以很自然地會考慮對其進行回收。
- 而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生。事實上內(nèi)部作用域依然存在,因此沒有被回收。誰在使用這個內(nèi)部作用域?原來是 bar() 本身在使用。
- 拜 bar() 所聲明的位置所賜,它擁有涵蓋foo()內(nèi)部作用域的閉包,使得該作用域能夠一 直存活,以供 bar() 在之后任何時間進行引用。
bar()依然持有對該作用域的引用,而這個引用就叫作閉包。
例子2
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 媽媽快看呀,這就是閉包!
}
foo(); // 2
等效如下:
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; //將baz分配給全局變量
}
function bar() {
fn(); // 媽媽快看呀,這就是閉包!
}
foo();
bar(); // 2
按正常的作用域思考方式,bar是沒有辦法訪問foo的內(nèi)部的變量的;
- 但是foo可以訪問外部作用域下的bar;
- bar在foo內(nèi)部;
- 將baz傳遞給bar的內(nèi)部,baz無論在哪里都依然持有對foo內(nèi)部變量的引用;
baz 和 變量a,還有foo形成了一個閉包,這個作用域?qū)⒈灰婢彺嫫饋?;baz隨時都可以訪問;
function foo(a) {
var b = 2;
// 一些代碼
function bar() {
// ...
}
// 更多的代碼
var c = 3;
}
foo();
bar(); // 失敗
console.log( a, b, c ); // 三個全都失敗
這種就無法訪問foo里面的變量和函數(shù)了,因為foo里面都是局部變量,外部無法直接訪問,這種里面變量再會被其他地方引用,將會被引擎垃圾回收釋放掉。
形成閉包的條件
- 一個函數(shù)
foo包含一個函數(shù)baz和一個變量a;(名字隨意)baz內(nèi)部存在對a的引用;foo需要被執(zhí)行;
正確例子示范
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
這就是個閉包;
- timer和message都在wait內(nèi)部
- timer對wait的message有引用;
- wait被執(zhí)行了
一般來說,只要使 用了回調(diào)函數(shù),實際上就是在使用閉包!
錯誤例子示范
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
// 打印5次6;
這樣不行!這只是一個都沒有的空作用域。不能形成閉包
修改1:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})(i);//從外部傳進來
}
修改2:
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
//塊作用域和閉包聯(lián)手便可天下無敵
應用——模塊
模塊有兩個主要特征:
- 為創(chuàng)建內(nèi)部作用域而調(diào)用了一個包裝函數(shù);
- 包裝函數(shù)的返回 值必須至少包括一個對內(nèi)部函數(shù)的引用,這樣就會創(chuàng)建涵蓋整個包裝函數(shù)內(nèi)部作用域的閉包。
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome(){
console.log( bar.hello( hungry ).toUpperCase() )
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
bar.hello( "hippo" )
); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
"foo" 和 "bar" 模塊通過一個返回公共 API 的函數(shù)來定義的。"foo" 甚至接受 "bar" 的 示例作為依賴參數(shù),并能相應地使用它。
總結(jié)
最后記?。寒敽瘮?shù)可以記住并訪問所在的作用域,即使函數(shù)是在當前作用域之外執(zhí)行,這時 就產(chǎn)生了閉包。