javascript
Web前端入门第 66 问:JavaScript 作用域应用场景(闭包)
什么是作用域?
就像孫悟空給唐僧畫個圈圈一樣,這個圈圈就可以稱之為作用域,這個比喻可能不太形象。
作用域和孫悟空的圈圈還是有點區別,作用域內部可以獲得作用域外部的變量,而內部的變量無法逃逸到作用域外面,如果逃逸出去了,那就造成內存泄漏了,程序將會出現崩潰!
全局作用域
可以理解為就是放在 JS 最外層的那部分內容,比如:變量、函數、對象等等。凡是定義在最外層的內容,都是屬于全局作用域,在全局作用域下的任意函數都可訪問到這部分內容。
var wechat = '前端路引';
(function () {
function test1 () {
console.log(wechat);
}
test1(); // 輸出 '前端路引'
})()
以上代碼用到了自執行函數 (function () {})(),作用就是為了創建一個局部作用域,避免變量污染全局作用域,在很多優秀的插件中都能看到它的影子。
上面代碼中的 wechat 變量,就是全局作用域下的變量,test1 函數定義在全局作用域內部,所以對于 test1 函數來說,全局作用域中的變量它都是可以訪問的。
函數作用域
也可稱之為 局部作用域,生效范圍在函數內部,在函數外面無法訪問。
function test2 () {
var wechat = '前端路引';
console.log(wechat);
}
test2();
console.log(wechat); // 報錯:wechat is not defined
wechat 變量定義在函數內部,便是函數作用域,在函數外面無法訪問,這就是局部作用域的特性。
塊級作用域
ES6 新增的玩法,一對花括號圈出來的區域,就稱之為塊級作用域。需注意 var 聲明的變量是不存在塊級作用域的,只有 let 和 const 才存在塊級作用域。
{
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 輸出:前端路引
console.log(wechat2); // 報錯:wechat2 is not defined
console.log(wechat3); // 報錯:wechat3 is not defined
或者是像 if 條件判斷的花括號一樣也存在塊級作用域:
if (true) {
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 輸出:前端路引
console.log(wechat2); // 報錯:wechat2 is not defined
console.log(wechat3); // 報錯:wechat3 is not defined
當然其他 while、for、do 等循環語句也存在塊級作用域。
作用域鏈
作用域鏈總是從內部開始,一圈一圈往外部查找,比如:
let globalVal = '全局';
function outer() {
let outerVal = '外部';
function inner() {
let innerVal = '內部';
console.log(innerVal); // '內部'(當前作用域)
console.log(outerVal); // '外部'(外層作用域)
console.log(globalVal); // '全局'(全局作用域)
console.log(wechat); // 報錯:ReferenceError: wechat is not defined
}
inner();
}
outer();
當內部找不到的時候,就往外一層查找,外層找不到就在全局作用域找,如果全局作用域也找不到,就會報錯 ReferenceError。
閉包使用
基于作用域的特性,就有前輩們發現了 閉包 的用法,閉包這個東東,用得好呢可以說是一把利劍,用得不好那就要反噬主人了。
閉包 的用處就是搭建函數內部和外部的橋梁,使函數外部可以訪問到函數內部的變量。
閉包的基本樣子
function test1 () {
const wechat = '前端路引';
function test2 () {
console.log(wechat);
}
return test2;
}
test1()(); // 輸出:前端路引
上面代碼中 wechat 定義在函數內部,屬于函數作用域,test2 也定義在函數內部,使用 test2 訪問 wechat 變量的這種方法,就稱之為 閉包。
為什么需要調用 test1 需要 ()() ?這個只是一種簡寫,其完整寫法應該是這樣的:
const temp = test1(); // 獲得 test1 返回的函數
temp(); // 執行返回函數輸出:'前端路引'
解決循環中的陷阱
在 ES6 出現之前,var 沒有塊級作用域這個特性,所以循環語句中常常會出現一些坑,比如:
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 輸出:3 3 3
}, 100)
}
上面代碼會輸出三次 3,原因是 var 沒有塊級作用域,setTimeout 函數執行時候,獲得的是 for 循環之后的 i 值,所以最終輸出都是 3。
使用 let 優化:
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 輸出:0 1 2
}, 100)
}
let 的塊級作用域可以完美保存每次 i 的值,所以最終輸出是 0 1 2,這也相當于一種閉包的用法。
使用閉包優化:
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 輸出:0 1 2
}, 100)
})(i)
}
將 i 以函數參數的形式傳入,這樣每次循環后,函數內部獲得的 j 都是當時的 i 值,所以最終輸出是 0 1 2。
上面代碼可能難以理解,那么換一種寫法看看:
function temp (j) {
setTimeout(function () {
console.log(j); // 輸出:0 1 2
}, 100)
}
for (var i = 0; i < 3; i++) {
temp(i)
}
這樣寫是否一眼就懂了?
(function (j) {})(i) 這種寫法就相當于一個自執行函數,這個函數有一個參數 j,每次執行的時候傳入 i 值而已。
為什么要一個小括號把 function (j) {} 包起來呢?
如果直接寫成 function (j) {}(i),JS 解析器沒辦法識別這是一個函數調用,所以需要用小括號括起來。也可以寫成 !function (j){}(i) ,也是自執行函數的一種方式。其他的一元運算符都可以用來這么玩,比如:
+function (j) {}(i)
-function (j) {}(i)
~function (j) {}(i)
個人覺得還是小括號比較容易理解。
私有變量
模塊化開發的時候,可以使用閉包封裝內部的私有變量,這樣外部就無法直接訪問,以保證私有變量安全,比如:
const counter = (function() {
let count = 0; // 私有變量
return {
increment: () => count++,
getCount: () => count,
};
})();
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined(無法直接訪問)
函數柯里化
閉包的又一種使用形式,柯里化就是把接受多個參數的函數變換成接受一個單一參數的函數。如下:
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5); // 返回一個閉包,記住 a=5
console.log(add5(3)); // 8
內存泄漏
由于閉包中的變量會常駐內存,如果不及時釋放閉包,那么就會造成內存泄漏,比如:
function createHeavyObj() {
const bigData = new Array(1000000).fill('*'); // 生成一個大對象
return () => bigData; // 閉包引用 bigData
}
let fn = createHeavyObj();
// 即使不再需要 bigData,它仍被閉包引用,無法被回收
// 解決方法:手動解除引用
fn = null; // 解除閉包對 bigData 的引用
如果沒有 fn = null 這句代碼,那么 bigData 會一直存在(直到頁面刷新或者被垃圾回收機制回收),如果 createHeavyObj 有多個地方調用,那么就可能導致內存泄漏。
寫在最后
JS 的代碼,閉包概念隨處可見,在使用時也需特別小心,不放心的時候,就將變量釋放 xx = null!
總結
以上是生活随笔為你收集整理的Web前端入门第 66 问:JavaScript 作用域应用场景(闭包)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 泰康人寿属于私企吗
- 下一篇: Qt 的一个大坑:visual stud