





作用域和閉包是前端面試中,最可能考查的知識點。例如下面的題目:
> 題目:現在有個 HTML 片段,要求編寫代碼,點擊編號為幾的鏈接就`alert`彈出其編號
~~~
<ul>
<li>編號1,點擊我請彈出1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
~~~
一般不知道這個題目用閉包的話,會寫出下面的代碼:
~~~
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
list[i].addEventListener('click', function(){
alert(i + 1)
}, true)
}
~~~
實際上執行才會發現始終彈出的是`6`,這時候就應該通過閉包來解決:
~~~
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
list[i].addEventListener('click', function(i){
return function(){
alert(i + 1)
}
}(i), true)
}
~~~
要理解閉包,就需要我們從「執行上下文」開始講起。
### 執行上下文
先講一個關于**變量提升**的知識點,面試中可能會遇見下面的問題,很多候選人都回答錯誤:
> 題目:說出下面執行的結果(這里筆者直接注釋輸出了)
~~~
console.log(a) // undefined
var a = 100
fn('zhangsan') // 'zhangsan' 20
function fn(name) {
age = 20
console.log(name, age)
var age
}
console.log(b); // 這里報錯
// Uncaught ReferenceError: b is not defined
b = 100;
~~~
在一段 JS 腳本(即一個`<script>`標簽中)執行之前,要先解析代碼(所以說 JS 是解釋執行的腳本語言),解析的時候會先創建一個**全局執行上下文**環境,先把代碼中即將執行的(內部函數的不算,因為你不知道函數何時執行)變量、函數聲明都拿出來。變量先暫時賦值為`undefined`,函數則先聲明好可使用。這一步做完了,然后再開始正式執行程序。再次強調,這是在代碼執行之前才開始的工作。
我們來看下上面的面試小題目,為什么`a`是`undefined`,而`b`卻報錯了,實際 JS 在代碼執行之前,要「全文解析」,發現`var a`,知道有個`a`的變量,存入了執行上下文,而`b`沒有找到`var`關鍵字,這時候沒有在執行上下文提前「占位」,所以代碼執行的時候,提前報到的`a`是有記錄的,只不過值暫時還沒有賦值,即為`undefined`,而`b`在執行上下文沒有找到,自然會報錯(沒有找到`b`的引用)。
另外,一個函數在執行之前,也會創建一個**函數執行上下文**環境,跟**全局上下文**差不多,不過**函數執行上下文**中會多出`this``arguments`和函數的參數。參數和`arguments`好理解,這里的`this`咱們需要專門講解。
總結一下:
* 范圍:一段`<script>`、js 文件或者一個函數
* 全局上下文:變量定義,函數聲明
* 函數上下文:變量定義,函數聲明,`this`,`arguments`
### `this`
先搞明白一個很重要的概念 ——**`this`的值是在執行的時候才能確認,定義的時候不能確認!**為什么呢 —— 因為`this`是執行上下文環境的一部分,而執行上下文需要在代碼執行之前確定,而不是定義的時候。看如下例子
~~~
var a = {
name: 'A',
fn: function () {
console.log(this.name)
}
}
a.fn() // this === a
a.fn.call({name: 'B'}) // this === {name: 'B'}
var fn1 = a.fn
fn1() // this === window
~~~
`this`執行會有不同,主要集中在這幾個場景中
* 作為構造函數執行,構造函數中
* 作為對象屬性執行,上述代碼中`a.fn()`
* 作為普通函數執行,上述代碼中`fn1()`
* 用于`call``apply``bind`,上述代碼中`a.fn.call({name: 'B'})`
下面再來講解下什么是作用域和作用域鏈,作用域鏈和作用域也是常考的題目。
> 題目:如何理解 JS 的作用域和作用域鏈
### 作用域
ES6 之前 JS 沒有塊級作用域。例如
~~~
if (true) {
var name = 'zhangsan'
}
console.log(name)
~~~
從上面的例子可以體會到作用域的概念,作用域就是一個獨立的地盤,讓變量不會外泄、暴露出去。上面的`name`就被暴露出去了,因此,**JS 沒有塊級作用域,只有全局作用域和函數作用域**。
~~~
var a = 100
function fn() {
var a = 200
console.log('fn', a)
}
console.log('global', a)
fn()
~~~

全局作用域就是最外層的作用域,如果我們寫了很多行 JS 代碼,變量定義都沒有用函數包括,那么它們就全部都在全局作用域中。這樣的壞處就是很容易撞車、沖突。
~~~
// 張三寫的代碼中
var data = {a: 100}
// 李四寫的代碼中
var data = {x: true}
~~~
這就是為何 jQuery、Zepto 等庫的源碼,所有的代碼都會放在`(function(){....})()`中。因為放在里面的所有變量,都不會被外泄和暴露,不會污染到外面,不會對其他的庫或者 JS 腳本造成影響。這是函數作用域的一個體現。
附:ES6 中開始加入了塊級作用域,使用`let`定義變量即可,如下:
~~~
if (true) {
let name = 'zhangsan'
}
console.log(name) // 報錯,因為let定義的name是在if這個塊級作用域
~~~
### 作用域鏈
首先認識一下什么叫做**自由變量**。如下代碼中,`console.log(a)`要得到`a`變量,但是在當前的作用域中沒有定義`a`(可對比一下`b`)。當前作用域沒有定義的變量,這成為**自由變量**。自由變量如何得到 —— 向父級作用域尋找。
~~~
var a = 100
function fn() {
var b = 200
console.log(a)
console.log(b)
}
fn()
~~~
如果父級也沒呢?再一層一層向上尋找,直到找到全局作用域還是沒找到,就宣布放棄。這種一層一層的關系,就是**作用域鏈**。
~~~
var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) // 自由變量,順作用域鏈向父作用域找
console.log(b) // 自由變量,順作用域鏈向父作用域找
console.log(c) // 本作用域的變量
}
F2()
}
F1()
~~~
### 閉包
講完這些內容,我們再來看一個例子,通過例子來理解閉包。
~~~
function F1() {
var a = 100
return function () {
console.log(a)
}
}
var f1 = F1()
var a = 200
f1()
~~~

自由變量將從作用域鏈中去尋找,但是**依據的是函數定義時的作用域鏈,而不是函數執行時**,以上這個例子就是閉包。閉包主要有兩個應用場景:
* **函數作為返回值**,上面的例子就是
* **函數作為參數傳遞**,看以下例子
~~~
function F1() {
var a = 100
return function () {
console.log(a)
}
}
function F2(f1) {
var a = 200
console.log(f1())
}
var f1 = F1()
F2(f1)
~~~

至此,對應著「作用域和閉包」這部分一開始的點擊彈出`alert`的代碼再看閉包,就很好理解了。
- 文檔說明
- 大廠面試題
- HTML
- 001.如何遍歷一個dom樹
- 002.為什么操作DOM會很慢
- 003.瀏覽器渲染HTML的步驟
- 004.DOM和JavaScript的關系
- JS
- 001.數組扁平化并去重排序
- 002.高階函數
- 003.sort() 對數組進行排序
- 004.call 、 apply 和bind的區別
- 006.0.1+0.2為什么等于0.30000000000000004
- 011.var、let、const 的區別及實現原理?
- 010.new操作符都做了什么
- 009.a.b.c.d 和 a['b']['c']['d'],哪個性能更高?
- 016.什么是防抖和節流?有什么區別?如何實現?
- 017.['1', '2', '3'].map(parseInt) what & why ?
- 018.為什么 for 循環嵌套順序會影響性能?
- 019.介紹模塊化發展歷程
- 020.push輸出問題
- 021.判斷數組的三個方法
- 022.全局作用域中,用 const 和 let 聲明的變量不在 window 上,那到底在哪里?如何去獲取?
- 023.輸出以下代碼的執行結果并解釋為什么
- 024.ES6 代碼轉成 ES5 代碼的實現思路是什么
- 025.為什么普通 for 循環的性能遠遠高于 forEach 的性能,請解釋其中的原因。
- 026.數組里面有10萬個數據,取第一個元素和第10萬個元素的時間相差多少
- 027.變量類型
- 028.原型和原型鏈
- 029.作用域和閉包
- 030. 異步
- 031.ES6/7 新標準的考查
- 024.事件冒泡/事件代理
- 025.手寫 XMLHttpRequest 不借助任何庫
- 026.什么是深拷貝?
- 0027.克隆數組的方法
- 0028.ES6之展開運算符(...)
- 0029.arguments
- 0030. requestAnimationFrame
- 0031.遞歸爆棧問題與解決
- 021.簡單改造下面的代碼,使之分別打印 10 和 20
- 032.箭頭函數與普通函數
- 033.去除掉html標簽字符串里的所有屬性
- 034.查找公共父節點
- 035.Promise
- 0036.JSON.stringify ()
- CSS
- 001. BFC
- 002.介紹下 BFC、IFC、GFC 和 FFC
- 003.分析比較 opacity: 0、visibility: hidden、display: none 優劣和適用場景
- 004.怎么讓一個 div 水平垂直居中
- 005.重排重繪
- 006.inline/block/inline-block的區別
- 007.選擇器的權重和優先級
- 008.盒模型
- 009.清除浮動
- 010.flex
- 011.nth-child和nth-of-type的區別
- 0012.overflow
- 0013.CSS3中translate、transform和translation的區別和聯系
- 0014.flex
- 0015.px、em、rem
- 0016.width:100%
- 網絡
- 001.講解下HTTPS的工作原理
- 002.介紹下 HTTPS 中間人攻擊
- 003.談談你對TCP三次握手和四次揮手的理解
- 004.A、B 機器正常連接后,B 機器突然重啟,問 A 此時處于 TCP 什么狀態
- 005.簡單講解一下http2的多路復用
- 006. 介紹下 http1.0、1.1、2.0 協議的區別?
- 007.永久性重定向(301)和臨時性重定向(302)對 SEO 有什么影響
- 008.URL從輸入到頁面展示的過程
- 009.接口如何防刷
- 010.http狀態碼?
- 0111.跨域/如何解決?
- 012.cookie 和 localStorage 有何區別?
- 013.Fetch API
- 014.跨域Ajax請求時是否帶Cookie的設置
- 0015.協商緩存和強緩存
- 性能優化
- 001.前后端分離的項目如何seo
- 002.性能優化的方法
- 003.防抖和節流
- React
- 001.React 中 setState 什么時候是同步的,什么時候是異步的?
- 002.Virtual DOM 真的比操作原生 DOM 快嗎?談談你的想法。
- 003.Hooks 的特別之處
- 004.元素和組件有什么區別?
- 005.什么是 Pure Components?
- 006.HTML 和 React 事件處理有什么區別?
- 007.如何將參數傳遞給事件處理程序或回調函數?
- 008.如何創建 refs?
- 009.什么是 forward refs?
- 010.什么是 Virtual DOM?
- 011.什么是受控組件、非受控組件?
- 012.什么是 Fragments ?
- 013.為什么React元素有一個$$typeof屬性?
- 014.如何在 React 中創建組件?
- 015.React 如何區分 Class 和 Function?
- 016.React 的狀態是什么?
- 017.React 中的 props 是什么?
- 018.狀態和屬性有什么區別?
- 019.如何在 JSX 回調中綁定方法或事件處理程序?
- 020.什么是 "key" 屬性,在元素數組中使用它們有什么好處?
- 021.為什么順序調用對 React Hooks 很重要?
- 022.setState如何知道該做什么?
- 023.hook規則?
- 024.Hooks 與 Class 中調用 setState 有不同的表現差異么?
- 025.useEffect
- 026.fiber的作用
- 027.context的作用?
- 028.setState何時同步何時異步?
- 029.react性能優化
- 030.fiber
- 031.React SSR
- 異步
- 001.介紹下promise
- 002.Async/Await 如何通過同步的方式實現異步
- 003.setTimeout、Promise、Async/Await 的區別
- 004.JS 異步解決方案的發展歷程以及優缺點
- 005.Promise 構造函數是同步執行還是異步執行,那么 then 方法呢?
- 006.模擬實現一個 Promise.finally
- 012.簡單手寫實現promise
- 015.用Promise對象實現的 Ajax
- 007.簡單實現async/await中的async函數
- 008.設計并實現 Promise.race()
- 009.Async/await
- 010.珠峰培訓promise
- git
- 001.提交但沒有push
- 002.gitignore沒有作用?
- Node
- 001.用nodejs,將base64轉化成png文件
- Koa
- 001.koa和express的區別
- 數據庫
- redux
- 001.redux 為什么要把 reducer 設計成純函數
- 002.在 React 中如何使用 Redux 的 connect() ?
- 003.mapStateToProps() 和 mapDispatchToProps() 之間有什么區別?
- 004.為什么 Redux 狀態函數稱為 reducers ?
- 005.如何在 Redux 中發起 AJAX 請求?
- 006.訪問 Redux Store 的正確方法是什么?
- 007.React Redux 中展示組件和容器組件之間的區別是什么?
- 008.Redux 中常量的用途是什么?
- 009.什么是 redux-saga?
- 設計模式
- 公司題目
- 001.餓了么
- 001.div垂直水平居中(flex、絕對定位)
- 002.React子父組件之間如何傳值
- 003.Emit事件怎么發,需要引入什么
- 004.介紹下React高階組件,和普通組件有什么區別
- 005.一個對象數組,每個子對象包含一個id和name,React如何渲染出全部的name
- 006.在哪個生命周期里寫
- 007.其中有幾個name不存在,通過異步接口獲取,如何做
- 008.渲染的時候key給什么值,可以使用index嗎,用id好還是index好
- 009.webpack如何配sass,需要配哪些loader
- 010.配css需要哪些loader
- 011.如何配置把js、css、html單獨打包成一個文件
- 012.監聽input的哪個事件,在什么時候觸發
- 013.兩個元素塊,一左一右,中間相距10像素
- 014.上下固定,中間滾動布局如何實現
- 016.取數組的最大值(ES5、ES6)
- 017.apply和call的區別
- 018.ES5和ES6有什么區別
- 019.some、every、find、filter、map、forEach有什么區別
- 020.上述數組隨機取數,每次返回的值都不一樣
- 021.如何找0-5的隨機數,95-99呢
- 022.頁面上有1萬個button如何綁定事件
- 023.如何判斷是button
- 024.頁面上生成一萬個button,并且綁定事件,如何做(JS原生操作DOM)
- 025.循環綁定時的index是多少,為什么,怎么解決
- 026.頁面上有一個input,還有一個p標簽,改變input后p標簽就跟著變化,如何處理
- 瀏覽器相關
- 001.性能優化
- 002.web安全
- 003.獲取瀏覽器大小
- 004.從輸入 URL 到頁面加載完成的過程中都發生了什么事情?
- 后端
- 001.分布式
- zuku
- 字節
- webpack
- webpack的打包原理是什么
- Webpack-- 常見面試題
- webscoket