JavaScript 閉包
為了更好地理解 JavaScript
閉包,筆者將先從 JavaScript
執行上下文以及 JavaScript
作用域開始寫起,如果讀者對這方面已經瞭解了,可以直接跳過。
1. 執行上下文
簡單來說,JavaScript
有三種程式碼執行環境,分別是:
- Global Code 是
JavaScript
程式碼開始執行的預設環境 - Function Code 是
JavaScript
函式執行的環境 - Eval Code 是 利用
eval
函式執行的程式碼環境
執行上下文可以理解為上述為了執行對應的程式碼而建立的環境。
例如在上述某個環境執行前,我們需要考慮
-
該環境下的所有變數物件
例如用
let
const
var
定義的變數,或者是函式宣告,函式引數arguments
等 -
該環境下的作用域鏈
包括 該環境下的所用變數物件 以及父親作用域 (我們當然可以用到父親作用域提供的函式和變數
-
是誰執行了這個環境 (this)
擁有了這些東西后,我們才可以分配記憶體,起到一個準備的作用。
我們用下述程式碼加深對執行上下文的理解
let global = 1;
function getAgeByName(name) {
let xxx = 1;
function age() {
console.log(this);
const age = 10;
if (name === "huro")
return age;
else
return age * 10;
}
return age();
}
假設我們執行 age
函式
-
建立當前環境下的作用域鏈
這裡作用域鏈顯然是 當前環境下的變數(還沒初始化)以及父親作用域(這裡麵包括了
global
變數以及xxx
變數,name
形參)等,這些我們當然都可以在age
中使用。 -
建立當前環境下的變數
當前環境下的變數包括接收到的形參
arguments
age
變數 -
設定
this
是誰由於沒有明確指定是誰呼叫
age
方法,因此this
在瀏覽器環境下設定為window
在建立好上下文後當需要進行變數的搜尋的時候
會先搜尋當前環境下的變數,如果沒有隨著作用域鏈往上搜尋。
另外由於 ES6
箭頭函式並不建立 this
,通過上述講解,相信你可以瞭解為什麼箭頭函式用的是上一層函式的 this
了。
上述提到了作用域,作用域也分幾種
作用域
-
塊級作用域
在很多語言的規範裡經常告訴我們,如果你需要一個變數再去定義,但是如果你使用
JavaScript
的var
定義變數,你最好別這麼幹。最好是都定義在頭部。因為
var
沒有塊級作用域
if (true) {
var name = "huro";
}
console.log(name); // huro
不過當你使用 let
或 const
定義的話,就不存在這樣的問題。
if (true) {
let name = "huro";
}
console.log(name); // name is not defined
-
函式和全域性作用域
這個和大部分語言是一致的。
let a = 1;
function fn() {
let a = 2;
console.log(a); // 2
}
閉包
閉包實質上可以理解為"定義在一個函式內部的函式"
擁有了作用域和作用域鏈,內部函式可以訪問定義他們的外部函式的引數和變數,這非常好。
如果我們希望一個物件不被外界更改(汙染)
const myObject = () => {
let value = 1;
return {
increment: (inc) => {
value += inc;
}
getValue: () => {
return value;
}
}
}
由於外界不可能直接訪問到 value
因此就不可能修改他。
利用閉包
在建構函式中,物件的屬性都是可見的,沒法得到私有變數和私有函式。一些不知情的程式設計師接受了一種偽裝私有的模式。
例如
function Person() {
this.________name = "huro";
}
用於保護這個屬性,並且希望使用程式碼的使用者假裝看不到這種奇怪的成員元素,但是其實編譯器並不知情,仍會在你輸入 xxx.__
的時候提示你有 xxx.________name
屬性
利用閉包可以很輕易的解決這個問題。
function Person(spec) {
let { name } = spec;
this.getName = () => {
return name;
}
this.setName = (name) => {
name = "huro";
}
return this;
}
const p = new Person({ name: "huro" });
console.log(p.name) // undefined
console.log(p.getName()) // "huro"
注意閉包帶來的問題
<body>
<div class="name">
huro
</div>
<div class="name">
lero
</div>
</body>
const addHandlers = (nodes) => {
let i ;
for (i = 0; i < nodes.length; i += 1) {
nodes[i].addEventListener("click", () => {
alert(i); // 總是 nodes.length
})
}
}
const doms = document.getElementsByClassName("name");
addHandlers(doms);
你會發現,列印出來的結果總是 2
,這是作用域的原因,由於 i
是父作用域鏈的變數,當向上查詢的時候,i
已經變成 2
了。
正確的寫法應該是
const addHandlers = (nodes) => {
for (let i = 0; i < nodes.length; i += 1) {
nodes[i].addEventListener("click", () => {
alert(i);
})
}
}
const doms = document.getElementsByClassName("name");
addHandlers(doms);