#背景 本文是 「2019年,看了這一份, 不再怕前端面試了」中的一部分:javascript
參考了以前寫過的博客
和額外的資料
, 分享給你們, 但願能給你們帶來一些啓發和幫助
。前端
如需轉載,請聯繫做者得到許可。java
#正文面試
上下文
是Javascript 中的一個比較重要的概念, 可能不少朋友對這個概念並非很熟悉, 那換成「做用域
」 和 「閉包
」呢?是否是就很親切了。segmentfault
「做用域」
和「閉包」
都是和「執行上下文」
密切相關的兩個概念。bash
在解釋「執行上下文」是什麼以前, 咱們仍是先回顧下「做用域」 和 「閉包」。微信
首先, 什麼是做用域呢?閉包
域, 便是範圍
。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 , 感興趣的朋友能夠參考: blog.nixiaolei.com/2019/06/10/…
下面咱們說一下閉包。
閉包也是面試中常常會問到的問題, 考察的形式也很靈活, 譬如:
那閉包到底是什麼呢?
說白了, 閉包其實也就是函數
, 一個能夠訪問自由變量
的函數。
自由變量
: 不在函數內部聲明的變量。
不少所謂的代碼規範裏都說, 不要濫用閉包, 會致使性能問題, 我固然是不太認同這種說法的, 不過這個說法被人提出來,也是有一些緣由的。
畢竟,閉包裏的自由變量會綁定
在代碼塊上,在離開創造它的環境下依舊生效,而使用代碼塊的人可能沒法察覺。
閉包裏的自由變量的形式有不少,先舉個簡單例子。
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進階 」,瞭解最新動態。
也能夠聯繫我加入微信羣,羣裏有諸多大佬坐鎮, 能夠一塊兒探討技術, 一塊兒摸魚。一塊兒學習成長!