一文搞清 Javascript 中的「上下文」

javavs.jpg

背景

本文是 「2019年,看了這一份, 不再怕前端面試了」中的一部分:javascript

image.png

參考了以前寫過的博客和額外的資料, 分享給你們, 但願能給你們帶來一些啓發和幫助前端

如需轉載,請聯繫做者得到許可。java

正文

上下文 是Javascript 中的一個比較重要的概念, 可能不少朋友對這個概念並非很熟悉, 那換成「做用域」 和 「閉包」呢?是否是就很親切了。面試

「做用域」「閉包」 都是和「執行上下文」密切相關的兩個概念。segmentfault

在解釋「執行上下文」是什麼以前, 咱們仍是先回顧下「做用域」 和 「閉包」。微信

做用域

首先, 什麼是做用域呢?閉包

域, 便是範圍app

做用域,其實就是某個變量或者函數的可訪問範圍函數

它控制着變量和函數的可見性生命週期性能

做用域也分爲: 「全局做用域 」和 「局部做用域」。

全局做用域:

若是一個對象在任何位置都能被訪問到, 那麼這個對象, 就是一個全局對象, 擁有一個全局做用域。

擁有全局做用域的對象能夠分爲如下幾種狀況:

  • 定義在最外層的變量
  • 全局對象的屬性
  • 任何地方隱式定義的變量(即:未定義就直接賦值的變量)。隱式定義的變量都會定義在全局做用域中。

局部做用域:

JavaScript的做用域是經過函數來定義的。

在一個函數中定義的變量, 只對此函數內部可見

這類做用域,稱爲局部做用域。

還有一個概念和做用域聯繫密切, 那就是做用域鏈

做用域鏈

做用域鏈是一個集合, 包含了一系列的對象, 它能夠用來檢索上下文中出現的各種標識符(變量, 參數, 函數聲明等)。

函數在定義的時候, 會把父級的變量對象AO/VO的集合保存在內部屬性 [[scope]] 中,該集合稱爲做用域鏈。

  • AO : Activation Object 活動對象
  • VO : Variable object 變量對象

Javascript 採用了詞法做用域(靜態做用域),函數運行在他們被定義的做用域中,而不是他們被執行的做用域。

看個簡單的例子 :

var a = 3;
​
function foo () {
  console.log(a)
}
​
function bar () {
  var a = 6
  foo()
}
​
bar()

若是js採用動態做用域,打印出來的應該是6而不是3.

這個例子說明了javasript是靜態做用域

此函數做用域鏈的僞代碼:

function bar() {
    function foo() {
       // ...
    }
}
​
bar.[[scope]] = [
  globalContext.VO
];
​
foo.[[scope]] = [
    barContext.AO,
    globalContext.VO
];

函數在運行激活的時候,會先複製 [[scope]] 屬性建立做用域鏈,而後建立變量對象VO,而後將其加入到做用域鏈。

executionContextObj: {
   VO: {},
   scopeChain: [VO, [[scope]]]
}

總的來講, VO要比AO的範圍大不少, VO是負責把各個調用的函數串聯起來的。
VO是外部的, 而AO是函數自身內部的。

與AO, VO 密切相關的概念還有GO, EC , 感興趣的朋友能夠參考:
https://blog.nixiaolei.com/20...

下面咱們說一下閉包。

閉包

閉包也是面試中常常會問到的問題, 考察的形式也很靈活, 譬如:

  • 描述下什麼是閉包
  • 寫一段閉包的代碼
  • 閉包有什麼用
  • 給你一個閉包的例子,讓你修改, 或者看輸出

那閉包到底是什麼呢?

說白了, 閉包其實也就是函數, 一個能夠訪問自由變量的函數。

自由變量: 不在函數內部聲明的變量。

不少所謂的代碼規範裏都說, 不要濫用閉包, 會致使性能問題, 我固然是不太認同這種說法的, 不過這個說法被人提出來,也是有一些緣由的。

畢竟,閉包裏的自由變量會綁定在代碼塊上,在離開創造它的環境下依舊生效,而使用代碼塊的人可能沒法察覺。

閉包裏的自由變量的形式有不少,先舉個簡單例子。

function add(p1){
   return function(p2){
     return p1 + p2;
  }
}
​
var a = add(1);
var b = add(2);
​
a(1) //2
b(1) // 3

在上面的例子裏,a 和 b這兩個函數,代碼塊是相同的,但如果執行a(1)和b(1)的結果倒是不一樣的,緣由在於這二者所綁定的自由變量是不一樣的,這裏的自由變量其實就是函數體裏的 p1 。

自由變量的引入,能夠起到和OOP裏的封裝一樣做用,咱們能夠在一層函數裏封裝一些不被外界知曉的自由變量,從而達到相同的效果, 不少模塊的封裝, 也是利用了這個特性。

而後說一下我遇到的真實案例, 是去年面試騰訊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)
}

使用當即執行函數將 i 傳入函數內部。

這個時候值就被固定在了參數 j 上面不會改變,當下次執行 timer 這個閉包的時候,就可使用外部函數的變量 j ,從而達到目的。

  1. [推薦] 使用 let
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
     console.log(i)
  }, i * 1000)
}

const , let 的原理和相關細節能夠參考個人另外一篇:

[第13期] 掌握前端面試基礎系列一:ES6

解釋完這兩個概念,就回到咱們的主題, 上下文

執行上下文

首先, 執行上下文是什麼呢?

簡單來講, 執行上下文就是Javascript 的執行環境

當javascript執行一段可執行代碼的時候時,會建立對應的執行上下文

組成以下:

executionContextObj = {
  this,
  VO,
  scopeChain: 做用域鏈,跟閉包相關
}

因爲Javavscript是單線程的,一次只能處理一件事情,其餘任務會放在指定上下文中排隊。

Javascript 解釋器在初始化執行代碼時,會建立一個全局執行上下文到棧中,接着隨着每次函數的調用都會建立並壓入一個新的執行上下文棧

函數執行後,該執行上下文被彈出。

執行上下文創建的步驟:

  1. 建立階段
  2. 初始化做用域鏈
  3. 建立變量對象
  4. 建立arguments
  5. 掃描函數聲明
  6. 掃描變量聲明
  7. 求this
  8. 執行階段
  9. 初始化變量和函數的引用
  10. 執行代碼

this

this 是Javascript中一個很重要的概念, 也是不少初級開發者容易搞混到的一個概念。

今天咱們就好好說道說道。

首先, this 是運行時才能確認的, 而非定義時確認的。

在函數執行時,this 老是指向調用該函數的對象。

要判斷 this 的指向,其實就是判斷 this 所在的函數屬於誰

this 的執行,會有不一樣的指向狀況, 大概能夠分爲:

  • 指向調用對象
  • 指向全局對象
  • 用new 構造就指向新對象
  • apply/call/bind, 箭頭函數

咱們一個個來看。

1. 指向調用對象

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

沒什麼疑問。

看個稍微複雜點的:

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

發現不一樣了嗎?

你可能會問, 爲何下面的 a 不是 doFooa呢?

難道是foo裏面的a被優先讀取了嗎?

打印foo和doFoo的this,就能夠知道,他們的this都是指向window的。

他們的操做會修改window中的a的值。並非優先讀取foo中設置的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

結果證明了咱們上面的結論,並不存在什麼優先。

3. 用new構造就指向新對象

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

這個你們應該都很熟悉了。

令this指向傳遞的第一個參數,若是第一個參數爲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返回一個新的函數
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. 箭頭函數

箭頭函數比較特殊,它沒有本身的this。它使用封閉執行上下文(函數或是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

總結

以上咱們系統的介紹了上下文, 以及與之相關的做用域閉包this等相關概念。

介紹了他們的做用,使用場景以及區別和聯繫。

但願能對你們有所幫助, 文中如有紕漏, 歡迎指正, 謝謝。

最後

若是以爲內容有幫助能夠關注下個人公衆號 「 前端e進階 」,瞭解最新動態。

也能夠聯繫我加入微信羣,羣裏有諸多大佬坐鎮, 能夠一塊兒探討技術, 一塊兒摸魚。一塊兒學習成長!

clipboard.png

相關文章
相關標籤/搜索