本文是 「2019年,看了這一份, 不再怕前端面試了」中的一部分:javascript
參考了以前寫過的博客
和額外的資料
, 分享給你們, 但願能給你們帶來一些啓發和幫助
。前端
如需轉載,請聯繫做者得到許可。java
上下文
是Javascript 中的一個比較重要的概念, 可能不少朋友對這個概念並非很熟悉, 那換成「做用域
」 和 「閉包
」呢?是否是就很親切了。面試
「做用域」
和「閉包」
都是和「執行上下文」
密切相關的兩個概念。segmentfault
在解釋「執行上下文」是什麼以前, 咱們仍是先回顧下「做用域」 和 「閉包」。微信
首先, 什麼是做用域呢?閉包
域, 便是範圍
。app
做用域,其實就是某個變量或者函數的可訪問範圍
。函數
它控制着變量和函數的可見性
和生命週期
。性能
做用域也分爲: 「全局做用域
」和 「局部做用域
」。
若是一個對象在任何位置都能被訪問到, 那麼這個對象, 就是一個全局對象, 擁有一個全局做用域。
擁有全局做用域的對象能夠分爲如下幾種狀況:
JavaScript的做用域是經過函數
來定義的。
在一個函數中定義的變量, 只對此函數內部可見
。
這類做用域,稱爲局部做用域。
還有一個概念和做用域聯繫密切, 那就是做用域鏈
。
做用域鏈是一個集合
, 包含了一系列的對象, 它能夠用來檢索
上下文中出現的各種標識符
(變量, 參數, 函數聲明等)。
函數在定義的時候, 會把父級的變量對象AO/VO的集合保存在內部屬性 [[scope]]
中,該集合稱爲做用域鏈。
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
解決辦法仍是不少的, 就簡單說兩個常見的。
for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i) }
使用當即執行函數
將 i 傳入函數內部。
這個時候值就被固定在了參數 j 上面不會改變,當下次執行 timer 這個閉包的時候,就可使用外部函數的變量 j ,從而達到目的。
let
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
const , let 的原理和相關細節能夠參考個人另外一篇:
解釋完這兩個概念,就回到咱們的主題, 上下文
。
首先, 執行上下文是什麼呢?
簡單來講, 執行上下文就是Javascript 的執行環境
。
當javascript執行一段可執行
代碼的時候時,會建立
對應的執行上下文
。
組成以下:
executionContextObj = { this, VO, scopeChain: 做用域鏈,跟閉包相關 }
因爲Javavscript是單線程
的,一次只能處理一件事情,其餘任務會放在指定上下文棧
中排隊。
Javascript 解釋器在初始化執行代碼時,會建立一個全局執行上下文到棧中,接着隨着每次函數的調用都會建立並壓入一個新的執行上下文棧
。
函數執行後,該執行上下文被彈出。
執行上下文創建的步驟:
this 是Javascript中一個很重要的概念, 也是不少初級開發者容易搞混到的一個概念。
今天咱們就好好說道說道。
首先, this 是運行時
才能確認的, 而非定義時
確認的。
在函數執行時,this 老是指向調用該函數
的對象。
要判斷 this 的指向,其實就是判斷 this 所在的函數屬於誰
。
this 的執行,會有不一樣的指向狀況, 大概能夠分爲:
咱們一個個來看。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 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
不是 doFoo
的a
呢?
難道是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
結果證明了咱們上面的結論,並不存在什麼優先。
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 返回的對象裏
這個你們應該都很熟悉了。
令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
箭頭函數比較特殊,它沒有本身的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進階 」,瞭解最新動態。
也能夠聯繫我加入微信羣,羣裏有諸多大佬坐鎮, 能夠一塊兒探討技術, 一塊兒摸魚。一塊兒學習成長!