web前端入門到實戰(zhàn):Javascript 中的「上下文」你只需要看這一篇

正文

上下文 是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

解決辦法還是很多的, 就簡單說兩個常見的。

  1. 用閉包解決
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 ,從而達到目的。

  1. [推薦] 使用 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í)行上下文建立的步驟:

  1. 創(chuàng)建階段
  2. 初始化作用域鏈
  3. 創(chuàng)建變量對象
  4. 創(chuàng)建arguments
  5. 掃描函數(shù)聲明
  6. 掃描變量聲明
  7. 求this
  8. 執(zhí)行階段
  9. 初始化變量和函數(shù)的引用
  10. 執(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 不是 doFooa呢?

難道是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)系。

希望能對大家有所幫助, 文中若有紕漏, 歡迎指正, 謝謝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容