【面試】JS基礎

JS的數據類型

最新的 ECMAScript 標準定義了 8 種數據類型:

7 種原始類型:

  1. Boolean
  2. Null Undefined
  3. Number
  4. BigInt
  5. String
  6. 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

JavaScript中的this

  1. this指向調用者這個關係一定要清楚
  2. 要知道改變this指向的幾種方式(call, bind, apply)
  3. 箭頭函數中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

MDN對微任務和宏任務的講解

垃圾回收

兩種方式(實現思路以及各自的優缺點)

  • 引用計數垃圾收集
  • 標記清除算法

MDN

Map & Set

參考資料:

  • https://github.com/mqyqingfeng/Blog/issues/6
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章