前言
本文承接上篇 夯實基礎上篇-圖解 JavaScript 執行機制,請先閱讀上篇~
講基礎不容易,本文通過 7個demo和6張圖,和大家一起學習溫故作用域鏈和閉包,本文大綱:
- 什么是作用域鏈
- 什么是詞法作用域
- 什么是閉包
- 閉包的實際使用案例
什么是作用域鏈
正文開始~
請思考下面 demo 的 name 列印什么
function test() {
console.log(name)
}
function test1() {
const name = 'test1的name'
test()
}
const name = 'global的name'
test1()
通過執行背景關系來分析代碼的執行流程,執行到 test 函式時:
那 test 函式里的 name 是哪個呢?這就涉及到了作用域鏈的定義:變數和函式的查找鏈條就是作用域鏈,它決定了各級背景關系中的代碼在訪問變數和函式時的順序:查找變數和函式時,先在當前執行背景關系找,當前沒有,到下一個執行背景關系找,沒有再到下一個,直到全域執行背景關系,都沒有就報錯使用未定義的變數或函式,
而在每個執行背景關系的變數環境中,都包含了一個外部參考 outer,用來指向外部的執行背景關系,鏈條結構是當前執行背景關系 > 包含當前背景關系的背景關系1 > 包含背景關系1的背景關系2 ...,
而這個 demo 會列印global的name,原因是 test 執行背景關系的 outer 指向全域執行背景關系,包括 test1 的 outer 也是指向全域執行背景關系:
也許會有同學疑惑,為什么 test 的 outer 指向全域執行背景關系,而不是 test1,這是因為在 JavaScript 執行程序中,其作用域鏈是由詞法作用域決定的,
什么是詞法作用域
詞法作用域就是作用域是由代碼中函式宣告的位置來決定的,它是靜態的作用域,通過它就能夠預測代碼在執行程序中如何查找識別符號,它與函式是怎樣呼叫的沒有關系,所以剛才的例子列印的是global的name,
看個具體例子:
const count = 0
function test() {
const count = 1
function test1() {
const count = 2
function test2() {
const count = 3
}
}
}
其包含關系和作用域鏈:
事實上在 Global Scope 全域作用域(Window)之前,還有一個 Script Scope 腳本作用域,它存放的是當前 Script 內可訪問的 let 變數和 const 變數,而 var 變數存放在 Global 上的就不在 Script Scope,它類似于是腳本范圍內的全域作用域,在下面的 demo 中再舉例,
什么是閉包
閉包指的是那些參考了另一個函式作用域中變數的函式,通常是在嵌套函式中實作的,
比如這個例子:
var globalVariable = 1
const scriptVariable = 2
function test() {
let name = 'Jaychou'
return {
getName() {
const count = 1
return name
},
setName(newValue) {
name = newValue
}
}
}
const testFun = test()
console.log(testFun.getName()) // Jaychou
testFun.setName('小明')
console.log(testFun.getName()) // 小明
大家可以根據作用域鏈的知識,思考一下執行到console.log(testFun.getName())的 getName 里面的時候作用域鏈是怎樣的~
我們用瀏覽器的開發者工具看一下:
作用域鏈是當前作用域 》test 函式的閉包 》Script 作用域 》Global 作用域
- 為什么叫 test 函式的閉包?因為當
const testFun = test()的 test 函式執行完之后,test 的函式執行背景關系已經被銷毀了,但它回傳的{ getName(){}, setName(){} }物件被 testFun 參考著,而 getName 和 setName 參考著 test 函式內定義的 name 變數,所以這些被參考的變數依然需要被保存在記憶體中,而這些變數的集合稱為閉包 Closure; - 目前閉包內的 name 變數就只能通過 getName 和 setName 去訪問和設定,而這也是閉包的作用之一:封裝私有變數;
- 剛才說的 Script Scope 中保存著 scriptVariable 變數,globalVariable 變數是 var 宣告的,所以在 Global Scope(Window)中,
再看1個具體案例理解閉包:
const globalCount = 0
function test() {
const count = 0
return test1
function test1() {
const count1 = 1
return test2
function test2() {
const count2 = 2
console.log('test2', globalCount + count + count1 + count2)
}
}
}
test()()()
執行到 test2 內部的 console.log 那一行時,其作用域鏈是當前作用域 》test1 的閉包 》test 的閉包 》Script Scope 》Global Scope
閉包使用建議:當不需要使用了之后,注意要解除參考著閉包的變數,這樣閉包才會被釋放,比如第1個案例的 testFun 如果不需要用了,就把它釋放 testFun = null,
閉包的實際使用案例
封裝私有變數
就是剛才的 getName、setName 案例,通過 getName 獲取 name,通過 setName 設定 name
封裝單例
const Single = (function () {
let instance = null
return function () {
if (!instance) {
instance = {
name: 'jaychou',
age: 40
}
}
return instance
}
})()
const obj1 = new Single()
const obj2 = new Single()
console.log(obj1 === obj2) // true
這里只是舉個例子,具體的 instance 是什么型別,支持什么功能要看實際專案,
防抖和節流
防抖:
function debounce(fn, delay) {
let timer = null;
return function () {
let context = this;
let args = arguments;
timer && clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
}
}
節流:
function throttle(fn, interval) {
let last = 0;
return function () {
let now = +new Date()
if (now - last >= interval) {
fn.apply(this, arguments);
last = now;
}
}
}
更完整的防抖和節流的實作可參考 Lodash ,這里主要是演示閉包的使用
總結
閉包的使用場景很多,功能很強大,可以說在前端專案中經常可見例如 React Hooks 等等,這里只列舉了幾個很簡單的很實用的應用場景,
總結
本文主要介紹了作用域鏈和閉包,沿著 夯實基礎上篇-圖解 JavaScript 執行機制 來一起看的話應該比較容易理解,若對大家有所幫助,請不吝點贊關注~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/458565.html
標籤:其他
