JS的數據類型
最新的 ECMAScript 標準定義了 8 種數據類型:
7 種原始類型:
- Boolean
- Null Undefined
- Number
- BigInt
- String
- Symbol
和Object
原型鏈
什麼是原型(_proto_)呢?原型其實可以理解爲JS中 對象與對象之間的關聯關係,具體可以這樣理解:每一個JavaScript對象(null除外)在創建的時候就會與之關聯另一個對象,這個對象就是我們所說的原型,每一個對象都會從原型"繼承"屬性。
當訪問某個對象屬性的時候,首先會從對象自身查找該屬性,如果查找不到,則繼續從該對象的原型上查找,如果還查查不到,繼續從原型的原型上查找,直到 Object.prototype
這樣就形成了一個鏈,就是原型鏈。
兩個屬性:
_proto_
這是每一個JavaScript對象(除了 null )都具有的一個屬性,叫__proto__,這個屬性會指向該對象的原型,也就是生成該對象的構造函數的prototype屬性。
比如:
const a = function(){};
const b = {};
const c = [];
a.__proto__ === Function.prototype;// true a的構造函數爲Function 引用MDN的話:每個 JavaScript 函數實際上都是一個 Function 對象。運行 (function(){}).constructor === Function // true 便可以得到這個結論
b.__proto__ === Object.prototype;//true
c.__proto__ === Array.prototype;//true
prototype
只有函數纔有prototype
屬性,該 prototype 的值正是調用該構造函數而創建的實例的原型(也就是上面提到的 person1.__proto__
)
作用域
一句話描述:
作用域是一套規則,規定了在何處 以及如何去查找變量
JavaScript 採用詞法作用域(lexical scoping),也就是靜態作用域,函數的作用域在定義的時候就決定了,而與之相對的是動態作用域,即:函數的作用域是在函數調用的時候才決定的。
執行上下文棧
JS引擎在遇到一段可執行代碼(全局代碼、函數代碼、eval代碼)時,就會創建一個執行上下文,可以理解爲JS引擎執行當前代碼的一個環境。
而執行上下文棧,就是用來管理執行上下文的,這是一種先入後出的數據結構
執行上下文的三種類型:
- 全局執行上下文
- 函數執行上下文
- eval執行上下文
執行上下文的三個重要內容:
- 變量對象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
// 嘗試分析如下代碼中執行上下文棧的變化過程
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
變量對象
變量對象是與執行上下文相關的數據作用域,存儲了在上下文中定義的變量和函數聲明。
在全局上下文中,變量對象就是全局對象。
在函數執行上下文中,我們用活動對象(activation object, AO)來表示變量對象。
注:它們其實都是同一個對象,只是處於執行上下文的不同生命週期
未進入執行階段之前,變量對象(VO)中的屬性都不能訪問!但是進入執行階段之後,變量對象(VO)轉變爲了活動對象(AO),裏面的屬性都能被訪問了,然後開始進行執行階段的操作。
執行過程
代碼的執行可以分爲兩個階段:
- 代碼分析(進入執行上下文)
- 代碼執行
在代碼分析時,VO 包含:
- 函數形參
- 函數聲明(注意函數提升)
- 由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建
- 如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性
- 變量聲明(注意變量提升,但這時候變量的值是undefined)
在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值
⚠️注意:
在進入執行上下文時,首先會處理函數聲明,其次會處理變量聲明,如果如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
// 打印結果爲 foo 函數
作用域鏈
一句話描述:
由多個執行上下文
的變量對象
構成的鏈表
就叫做作用域鏈
注意:作用域鏈是在代碼執行前,初始化執行上下文的時候,最終確定的。他是根據當前可執行上下文中的AO,以及在編譯過程中,爲函數生成的[[scope]]屬性,來最終確定的
當查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象
構成的鏈表
就叫做作用域鏈
。
作用域鏈的創建和生成流程:
在源代碼中當你定義(書寫)一個函數的時候(並未調用),js引擎也能根據你函數書寫的位置,函數嵌套的位置,給你生成一個[[scope]],作爲該函數的屬性存在(這個屬性屬於函數的)。即使函數不調用,所以說基於詞法作用域(靜態作用域)(可以理解 [[scope]]
就是所有父變量對象的層級鏈,但是注意:[[scope]]
並不代表完整的作用域鏈,因爲下面講到,還有當前被執行函數的活動對象,即AO
)。
// 理解函數的[[scope]]屬性
function foo() {
function bar() {
...
}
}
// 在函數創建後,各自的[[scope]]爲
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,// 這裏爲什麼是AO呢?
globalContext.VO
];
需要注意的是,bar中必須訪問了foo中的某個變量,纔會將foo的變量對象推入bar的作用域鏈中(這可能是V8引擎的優化)
然後進入函數執行階段,生成執行上下文,執行上下文你可以宏觀的看成一個對象,(包含vo,scope,this),此時,執行上下文裏的scope和之前屬於函數的那個[[scope]]不是同一個,執行上下文裏的scope,是在之前函數的[[scope]]的基礎上,又新增一個當前的AO對象構成的。
函數定義時候的[[scope]]和函數執行時候的scope,前者作爲函數的屬性,後者作爲函數執行上下文的屬性。
this
- this指向調用者這個關係一定要清楚
- 要知道改變this指向的幾種方式(call, bind, apply)
- 箭頭函數中this的特殊性要能講清楚
箭頭函數中的this
在函數聲明之後就被唯一確定了,不會隨着函數的調用方式和所處的環境而改變,當執行箭頭函數,初始化執行上下文的時候,js引擎會從聲明該箭頭函數的詞法作用域中確定this
的指向
閉包
閉包,在形式看是一個函數嵌套着一個函數,並且內部函數訪問了外部函數的變量。當內部函數函數執行過程中,即使外部函數的執行上下文被銷燬,但是被訪問的這部分變量仍然會常駐內存,並且內部函數可以順着自己的作用域鏈
,訪問到該變量。
MDN 對閉包的定義爲:
閉包是指那些能夠訪問自由變量的函數。
那什麼是自由變量呢?
自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。
由此,我們可以看出閉包共有兩部分組成:
閉包 = 函數 + 函數能夠訪問的自由變量
必刷題:
var data = [];
// 雖然函數內部訪問了變量i,但是i存在於全局執行上下文的變量對象中,所以其實沒有形成閉包的形式
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();//3
data[1]();//3
data[2]();//3
var data = [];
// let 創建了一個塊作用域,可以查看babel編譯後的代碼,其實是形成了一種閉包:函數嵌套函數
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();//0
data[1]();//1
data[2]();//2
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();//0
data[1]();//1
data[2]();//2
new
function Person(name) {
this.name = name
}
const bob = new Person('bob')
// new調用構造函數的過程
1. 首先新建一個對象
2. 然後將對象的原型 bob.__proto__ 指向Person.prototype
3. 然後 Person.apply(obj)
4. 返回這個對象
JS創建對象的幾種方式
- Object 構造函數
- 對象字面量
- 工廠函數
- 構造函數模式
- 原型模式()
- 組合模式
JS繼承的幾種方式
可以列舉如下的幾種方式 手寫代碼並說出各自的優缺點
- 原型鏈繼承
- 借用構造函數繼承(經典繼承)
- 組合繼承(原型鏈繼承和經典繼承雙劍合璧)
- 原型式繼承(Object.create)
- 寄生組合式繼承(重要)
變量提升
Event Loop
JS中的任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務會在調用棧
中按照順序
等待主線程依次執行,異步任務會在異步任務有了結果後,將註冊的回調函數放入任務隊列
中等待主線程空閒的時候(調用棧被清空),被讀取到棧內等待主線程的執行。這個過程是不斷循環往復的,就形成了一種事件循環
主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done)(注意:爲DOM註冊的事件回調,在觸發時,也是先要被推入任務隊列,等等主線程空閒的時候讀取 執行)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數(這裏其實還分爲微任務隊列和宏任務隊列,下面會有提到)
任務隊列
是一種先進先出的數據結構,排在前面的事件,優先被主線程讀取。
- 微任務隊列
- 宏任務隊列
引申點:
- 微任務:Promise MutationObserver
- 宏任務: setTimeout、setInterval、setImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)、I/O、UI Rendering
執行棧在執行完同步任務後,查看執行棧是否爲空,如果執行棧爲空,就會去檢查微任務(microTask)隊列是否爲空,如果爲空的話,就執行Task(宏任務),否則就一次性執行完所有微任務。
每次單個宏任務執行完畢後,檢查微任務(microTask)隊列是否爲空,如果不爲空的話,會按照先入先出的規則全部執行完微任務(microTask)後,設置微任務(microTask)隊列爲null,然後再執行宏任務,如此循環。
對於全部執行完微任務的潛在風險:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
// 1 2 3 4
垃圾回收
兩種方式(實現思路以及各自的優缺點)
- 引用計數垃圾收集
- 標記清除算法
Map & Set
參考資料:
- https://github.com/mqyqingfeng/Blog/issues/6