👇 內容速覽 👇
- 實現ES5繼承的4種方法
- 原型和原型鏈
- 作用域和作用域鏈
- Event Loop
- 執行上下文
- 閉包的理解和分析
ES5繼承
題目:ES5中常用繼承方法。
方法一:綁定構造函數
缺點:不能繼承父類原型方法/屬性
function Animal(){
this.species = '動物'
}
function Cat(){
// 執行父類的構造方法, 上下文爲實例對象
Animal.apply(this, arguments)
}
/**
* 測試代碼
*/
var cat = new Cat()
console.log(cat.species) // output: 動物
方法二:原型鏈繼承
缺點:無法向父類構造函數中傳遞參數;子類原型鏈上定義的方法有先後順序問題。
注意:js中交換原型鏈,均需要修復prototype.constructor
指向問題。
function Animal(species){
this.species = species
}
Animal.prototype.func = function(){
console.log('Animal')
}
function Cat(){}
/**
* func方法是無效的, 因爲後面原型鏈被重新指向了Animal實例
*/
Cat.prototype.func = function() {
console.log('Cat')
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat // 修復: 將Cat.prototype.constructor重新指向本身
/**
* 測試代碼
*/
var cat = new Cat()
cat.func() // output: Animal
console.log(cat.species) // undefined
方法3:組合繼承
結合綁定構造函數和原型鏈繼承2種方式,缺點是:調用了2次父類的構造函數。
function Animal(species){
this.species = species
}
Animal.prototype.func = function(){
console.log('Animal')
}
function Cat(){
Animal.apply(this, arguments)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
/**
* 測試代碼
*/
var cat = new Cat('cat')
cat.func() // output: Animal
console.log(cat.species) // output: cat
方法4:寄生組合繼承
改進了組合繼承的缺點,只需要調用1次父類的構造函數。它是引用類型最理想的繼承範式。(引自:《JavaScript高級程序設計》)
/**
* 寄生組合繼承的核心代碼
* @param {Function} sub 子類
* @param {Function} parent 父類
*/
function inheritPrototype(sub, parent) {
// 拿到父類的原型
var prototype = Object(parent.prototype)
// 改變constructor指向
prototype.constructor = sub
// 父類原型賦給子類
sub.prototype = prototype
}
function Animal(species){
this.species = species
}
Animal.prototype.func = function(){
console.log('Animal')
}
function Cat(){
Animal.apply(this, arguments) // 只調用了1次構造函數
}
inheritPrototype(Cat, Animal)
/**
* 測試代碼
*/
var cat = new Cat('cat')
cat.func() // output: Animal
console.log(cat.species) // output: cat
原型和原型鏈
- 所有的引用類型(數組、對象、函數),都有一個
__proto__
屬性,屬性值是一個普通的對象 - 所有的函數,都有一個prototype屬性,屬性值也是一個普通的對象
- 所有的引用類型(數組、對象、函數),
__proto__
屬性值指向它的構造函數的prototype屬性值
注:ES6的箭頭函數沒有prototype
屬性,但是有__proto__
屬性。
const obj = {};
// 引用類型的 __proto__ 屬性值指向它的構造函數的 prototype 屬性值
console.log(obj.__proto__ === Object.prototype); // output: true
原型
題目:如何JS中的原型?
// 構造函數
function Foo(name, age) {
this.name = name
}
Foo.prototype.alertName = function () {
alert(this.name)
}
// 創建示例
var f = new Foo('zhangsan')
f.printName = function () {
console.log(this.name)
}
// 測試
f.printName()
f.alertName()
但是執行alertName
時發生了什麼?這裏再記住一個重點 當試圖得到一個對象的某個屬性時,如果這個對象本身沒有這個屬性,那麼會去它的__proto__
(即它的構造函數的prototype
)中尋找,因此f.alertName
就會找到Foo.prototype.alertName
。
原型鏈
題目:如何JS中的原型鏈?
以上一題爲基礎,如果調用f.toString()
。
-
f
試圖從__proto__
中尋找(即Foo.prototype
),還是沒找到toString()
方法。 - 繼續向上找,從
f.__proto__.__proto__
中尋找(即Foo.prototype.__proto__
中)。因爲Foo.prototype
就是一個普通對象,因此Foo.prototype.__proto__ = Object.prototype
- 最終對應到了
Object.prototype.toString
這是對深度遍歷的過程,尋找的依據就是一個鏈式結構,所以叫做“原型鏈”。
作用域和作用域鏈
題目:如何理解 JS 的作用域和作用域鏈。
①作用域
ES5有”全局作用域“和”函數作用域“。ES6的let
和const
使得JS用了”塊級作用域“。
爲了解決ES5的全局衝突,一般都是閉包編寫:(function(){ ... })()
。將變量封裝到函數作用域。
②作用域鏈
當前作用域沒有找到定義,繼續向父級作用域尋找,直至全局作用域。這種層級關係,就是作用域鏈。
Event Loop
單線程
題目:講解下面代碼的執行過程和結果。
var a = true;
setTimeout(function(){
a = false;
}, 100)
while(a){
console.log('while執行了')
}
這段代碼會一直執行並且輸出"while..."。JS是單線程的,先跑執行棧裏的同步任務,然後再跑任務隊列的異步任務。
執行棧和任務隊列
題目:說一下JS的Event Loop。
簡單總結如下:
- JS是單線程的,其上面的所有任務都是在兩個地方執行:執行棧和任務隊列。前者是存放同步任務;後者是異步任務有結果後,就在其中放入一個事件。
- 當執行棧的任務都執行完了(棧空),js會讀取任務隊列,並將可以執行的任務從任務隊列丟到執行棧中執行。
- 這個過程是循環進行,所以稱作
Loop
。
執行上下文
題目:解釋下“全局執行上下文“和“函數執行上下文”。
①全局執行上下文
解析JS時候,創建一個 全局執行上下文 環境。把代碼中即將執行的(內部函數的不算,因爲你不知道函數何時執行)變量、函數聲明都拿出來。未賦值的變量就是undefined
。
下面這段代碼輸出:undefined
;而不是拋出Error
。因爲在解析JS的時候,變量a已經存入了全局執行上下文中了。
console.log(a);
var a = 1;
②函數執行上下文
和全局執行上下文差不多,但是多了this
和arguments
和參數。
在JS中,this
是關鍵字,它作爲內置變量,其值是在執行的時候確定(不是定義的時候確定)。
閉包的理解和分析
題目:解釋下js的閉包
直接上MDN的解釋:閉包是函數和聲明該函數的詞法環境的組合。
而在JavaScript中,函數是被作爲一級對象使用的,它既可以本當作值返回,還可以當作參數傳遞。理解了:“Js中的函數運行在它們被定義的作用域,而不是它們被執行的作用域”(摘自《JavaScript語言精粹》) 這句話即可。
題目:閉包優缺點
閉包封住了變量作用域,有效地防止了全局污染;但同時,它也存在內存泄漏的風險:
- 在瀏覽器端可以通過強制刷新解決,對用戶體驗影響不大
- 在服務端,由於node的內存限制和累積效應,可能會造成進程退出甚至服務器沓機
解決方法是顯式對外暴露一個接口,專門用以清理變量:
function mockData() {
const mem = {}
return {
clear: () => mem = null, // 顯式暴露清理接口
get: (page) => {
if(page in mem) {
return mem[page]
}
mem[page] = Math.random()
}
}
}
更多系列教程
《前端知識體系》
- JavaScript基礎知識梳理(上)
- JavaScript基礎知識梳理(下)
- 談談promise/async/await的執行順序與V8引擎的BUG
- 前端面試中常考的源碼實現
- Flex上手與實戰
- ......
《設計模式手冊》
《Webpack4漸進式教程》