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);