[TOC]
>大佬的 github:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)
# 詞法作用域(靜態作用域)和動態作用域
JavaScript 采用的是詞法作用域,函數的作用域在函數定義的時候就決定了。
而與詞法作用域相對的是動態作用域,函數的作用域是在函數調用的時候才決定的。
~~~
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 結果是 ???
~~~
假設JavaScript采用靜態作用域,讓我們分析下執行過程:
執行 foo 函數,先從 foo 函數內部查找是否有局部變量 value,如果沒有,就根據<span style="color: red">書寫的位置</span>,查找上面一層的代碼,也就是 value 等于 1,所以結果會打印 1。
假設JavaScript采用動態作用域,讓我們分析下執行過程:
執行 foo 函數,依然是從 foo 函數內部查找是否有局部變量 value。如果沒有,就從<span style="color: red">調用函數的作用域</span>,也就是 bar 函數內部查找 value 變量,所以結果會打印 2。
前面我們已經說了,JavaScript采用的是靜態作用域,所以這個例子的結果是 1。
再看一個例子:下面兩段代碼的執行結果?
~~~js
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
~~~
~~~js
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
~~~
兩段代碼都會打印:`local scope`。
原因也很簡單,因為JavaScript采用的是詞法作用域,函數的作用域基于函數創建的位置。
引用《JavaScript權威指南》的回答就是:
JavaScript 函數的執行用到了作用域鏈,這個作用域鏈是在函數定義的時候創建的。嵌套的函數 f() 定義在這個作用域鏈里,其中的變量 scope 一定是局部變量,不管何時何地執行函數 f(),這種綁定在執行 f() 時依然有效。
# 執行上下文與執行上下文棧
~~~
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
~~~
~~~
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
~~~
為什么打印的結果會是兩個`foo2`?
這是因為 JavaScript 引擎并非一行一行地分析和執行程序,而是一段一段地分析執行。當**執行一段代碼**的時候,會進行一個“準備工作”,比如第一個例子中的**變量提升**,和第二個例子中的**函數提升**。
JavaScript 的可執行代碼(executable code)的類型有三種:全局代碼、函數代碼、eval代碼。
舉個例子,當執行到一個函數的時候,就會進行準備工作,這里的“準備工作”,讓我們用個更專業一點的說法,就叫做**執行上下文(execution context)**。
我們寫的函數多了去了,如何管理創建的那么多執行上下文呢?
所以 JavaScript 引擎創建了**執行上下文棧(Execution context stack,ECS)** 來管理執行上下文
為了模擬執行上下文棧的行為,讓我們定義執行上下文棧是一個數組:
~~~js
ECStack = [];
~~~
試想當 JavaScript 開始要解釋執行代碼的時候,最先遇到的就是全局代碼,所以初始化的時候首先就會向執行上下文棧壓入一個全局執行上下文,我們用 globalContext 表示它,并且只有當整個應用程序結束的時候,ECStack 才會被清空,所以程序結束之前, ECStack 最底部永遠有個 globalContext:
~~~js
ECStack = [
globalContext
];
~~~
現在 JavaScript 遇到下面的這段代碼了:
~~~js
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
~~~
當**執行一個函數的時候**,就會創建一個執行上下文,并且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。知道了這樣的工作原理,讓我們來看看如何處理上面這段代碼:
~~~js
// 偽代碼
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中竟然調用了fun2,還要創建fun2的執行上下文
ECStack.push(<fun2> functionContext);
// 擦,fun2還調用了fun3!
ECStack.push(<fun3> functionContext);
// fun3執行完畢
ECStack.pop();
// fun2執行完畢
ECStack.pop();
// fun1執行完畢
ECStack.pop();
// javascript接著執行下面的代碼,但是ECStack底層永遠有個globalContext
~~~
>[success]大佬寫的太好了根本找不出多余的字所以就把需要理解的地方直接搬運了
# 執行上下文(excution context)的三個屬性
當 JavaScript 代碼執行一段可執行代碼(executable code)時,會創建對應的執行上下文(execution context)。
對于每個執行上下文,都有三個重要屬性:
* 變量對象(Variable object,VO)
* 作用域鏈(Scope chain)
* this
## 變量對象
變量對象是與執行上下文相關的數據作用域,<span style="color: red">存儲了在上下文中定義的變量和函數聲明。</span>
不同執行上下文下的變量對象稍有不同
- 全局上下文中的變量對象就是全局對象,在客戶端 JavaScript 中,全局對象就是 Window 對象,預定義了一堆的函數和屬性
- 在函數上下文中,我們用**活動對象(activation object, AO)** 來表示變量對象。
活動對象和變量對象其實是一個東西,只是變量對象是規范上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象才會被激活,所以才叫 activation object ,而只有被激活的變量對象,也就是活動對象上的各種屬性才能被訪問。
<span style="color: red">活動對象是在進入函數上下文時刻被創建的</span>,它通過函數的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。
**執行過程**
執行上下文的代碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:
1. 進入執行上下文
2. 代碼執行
**1.進入執行上下文**
當進入執行上下文時,這時候還沒有執行代碼,
變量對象會包括:
1. 函數的所有形參 (如果是函數上下文)
* 由名稱和對應值組成的一個變量對象的屬性被創建
* 沒有實參,屬性值設為 undefined
2. 函數聲明
* 由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建
* 如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性
3. 變量聲明
* 由名稱和對應值(undefined)組成一個變量對象的屬性被創建;
* 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
舉個例子:
~~~js
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
~~~
在進入執行上下文后,這時候的 AO 是:
~~~js
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
~~~
**2.代碼執行**
在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值
還是上面的例子,當代碼執行完后,這時候的 AO 是:
~~~js
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
~~~
到這里變量對象的創建過程就介紹完了,讓我們簡潔的總結我們上述所說:
1. 全局上下文的變量對象初始化是全局對象
2. 函數上下文的變量對象初始化只包括 Arguments 對象
3. 在進入執行上下文時會給變量對象添加形參、函數聲明、變量聲明等初始的屬性值
4. 在代碼執行階段,會再次修改變量對象的屬性值
看懂了這篇文章或許才算是真正理解**變量提升**和**函數提升**的問題了吧
~~~
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
~~~
會打印函數,而不是 undefined 。
這是因為在進入執行上下文時(個人認為姑且叫做分析階段吧,完成賦值的過程叫執行階段),如果如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。
## 作用域鏈
查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈。
下面,讓我們以一個函數的創建和激活兩個時期來講解作用域鏈是如何創建和變化的。
**函數創建**
函數的作用域在函數定義的時候就決定了。
這是因為函數有一個內部屬性 \[\[scope\]\],當函數創建的時候,就會保存所有父變量對象到其中,你可以理解 \[\[scope\]\] 就是所有父變量對象的層級鏈,但是注意:\[\[scope\]\] 并不代表完整的作用域鏈!
舉個例子:
~~~js
function foo() {
function bar() {
...
}
}
~~~
函數創建時,各自的\[\[scope\]\]為:
~~~js
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
~~~
**函數激活**
當函數激活時,進入函數上下文,創建 VO/AO 后,就會將活動對象添加到作用鏈的前端。
這時候執行上下文的作用域鏈,我們命名為 Scope:
~~~js
Scope = [AO].concat([[Scope]]);
~~~
至此,作用域鏈創建完畢。
**完整過程**
以下面的例子為例,結合著之前講的變量對象和執行上下文棧,我們來總結一下函數執行上下文中作用域鏈和變量對象的創建過程:
~~~js
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
~~~
執行過程如下:
1.checkscope 函數被創建,保存作用域鏈到 內部屬性\[\[scope\]\]
~~~js
checkscope.[[scope]] = [
globalContext.VO
];
~~~
2.執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
~~~js
ECStack = [
globalContext,
checkscopeContext
];
~~~
3.checkscope 函數并不立刻執行,開始做準備工作,第一步:復制函數\[\[scope\]\]屬性創建作用域鏈
~~~js
checkscopeContext = {
Scope: checkscope.[[scope]],
}
~~~
4.第二步:用 arguments 創建活動對象,隨后初始化活動對象,加入形參、函數聲明、變量聲明
~~~js
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
~~~
5.第三步:將活動對象壓入 checkscope 作用域鏈頂端
~~~js
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
~~~
6.準備工作做完,開始執行函數,隨著函數的執行,修改 AO 的屬性值
~~~js
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
~~~
7.查找到 scope2 的值,返回后函數執行完畢,函數上下文從執行上下文棧中彈出
~~~js
ECStack = [
globalContext
];
~~~
> 讀完并理解了這篇后,對作用域鏈的創建過程有了更深的理解了。
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs