Web前端入門(mén)第 66 問(wèn):JavaScript 作用域應(yīng)用場(chǎng)景(閉包)
什么是作用域?
就像孫悟空給唐僧畫(huà)個(gè)圈圈一樣,這個(gè)圈圈就可以稱(chēng)之為作用域,這個(gè)比喻可能不太形象。
作用域和孫悟空的圈圈還是有點(diǎn)區(qū)別,作用域內(nèi)部可以獲得作用域外部的變量,而內(nèi)部的變量無(wú)法逃逸到作用域外面,如果逃逸出去了,那就造成內(nèi)存泄漏了,程序?qū)?huì)出現(xiàn)崩潰!
全局作用域
可以理解為就是放在 JS 最外層的那部分內(nèi)容,比如:變量、函數(shù)、對(duì)象等等。凡是定義在最外層的內(nèi)容,都是屬于全局作用域,在全局作用域下的任意函數(shù)都可訪問(wèn)到這部分內(nèi)容。
var wechat = '前端路引';
(function () {
function test1 () {
console.log(wechat);
}
test1(); // 輸出 '前端路引'
})()
以上代碼用到了自執(zhí)行函數(shù) (function () {})(),作用就是為了創(chuàng)建一個(gè)局部作用域,避免變量污染全局作用域,在很多優(yōu)秀的插件中都能看到它的影子。
上面代碼中的 wechat 變量,就是全局作用域下的變量,test1 函數(shù)定義在全局作用域內(nèi)部,所以對(duì)于 test1 函數(shù)來(lái)說(shuō),全局作用域中的變量它都是可以訪問(wèn)的。
函數(shù)作用域
也可稱(chēng)之為 局部作用域,生效范圍在函數(shù)內(nèi)部,在函數(shù)外面無(wú)法訪問(wèn)。
function test2 () {
var wechat = '前端路引';
console.log(wechat);
}
test2();
console.log(wechat); // 報(bào)錯(cuò):wechat is not defined
wechat 變量定義在函數(shù)內(nèi)部,便是函數(shù)作用域,在函數(shù)外面無(wú)法訪問(wèn),這就是局部作用域的特性。
塊級(jí)作用域
ES6 新增的玩法,一對(duì)花括號(hào)圈出來(lái)的區(qū)域,就稱(chēng)之為塊級(jí)作用域。需注意 var 聲明的變量是不存在塊級(jí)作用域的,只有 let 和 const 才存在塊級(jí)作用域。
{
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 輸出:前端路引
console.log(wechat2); // 報(bào)錯(cuò):wechat2 is not defined
console.log(wechat3); // 報(bào)錯(cuò):wechat3 is not defined
或者是像 if 條件判斷的花括號(hào)一樣也存在塊級(jí)作用域:
if (true) {
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 輸出:前端路引
console.log(wechat2); // 報(bào)錯(cuò):wechat2 is not defined
console.log(wechat3); // 報(bào)錯(cuò):wechat3 is not defined
當(dāng)然其他 while、for、do 等循環(huán)語(yǔ)句也存在塊級(jí)作用域。
作用域鏈
作用域鏈總是從內(nèi)部開(kāi)始,一圈一圈往外部查找,比如:
let globalVal = '全局';
function outer() {
let outerVal = '外部';
function inner() {
let innerVal = '內(nèi)部';
console.log(innerVal); // '內(nèi)部'(當(dāng)前作用域)
console.log(outerVal); // '外部'(外層作用域)
console.log(globalVal); // '全局'(全局作用域)
console.log(wechat); // 報(bào)錯(cuò):ReferenceError: wechat is not defined
}
inner();
}
outer();
當(dāng)內(nèi)部找不到的時(shí)候,就往外一層查找,外層找不到就在全局作用域找,如果全局作用域也找不到,就會(huì)報(bào)錯(cuò) ReferenceError。
閉包使用
基于作用域的特性,就有前輩們發(fā)現(xiàn)了 閉包 的用法,閉包這個(gè)東東,用得好呢可以說(shuō)是一把利劍,用得不好那就要反噬主人了。
閉包 的用處就是搭建函數(shù)內(nèi)部和外部的橋梁,使函數(shù)外部可以訪問(wèn)到函數(shù)內(nèi)部的變量。
閉包的基本樣子
function test1 () {
const wechat = '前端路引';
function test2 () {
console.log(wechat);
}
return test2;
}
test1()(); // 輸出:前端路引
上面代碼中 wechat 定義在函數(shù)內(nèi)部,屬于函數(shù)作用域,test2 也定義在函數(shù)內(nèi)部,使用 test2 訪問(wèn) wechat 變量的這種方法,就稱(chēng)之為 閉包。
為什么需要調(diào)用 test1 需要 ()() ?這個(gè)只是一種簡(jiǎn)寫(xiě),其完整寫(xiě)法應(yīng)該是這樣的:
const temp = test1(); // 獲得 test1 返回的函數(shù)
temp(); // 執(zhí)行返回函數(shù)輸出:'前端路引'
解決循環(huán)中的陷阱
在 ES6 出現(xiàn)之前,var 沒(méi)有塊級(jí)作用域這個(gè)特性,所以循環(huán)語(yǔ)句中常常會(huì)出現(xiàn)一些坑,比如:
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 輸出:3 3 3
}, 100)
}
上面代碼會(huì)輸出三次 3,原因是 var 沒(méi)有塊級(jí)作用域,setTimeout 函數(shù)執(zhí)行時(shí)候,獲得的是 for 循環(huán)之后的 i 值,所以最終輸出都是 3。
使用 let 優(yōu)化:
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 輸出:0 1 2
}, 100)
}
let 的塊級(jí)作用域可以完美保存每次 i 的值,所以最終輸出是 0 1 2,這也相當(dāng)于一種閉包的用法。
使用閉包優(yōu)化:
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 輸出:0 1 2
}, 100)
})(i)
}
將 i 以函數(shù)參數(shù)的形式傳入,這樣每次循環(huán)后,函數(shù)內(nèi)部獲得的 j 都是當(dāng)時(shí)的 i 值,所以最終輸出是 0 1 2。
上面代碼可能難以理解,那么換一種寫(xiě)法看看:
function temp (j) {
setTimeout(function () {
console.log(j); // 輸出:0 1 2
}, 100)
}
for (var i = 0; i < 3; i++) {
temp(i)
}
這樣寫(xiě)是否一眼就懂了?
(function (j) {})(i) 這種寫(xiě)法就相當(dāng)于一個(gè)自執(zhí)行函數(shù),這個(gè)函數(shù)有一個(gè)參數(shù) j,每次執(zhí)行的時(shí)候傳入 i 值而已。
為什么要一個(gè)小括號(hào)把 function (j) {} 包起來(lái)呢?
如果直接寫(xiě)成 function (j) {}(i),JS 解析器沒(méi)辦法識(shí)別這是一個(gè)函數(shù)調(diào)用,所以需要用小括號(hào)括起來(lái)。也可以寫(xiě)成 !function (j){}(i) ,也是自執(zhí)行函數(shù)的一種方式。其他的一元運(yùn)算符都可以用來(lái)這么玩,比如:
+function (j) {}(i)
-function (j) {}(i)
~function (j) {}(i)
個(gè)人覺(jué)得還是小括號(hào)比較容易理解。
私有變量
模塊化開(kāi)發(fā)的時(shí)候,可以使用閉包封裝內(nèi)部的私有變量,這樣外部就無(wú)法直接訪問(wèn),以保證私有變量安全,比如:
const counter = (function() {
let count = 0; // 私有變量
return {
increment: () => count++,
getCount: () => count,
};
})();
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined(無(wú)法直接訪問(wèn))
函數(shù)柯里化
閉包的又一種使用形式,柯里化就是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)的函數(shù)。如下:
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5); // 返回一個(gè)閉包,記住 a=5
console.log(add5(3)); // 8
內(nèi)存泄漏
由于閉包中的變量會(huì)常駐內(nèi)存,如果不及時(shí)釋放閉包,那么就會(huì)造成內(nèi)存泄漏,比如:
function createHeavyObj() {
const bigData = new Array(1000000).fill('*'); // 生成一個(gè)大對(duì)象
return () => bigData; // 閉包引用 bigData
}
let fn = createHeavyObj();
// 即使不再需要 bigData,它仍被閉包引用,無(wú)法被回收
// 解決方法:手動(dòng)解除引用
fn = null; // 解除閉包對(duì) bigData 的引用
如果沒(méi)有 fn = null 這句代碼,那么 bigData 會(huì)一直存在(直到頁(yè)面刷新或者被垃圾回收機(jī)制回收),如果 createHeavyObj 有多個(gè)地方調(diào)用,那么就可能導(dǎo)致內(nèi)存泄漏。
寫(xiě)在最后
JS 的代碼,閉包概念隨處可見(jiàn),在使用時(shí)也需特別小心,不放心的時(shí)候,就將變量釋放 xx = null!
文章首發(fā)于微信公眾號(hào)【前端路引】,歡迎 微信掃一掃 查看更多文章。
本文來(lái)自博客園,作者:前端路引,轉(zhuǎn)載請(qǐng)注明原文鏈接:http://www.rzrgm.cn/linx/p/18932449

浙公網(wǎng)安備 33010602011771號(hào)