
正文
上下文 是Javascript 中的一個比較重要的概念, 可能很多朋友對這個概念并不是很熟悉, 那換成「作用域」 和 「閉包」呢?是不是就很親切了。
「作用域」和「閉包」 都是和「執(zhí)行上下文」密切相關(guān)的兩個概念。
在解釋「執(zhí)行上下文」是什么之前, 我們還是先回顧下「作用域」 和 「閉包」。
作用域
首先, 什么是作用域呢?
域, 即是范圍。
作用域,其實就是某個變量或者函數(shù)的可訪問范圍。
它控制著變量和函數(shù)的可見性和生命周期。
作用域也分為: 「全局作用域 」和 「局部作用域」。
全局作用域:
如果一個對象在任何位置都能被訪問到, 那么這個對象, 就是一個全局對象, 擁有一個全局作用域。
擁有全局作用域的對象可以分為以下幾種情況:
- 定義在最外層的變量
- 全局對象的屬性
- 任何地方隱式定義的變量(即:未定義就直接賦值的變量)。隱式定義的變量都會定義在全局作用域中。
局部作用域:
JavaScript的作用域是通過函數(shù)來定義的。
在一個函數(shù)中定義的變量, 只對此函數(shù)內(nèi)部可見。
這類作用域,稱為局部作用域。
還有一個概念和作用域聯(lián)系密切, 那就是作用域鏈。
作用域鏈
作用域鏈是一個集合, 包含了一系列的對象, 它可以用來檢索上下文中出現(xiàn)的各類標識符(變量, 參數(shù), 函數(shù)聲明等)。
函數(shù)在定義的時候, 會把父級的變量對象AO/VO的集合保存在內(nèi)部屬性 [[scope]] 中,該集合稱為作用域鏈。
- AO : Activation Object 活動對象
- VO : Variable object 變量對象
Javascript 采用了詞法作用域(靜態(tài)作用域),函數(shù)運行在他們被定義的作用域中,而不是他們被執(zhí)行的作用域。
看個簡單的例子 :
var a = 3;
?
function foo () {
console.log(a)
}
?
function bar () {
var a = 6
foo()
}
?
bar()
web前端開發(fā)學(xué)習(xí)Q-q-u-n: 731771211,分享學(xué)習(xí)的方法和需要注意的小細節(jié),不停更新最新的教程和學(xué)習(xí)方法(詳細的前端項目實戰(zhàn)教學(xué)視頻,PDF)
如果js采用動態(tài)作用域,打印出來的應(yīng)該是6而不是3.
這個例子說明了javasript是靜態(tài)作用域。
此函數(shù)作用域鏈的偽代碼:
function bar() {
function foo() {
// ...
}
}
?
bar.[[scope]] = [
globalContext.VO
];
?
foo.[[scope]] = [
barContext.AO,
globalContext.VO
];
函數(shù)在運行激活的時候,會先復(fù)制 [[scope]] 屬性創(chuàng)建作用域鏈,然后創(chuàng)建變量對象VO,然后將其加入到作用域鏈。
executionContextObj: {
VO: {},
scopeChain: [VO, [[scope]]]
}
總的來說, VO要比AO的范圍大很多, VO是負責(zé)把各個調(diào)用的函數(shù)串聯(lián)起來的。
VO是外部的, 而AO是函數(shù)自身內(nèi)部的。
下面我們說一下閉包。
閉包
閉包也是面試中經(jīng)常會問到的問題, 考察的形式也很靈活, 譬如:
- 描述下什么是閉包
- 寫一段閉包的代碼
- 閉包有什么用
- 給你一個閉包的例子,讓你修改, 或者看輸出
那閉包究竟是什么呢?
說白了, 閉包其實也就是函數(shù), 一個可以訪問自由變量的函數(shù)。
自由變量: 不在函數(shù)內(nèi)部聲明的變量。
很多所謂的代碼規(guī)范里都說, 不要濫用閉包, 會導(dǎo)致性能問題, 我當(dāng)然是不太認同這種說法的, 不過這個說法被人提出來,也是有一些原因的。
畢竟,閉包里的自由變量會綁定在代碼塊上,在離開創(chuàng)造它的環(huán)境下依舊生效,而使用代碼塊的人可能無法察覺。
閉包里的自由變量的形式有很多,先舉個簡單例子。
function add(p1){
return function(p2){
return p1 + p2;
}
}
?
var a = add(1);
var b = add(2);
?
a(1) //2
b(1) // 3
web前端開發(fā)學(xué)習(xí)Q-q-u-n: 731771211,分享學(xué)習(xí)的方法和需要注意的小細節(jié),不停更新最新的教程和學(xué)習(xí)方法(詳細的前端項目實戰(zhàn)教學(xué)視頻,PDF)
在上面的例子里,a 和 b這兩個函數(shù),代碼塊是相同的,但若是執(zhí)行a(1)和b(1)的結(jié)果卻是不同的,原因在于這兩者所綁定的自由變量是不同的,這里的自由變量其實就是函數(shù)體里的 p1 。
自由變量的引入,可以起到和OOP里的封裝同樣作用,我們可以在一層函數(shù)里封裝一些不被外界知曉的自由變量,從而達到相同的效果, 很多模塊的封裝, 也是利用了這個特性。
然后說一下我遇到的真實案例, 是去年面試騰訊QQ音樂的一道筆試題:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
這段代碼會輸出一堆 6, 讓你改一下, 輸出 1, 2, 3, 4, 5
解決辦法還是很多的, 就簡單說兩個常見的。
- 用閉包解決
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
web前端開發(fā)學(xué)習(xí)Q-q-u-n: 731771211,分享學(xué)習(xí)的方法和需要注意的小細節(jié),不停更新最新的教程和學(xué)習(xí)方法(詳細的前端項目實戰(zhàn)教學(xué)視頻,PDF)
使用立即執(zhí)行函數(shù)將 i 傳入函數(shù)內(nèi)部。
這個時候值就被固定在了參數(shù) j 上面不會改變,當(dāng)下次執(zhí)行 timer 這個閉包的時候,就可以使用外部函數(shù)的變量 j ,從而達到目的。
- [推薦] 使用
let
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
執(zhí)行上下文
首先, 執(zhí)行上下文是什么呢?
簡單來說, 執(zhí)行上下文就是Javascript 的執(zhí)行環(huán)境。
當(dāng)javascript執(zhí)行一段可執(zhí)行代碼的時候時,會創(chuàng)建對應(yīng)的執(zhí)行上下文。
組成如下:
executionContextObj = {
this,
VO,
scopeChain: 作用域鏈,跟閉包相關(guān)
}
由于Javavscript是單線程的,一次只能處理一件事情,其他任務(wù)會放在指定上下文棧中排隊。
Javascript 解釋器在初始化執(zhí)行代碼時,會創(chuàng)建一個全局執(zhí)行上下文到棧中,接著隨著每次函數(shù)的調(diào)用都會創(chuàng)建并壓入一個新的執(zhí)行上下文棧。
函數(shù)執(zhí)行后,該執(zhí)行上下文被彈出。
執(zhí)行上下文建立的步驟:
- 創(chuàng)建階段
- 初始化作用域鏈
- 創(chuàng)建變量對象
- 創(chuàng)建arguments
- 掃描函數(shù)聲明
- 掃描變量聲明
- 求this
- 執(zhí)行階段
- 初始化變量和函數(shù)的引用
- 執(zhí)行代碼
this
this 是Javascript中一個很重要的概念, 也是很多初級開發(fā)者容易搞混到的一個概念。
今天我們就好好說道說道。
首先, this 是運行時才能確認的, 而非定義時確認的。
在函數(shù)執(zhí)行時,this 總是指向調(diào)用該函數(shù)的對象。
要判斷 this 的指向,其實就是判斷 this 所在的函數(shù)屬于誰。
this 的執(zhí)行,會有不同的指向情況, 大概可以分為:
- 指向調(diào)用對象
- 指向全局對象
- 用new 構(gòu)造就指向新對象
- apply/call/bind, 箭頭函數(shù)
我們一個個來看。
1. 指向調(diào)用對象
function foo() {
console.log( this.a );
}
?
var obj = {
a: 2,
foo: foo
};
?
obj.foo(); // 2
2. 指向全局對象
這種情況最容易考到, 也最容易迷惑人。
先看個簡單的例子:
var a = 2;
function foo() {
console.log( this.a );
}
foo(); // 2
沒什么疑問。
看個稍微復(fù)雜點的:
function foo() {
console.log( this.a );
}
?
function doFoo(fn) {
this.a = 4
fn();
}
?
var obj = {
a: 2,
foo: foo
};
?
var a = 3
doFoo( obj.foo ); // 4
對比:
function foo() {
this.a = 1
console.log( this.a );
}
function doFoo(fn) {
this.a = 4
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = 3
doFoo(obj.foo); // 1
發(fā)現(xiàn)不同了嗎?
你可能會問, 為什么下面的 a 不是 doFoo 的a呢?
難道是foo里面的a被優(yōu)先讀取了嗎?
打印foo和doFoo的this,就可以知道,他們的this都是指向window的。
他們的操作會修改window中的a的值。并不是優(yōu)先讀取foo中設(shè)置的a。
簡單驗證一下:
function foo() {
setTimeout(() => this.a = 1, 0)
console.log( this.a );
}
?
function doFoo(fn) {
this.a = 4
fn();
}
?
var obj = {
a: 2,
foo: foo
};
?
var a = 3
doFoo(obj.foo); // 4
setTimeout(obj.foo, 0) // 1
web前端開發(fā)學(xué)習(xí)Q-q-u-n: 731771211,分享學(xué)習(xí)的方法和需要注意的小細節(jié),不停更新最新的教程和學(xué)習(xí)方法(詳細的前端項目實戰(zhàn)教學(xué)視頻,PDF)
結(jié)果證實了我們上面的結(jié)論,并不存在什么優(yōu)先。
3. 用new構(gòu)造就指向新對象
var a = 4
function A() {
this.a = 3
this.callA = function() {
console.log(this.a)
}
}
A() // 返回undefined, A().callA 會報錯。callA被保存在window上
a = new A()
a.callA() // 3, callA在 new A 返回的對象里
4. apply/call/bind
這個大家應(yīng)該都很熟悉了。
令this指向傳遞的第一個參數(shù),如果第一個參數(shù)為null,undefined或是不傳,則指向全局變量。
var a = 3
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call(obj); // 2
foo.call(null); // 3
foo.call(undefined); // 3
foo.call(); // 3
?
var obj2 = {
a: 5,
foo
}
obj2.foo.call() // 3,不是5
?
//bind返回一個新的函數(shù)
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj =
a: 2
};
?
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5
5. 箭頭函數(shù)
箭頭函數(shù)比較特殊,它沒有自己的this。它使用封閉執(zhí)行上下文(函數(shù)或是global)的 this 值:
var x=11;
var obj={
x:22,
say: () => {
console.log(this.x);
}
}
?
obj.say(); // 11
obj.say.call({x:13}) // 11
?
x = 14
obj.say() // 14
?
//對比一下
var obj2={
x:22,
say() {
console.log(this.x);
}
}
obj2.say();// 22
obj2.say.call({x:13}) // 13
web前端開發(fā)學(xué)習(xí)Q-q-u-n: 731771211,分享學(xué)習(xí)的方法和需要注意的小細節(jié),不停更新最新的教程和學(xué)習(xí)方法(詳細的前端項目實戰(zhàn)教學(xué)視頻,PDF)
總結(jié)
以上我們系統(tǒng)的介紹了上下文, 以及與之相關(guān)的作用域, 閉包, this等相關(guān)概念。
介紹了他們的作用,使用場景以及區(qū)別和聯(lián)系。
希望能對大家有所幫助, 文中若有紕漏, 歡迎指正, 謝謝。