以及其任何超类对此上下文都是未知的_web前端入门到实战:Javascript 中的「上下文」你只需要看这一篇
正文
上下文 是Javascript 中的一個比較重要的概念, 可能很多朋友對這個概念并不是很熟悉, 那換成「作用域」 和 「閉包」呢?是不是就很親切了。
「作用域」和「閉包」 都是和「執行上下文」密切相關的兩個概念。
在解釋「執行上下文」是什么之前, 我們還是先回顧下「作用域」 和 「閉包」。
作用域
首先, 什么是作用域呢?
域, 即是范圍。
作用域,其實就是某個變量或者函數的可訪問范圍。
它控制著變量和函數的可見性和生命周期。
作用域也分為: 「全局作用域 」和 「局部作用域」。
全局作用域:
如果一個對象在任何位置都能被訪問到, 那么這個對象, 就是一個全局對象, 擁有一個全局作用域。
擁有全局作用域的對象可以分為以下幾種情況:
- 定義在最外層的變量
- 全局對象的屬性
- 任何地方隱式定義的變量(即:未定義就直接賦值的變量)。隱式定義的變量都會定義在全局作用域中。
局部作用域:
JavaScript的作用域是通過函數來定義的。
在一個函數中定義的變量, 只對此函數內部可見。
這類作用域,稱為局部作用域。
還有一個概念和作用域聯系密切, 那就是作用域鏈。
作用域鏈
作用域鏈是一個集合, 包含了一系列的對象, 它可以用來檢索上下文中出現的各類標識符(變量, 參數, 函數聲明等)。
函數在定義的時候, 會把父級的變量對象AO/VO的集合保存在內部屬性 [[scope]] 中,該集合稱為作用域鏈。
- AO : Activation Object 活動對象
- VO : Variable object 變量對象
Javascript 采用了詞法作用域(靜態作用域),函數運行在他們被定義的作用域中,而不是他們被執行的作用域。
看個簡單的例子 :
var a = 3; ? function foo () {console.log(a) } ? function bar () {var a = 6foo() } ? 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是函數自身內部的。
下面我們說一下閉包。
閉包
閉包也是面試中經常會問到的問題, 考察的形式也很靈活, 譬如:
- 描述下什么是閉包
- 寫一段閉包的代碼
- 閉包有什么用
- 給你一個閉包的例子,讓你修改, 或者看輸出
那閉包究竟是什么呢?
說白了, 閉包其實也就是函數, 一個可以訪問自由變量的函數。
自由變量: 不在函數內部聲明的變量。
很多所謂的代碼規范里都說, 不要濫用閉包, 會導致性能問題, 我當然是不太認同這種說法的, 不過這個說法被人提出來,也是有一些原因的。
畢竟,閉包里的自由變量會綁定在代碼塊上,在離開創造它的環境下依舊生效,而使用代碼塊的人可能無法察覺。
閉包里的自由變量的形式有很多,先舉個簡單例子。
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
解決辦法還是很多的, 就簡單說兩個常見的。
使用立即執行函數將 i 傳入函數內部。
這個時候值就被固定在了參數 j 上面不會改變,當下次執行 timer 這個閉包的時候,就可以使用外部函數的變量 j ,從而達到目的。
執行上下文
首先, 執行上下文是什么呢?
簡單來說, 執行上下文就是Javascript 的執行環境。
當javascript執行一段可執行代碼的時候時,會創建對應的執行上下文。
組成如下:
executionContextObj = {this,VO,scopeChain: 作用域鏈,跟閉包相關 }由于Javavscript是單線程的,一次只能處理一件事情,其他任務會放在指定上下文棧中排隊。
Javascript 解釋器在初始化執行代碼時,會創建一個全局執行上下文到棧中,接著隨著每次函數的調用都會創建并壓入一個新的執行上下文棧。
函數執行后,該執行上下文被彈出。
執行上下文建立的步驟:
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(); // 22. 指向全局對象
這種情況最容易考到, 也最容易迷惑人。
先看個簡單的例子:
var a = 2; function foo() {console.log( this.a ); } foo(); // 2沒什么疑問。
看個稍微復雜點的:
function foo() {console.log( this.a ); } ? function doFoo(fn) {this.a = 4fn(); } ? var obj = {a: 2,foo: foo }; ? var a = 3 doFoo( obj.foo ); // 4對比:
function foo() {this.a = 1console.log( this.a ); } function doFoo(fn) {this.a = 4fn(); } 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 = 4fn(); } ? 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 = 3this.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); // 55. 箭頭函數
箭頭函數比較特殊,它沒有自己的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等相關概念。
介紹了他們的作用,使用場景以及區別和聯系。
希望能對大家有所幫助, 文中若有紕漏, 歡迎指正, 謝謝。
在學習上有什么疑問隨時可以找我我,與大家分享互聯網web前端實戰操作,無論你是否有基礎,我都歡迎。點:前端技術分享
總結
以上是生活随笔為你收集整理的以及其任何超类对此上下文都是未知的_web前端入门到实战:Javascript 中的「上下文」你只需要看这一篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 巅峰赛会掉胜率吗
- 下一篇: 小新air13与14区别