下面羅列的js語言中那些讓人抓狂混淆的概念,你遇到過幾個?node
建議收藏此文,每當要面試的時候提早拿出來溫習溫習鞏固鞏固,屢次下來,這些概念相信會永遠印在你的腦海中~~git
每一小節都會有一道對應的練習題供參考,若是你作出的答案和題目的答案同樣,那麼這一小節的內容相信你已經掌握了github
const months = ['Jan', 'March', 'April', 'June'];
const res1 = months.splice(1, 0, 'Feb');
const res2 = months.splice(4, 1, 'May');
const res3 = months.splice(3)
console.log(months, res1, res2, res3)
const fruits = ['apple', 'orange', 'cherry']
const res4 = fruits.slice(0, 2)
console.log(fruits, res4)
// 輸出結果應該是:
// ['Jan', 'Feb', 'March'] [] ['June'] ['April', 'May']
// ['apple', 'orange', 'cherry'] ['apple', 'orange']
複製代碼
這兩個方法名字何其類似,想區分開來蠻費勁的,最後可以記住的就是口訣是:放個p,就是不同
,話糙理不糙~接下去咱們好好總結一下兩者的區別面試
兩者共同點都是從數組中提取指定的一段範圍內的數據,可是有三個不一樣點:chrome
months.splice(1, 0, 'Feb')
"use strict"; // 這個去掉和沒去掉有什麼區別?
function fn(type, name) {
console.log(`I am ${name}, belongs to ${type}, what is this? Answer is: `, this);
}
const obj = {
type: '電器',
name: '吹風機'
}
fn('電器', '吹風機')
fn.call(obj, '水果', '蘋果')
fn.apply(obj, ['水果', '蘋果'])
fn.call(null, '水果', '蘋果')
fn.call('水果', '水果', '蘋果')
const bindFn = fn.bind(obj, '水果')
bindFn('蘋果')
// 輸出結果應該是:
// I am 吹風機, belongs to 電器, what is this? Answer is: undefined
// I am 蘋果, belongs to 水果, what is this? Answer is: {type: "電器", name: "吹風機"}
// I am 蘋果, belongs to 水果, what is this? Answer is: {type: "電器", name: "吹風機"}
// I am 蘋果, belongs to 水果, what is this? Answer is: null
// I am 蘋果, belongs to 水果, what is this? Answer is: 水果
// I am 蘋果, belongs to 水果, what is this? Answer is: {type: "電器", name: "吹風機"}
複製代碼
call和apply是一對孿生兄弟,而bind是這對兄弟的」經紀人「。call和apply說是孿生兄弟一點都不假,由於他們實現的功能如出一轍,都是爲了被調用的函數指定好執行的上下文(也就是this
),惟一的區別是傳參給被調用的函數的參數形式,call使用的是以數量取勝大法,有多少個參數就傳多少個參數,而apply則換了套路,以簡潔取勝,由於人家將全部參數都打包到一個數組裏了。express
注意:api
call
和apply
的this值不傳的時候,在嚴格模式下是被解釋爲undefined
,在非嚴格模式下是全局對象(瀏覽器環境下是window,nodejs環境下是global)。若是給傳的是一些原始值(如:'string'、11之類的值),那麼這些值都會被轉變爲對象,轉變規則是利用對應的構造函數進行new
,這條規則在待會解釋this指向頗有用的!!好比:數組
function test(){console.log(this)}
test.call('test') // 打印結果是: String {"test"}, 等價於 new String("test")
複製代碼
call
特別適用於那些本身自己沒有這個方法,可是又想用這個方法去完成一些東西,好比:Array.prototype.slice.call(arguments)
、再好比Object.prototype.toString.call([])
,都是利用call方法的特性,將slice
方法和toString
內部實現中的this
指針篡改達到目的的。*promise
那麼說bind是」經紀人「,又是爲啥呢?其實從實現的話也就是apply
的經紀人,由於它利用閉包原理,將apply包裹起來,對外輸出一個高階函數,最簡單的實現版本是:瀏覽器
Function.prototype.bind = function (thisArgs) {
var fn = this;
var restArguments = Array.prototype.slice.call(arguments, 1)
return function () {
return fn.apply(thisArgs, restArguments.concat(Array.prototype.slice.call(arguments)));
};
}
複製代碼
三者的一個區別總結以下:(取自call&apply&bind的學習)
call和apply是能夠相互替換的,這僅僅是取決於你傳遞參數使用數組方便仍是逗號分隔的參數列表方便。
call和apply很容易混淆掉,有時候會忘掉apply是使用數組仍是列表,那麼有一個簡單的記住辦法那就是apply的a和array的a是一致的,這樣就記住了吧?
bind稍微不一樣,由於它返回的是一個函數,能夠在任何你想要執行的時候執行,而前面兩個函數都是立馬執行的。所以整體來講bind的靈活性會比call和apply更好,適用的場景更多
// 使用"use strict"和不使用的區別?
const PersonA = {
firstName: 'Lin',
lastName: 'Xiaowu',
displayName: function() {
console.log(`My name is ${this.firstName}-${this.lastName}`)
}
}
function concat(firstName, lastName, callback) {
callback(`${firstName}-${lastName}`)
}
const PersonB = function(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
this.displayName = function() {
concat(this.firstName, this.lastName, function(fullName) {
console.log(`My name is ${fullName}, this is equal to window? ${this === window}`)
})
}
this.arrow = function() {
const arr = () => console.log(this)
return arr()
}
}
const pureArrow = () => { return this }
PersonA.displayName();
const personA = PersonA.displayName
personA()
const personB = new PersonB('Lin', 'Xiaowu')
personB.displayName()
personB.arrow()
const personC = { firstName: "dou", lastName: "mi" }
personB.displayName.call(personC)
const personD = personB.arrow.bind(personC)
personD()
const personE = personB.arrow.bind(pureArrow())
personE()
// 打印結果以下:
// My name is Lin-Xiaowu
// My name is undefined-undefined
// My name is Lin-Xiaowu, this is equal to window? true
// PersonB {firstName: "Lin", lastName: "Xiaowu", displayName: ƒ, arrow: ƒ}
// My name is dou-mi, this is equal to window? true
// {firstName: "dou", lastName: "mi"}
// Window {parent: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
複製代碼
this
這個小妖精,曾經迷惑了多少人。這裏的講解主要是總結,也不須要生硬地背下來,畢竟是有規律可循的。如下解析借鑑於這篇文章,略有擴展:this 的值究竟是什麼?一次說清楚
文中的做者結合call
,仍是把this的一些指向說的蠻清楚的(在此基礎上繼續擴展),總結出如下兩種狀況(涵蓋了95%以上的場景):
call
形式的調用,具體轉換規則以下:new
出來的實例this
this
保持與其外圍的上下文環境的this
一致。根據以上規則,咱們對上面的練習題進行解析以下:
// 根據規則,等價於這麼調用:PersonA.displayName.call(PersonA),因此此時this是等於PersonA,所以打印出:My name is Lin-Xiaowu
PersonA.displayName();
const personA = PersonA.displayName
// 根據規則,等價於這麼調用:PersonA.call(undefined),加上上一節提到的call的this參數原則,所以打印出:My name is undefined-undefined
personA()
const personB = new PersonB('Lin', 'Xiaowu')
// 這種new構造函數的形式也是符合咱們上面提到的規則,因此等價於調用personB.displayName.call(personB),所以打印:My name is Lin-Xiaowu,
// 接着在displayName裏面又有一個function,此時使用規則1的第一條,因此這個時候的this是等於window
personB.displayName()
// 這個是同時使用兩條規則進行判斷,先使用規則1的第二條,等價於調用:personB.arrow.call(personB),
// 因而arrow內部的this指向了personB,而後再用規則2的第一條,箭頭函數的this隨上下文,所以打印的this即是personB
personB.arrow()
const personC = { firstName: "dou", lastName: "mi" }
// 根據規則,並結合上面的一些分析,很容易得出答案:My name is dou-mi, this is equal to window? true
personB.displayName.call(personC)
const personD = personB.arrow.bind(personC)
// 根據規則,this指向了personC,再結合上面的分析,得出的打印結果是personC:{firstName: "dou", lastName: "mi"}
personD()
const personE = personB.arrow.bind(pureArrow())
// 這個搞懂pureArrow的this指針便可獲得答案,由於符合規則1的第一條,因此this指向了window,因而打印出了window對象
personE()
複製代碼
這麼講解下來,this的指向懂了嗎?
__proto__
和prototype
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype = {
y: 400,
getX: function () {
console.log(this.x);
},
getY: function () {
console.log(this.y);
},
sum: function () {
console.log(this.x + this.y);
}
};
Fn.prototype.getX = function () {
console.log(this.x);
};
Fn.prototype.getY = function () {
console.log(this.y);
};
var f1 = new Fn;
var f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
f1.sum();
Fn.prototype.sum();
// 打印結果以下:
false
// true
// true
// false
// false
// ƒ Object() { [native code] }
// ƒ Object() { [native code] }
// 100
// undefined
// 200
// 400
// 300
// NaN
複製代碼
這兩個概念牽扯到了原型和原型鏈,兩者的區別以及須要注意的東西都體如今了下圖,但願你們對原型鏈有這麼一張圖的印象(也就是每當出現這種題目的話,腦子可以浮現對應的關係圖),再結合下面總結的5條規律,深化印象,從而真正掌握住:
特此總結的一些規律貼在這裏(下面提到的每一條規律都用特定的顏色在上圖中標註一一對應):
一、只要是構造函數(構造函數能夠是原生的也能夠是自定義的,看上圖就知道了)都會有prototype
屬性,而且都是指向其原型對象
二、構造函數實例化後的實例都有__proto__
屬性,並指向其構造函數的原型對象
三、構造函數都有__proto__
屬性,統一指向了原生Function的原型對象
四、原型對象都有會一個constructor
的屬性,而且都是指向其構造函數
五、原型對象都有會一個__proto__
的屬性,而且都是指向Object的原型對象
六、有一種很特殊的狀況,那就是給原型對象從新賦值的時候,須要特別考慮,這一點沒有在上圖中體現,可是在練習題裏體現了(Fn.prototype = {})
typeof Math.LN2 === 'number';
typeof Infinity === 'number';
typeof NaN === 'number';
typeof 42n === 'bigint';
typeof undefined === 'undefined';
typeof class C {} === 'function';
function C(){} // defining a constructor
function D(){} // defining another constructor
var o = new C();
o instanceof C; // true, because: Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false, because D.prototype is nowhere in o's prototype chain o instanceof Object; // true, 原型鏈查找 C.prototype instanceof Object // true C.prototype = {}; var o2 = new C(); o2 instanceof C; // true o instanceof C; // false, 參考前面的原型鏈一節 D.prototype = new C(); // use inheritance var o3 = new D(); o3 instanceof D; // true o3 instanceof C; // true 複製代碼
typeof用於基本數據類型的類型判斷,返回值都爲小寫的字符串。若是是對象,除了function類型會返回「function」, 其餘對象統一返回「object」。所以這也是typeof使用的一個缺陷,沒法正確地告知具體的object。
typeof返回的結果整理以下:
類型 | 結果 | 備註 |
---|---|---|
Undefined | "undefined" | |
Null | "object" | 歷史緣由致使的結果 |
Boolean | "boolean" | |
Number | "number" | |
BigInt | "bigint" | |
String | "string" | |
Symbol | "symbol" | |
Host object(宿主對象,概念參考宿主對象定義) | 取決於實現 | 由編譯器各自實現的字符串,但不是"undefined","number","boolean","number","string"。 |
Function object | "function" | 諸如 function a() {} 之類的 |
Any other object | "object" |
所以基於typeof
的使用,咱們建議在用 typeof 來判斷變量類型的時候,咱們須要注意,最好是用 typeof 來判斷基本數據類型(包括symbol),避免對 null 的判斷。
instanceof
是二元操做符,用來判斷變量是否爲某個對象的實例,返回值爲true
或false
。操做符左邊爲對象,右邊爲構造函數。
instanceof
主要的實現原理就是隻要右邊變量的prototype
在左邊變量的原型鏈上便可。所以,instanceof
在查找的過程當中會遍歷左邊變量的原型鏈,直到找到右邊變量的 prototype,若是查找失敗,則會返回false
,告訴咱們左邊變量並不是是右邊變量的實例。
實現的簡約代碼以下:
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表達式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表達式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
複製代碼
const EventEmitter = require('events')
class EE extends EventEmitter {}
const yy = new EE()
console.log('測試開始')
yy.on('event', () => console.log('我是EventEmitter觸發的事件回調'))
setTimeout(() => {
console.log('0 毫秒後到期的定時器回調1')
process.nextTick(() => console.log('我是0毫秒定時器1加塞的一個微任務'))
}, 0)
setTimeout(() => {
console.log('0 毫秒後到期的定時器回調2')
process.nextTick(() => console.log('我是0毫秒定時器2加塞的一個微任務'))
}, 0)
setImmediate(() => console.log('immediate 當即回調'))
process.nextTick(() => console.log('process.nextTick 的第一次回調'))
new Promise((resolve) => {
console.log('我是promise')
}).then(() => {
yy.emit('event')
process.nextTick(() => console.log('process.nextTick 的第二次回調'))
console.log('promise 第一次回調')
})
.then(() => console.log('promise 第二次回調'))
console.log('測試結束?')
/* 打印結果以下:
測試開始
我是promise
測試結束?
process.nextTick 的第一次回調
0 毫秒後到期的定時器回調1
我是0毫秒定時器1加塞的一個微任務
0 毫秒後到期的定時器回調2
我是0毫秒定時器2加塞的一個微任務
immediate 當即回調
*/
複製代碼
js把異步任務隊列分爲兩種:宏任務(macro task)和微任務(micro task),兩者的區別是執行時機的不一樣。
異步隊列是怎麼執行這兩者任務的?請看下圖
上圖給的信息有如下幾點:
那麼知道了執行的機制以後,剩下的一個問題就是任務類型的劃分,整理以下一表,結合上面的問題,相信你心中有了答案了~
事件 | 宏任務/微任務 | 瀏覽器 | nodejs |
---|---|---|---|
I/O | 宏任務 | ✅ | ✅ |
setTimeout | 宏任務 | ✅ | ✅ |
setInterval | 宏任務 | ✅ | ✅ |
setImmediate | 宏任務 | ❌ | ✅ |
requestAnimationFrame | 宏任務 | ✅ | ❌ |
process.nextTick | 微任務 | ✅ | ✅ |
MutationObserver | 微任務 | ✅ | ❌ |
Promise.then catch finally | 微任務 | ✅ | ✅ |
EventEmitter | 微任務 | ❌ | ✅ |
Tips
const weakMap = new WeakMap()
let weakKey = {}
weakMap.set(weakKey, 'weakValue')
console.log(weakMap.get(weakKey))
weakKey = null
const map = new Map()
let key = {}
map.set(key, 'value')
console.log(map.get(key))
key = null
// chrome瀏覽器的Memory一欄中點擊一下「Collect garbage」後回來打印結果
console.log(weakMap, map)
複製代碼
WeakMap結構與Map結構基本相似,惟一的區別是它只接受對象做爲鍵名(null除外),不接受其餘類型的值做爲鍵名,並且鍵名所指向的對象,不計入垃圾回收機制。
WeakMap的設計目的在於,鍵名是對象的弱引用(垃圾回收機制不將該引用考慮在內),因此其所對應的對象可能會被自動回收。當對象被回收後,WeakMap自動移除對應的鍵值對。 典型應用是,一個對應DOM元素的WeakMap結構,當某個DOM元素被清除,其所對應的WeakMap記錄就會自動被移除。基本上,WeakMap的專用場合就是,它的鍵所對應的對象,可能會在未來消失。WeakMap結構有助於防止內存泄漏。
WeakMap與Map在API上的區別主要是兩個,
所以WeakMap只有四個方法可用:get()、set()、has()、delete()。
而Map有9個方法可用:get()
、set()
、has()
、delete()
、clear()
、keys()
、values()
、entries()
、forEach()
const express = require('express')
const app = express()
const sleep = (mseconds) => new Promise((resolve) => setTimeout(() => {
console.log('sleep timeout...')
resolve()
}, mseconds))
app.use(async (req, res, next) => {
console.log('I am the first middleware')
const startTime = Date.now()
console.log(`================ start ${req.method} ${req.url}`, { query: req.query, body: req.body });
next()
const cost = Date.now() - startTime
console.log(`================ end ${req.method} ${req.url} ${res.statusCode} - ${cost} ms`)
})
app.use((req, res, next) => {
console.log('I am the second middleware')
next()
console.log('second middleware end calling')
})
app.get('/api/test1', async(req, res, next) => {
console.log('I am the router middleware => /api/test1')
await sleep(2000)
res.status(200).send('hello')
})
app.listen(3000)
console.log('server listening at port 3000')
// 在Shell終端中請求: `curl 127.0.0.1:3000/api/test1`
// 打印結果爲:
I am the first middleware
================ start GET /api/test1
I am the second middleware
I am the router middleware => /api/test1
second middleware end calling
================ end GET /api/test1 200 - 3 ms
sleep timeout...
複製代碼
express框架和koa框架的中間件實現形式不同,前者使用回調的形式,後者採用async/await模式。回調形式在處理異步的中間件的時候沒可以很好處理執行流程,致使須要一些別的workaround。
更多區別參考:不再怕面試官問你express和koa的區別了
const _now = Date.now || function () {
return new Date().getTime();
}
const throttle = function (func, wait, options = {}) {
let context, args, result;
let timeout = null;
let previous = 0;
const later = function () {
previous = options.leading === false ? 0 : _now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function () {
// 記錄當前時間戳
const now = _now();
if (!previous && options.leading === false) previous = now;
const remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
// 解除引用,防止內存泄露
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) { // 最後一次須要觸發的狀況
timeout = setTimeout(later, remaining);
}
// 回調返回值
return result;
};
}
// 函數去抖(連續事件觸發結束後只觸發一次)
// sample 1: debounce(function(){}, 1000)
// 連續事件結束後的 1000ms 後觸發
// sample 1: debounce(function(){}, 1000, true)
// 連續事件觸發後當即觸發(此時會忽略第二個參數)
/* eslint-disable */
const debounce = function (func, wait, immediate) {
let timeout, args, context, timestamp, result;
const later = function () {
const last = _now() - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function () {
context = this;
args = arguments;
timestamp = _now();
const callNow = immediate && !timeout;
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
document.querySelector('.throttle').addEventListener('click', throttle(function(){
console.log('click event trigger')
}, 1000))
document.querySelector('.debounce').addEventListener('click', debounce(function(){
console.log('click event trigger')
}, 500))
複製代碼
兩者顧名思義,防抖(debounce)的含義即是爲了防止抖動形成的結果不許確,咱們在抖動的過程當中不去關注其中的變化,而是等到穩定的時候再處理結果。這種概念在硬件上一些電流或者電磁波圖有着不少的應用。在電流中通常會有毛刺,而咱們在計算結果的時候是不會去考慮這段異常的抖動,而是從總體上來評測結果,而在軟件上來實現防抖,即是在抖動的過程當中不去執行對應的動做,而是等到抖動結束趨於穩定的時候再來執行動做。
而節流(throttle)則是能夠形象地描述爲人爲地設置一個閘口,讓某一段時間內發生的時間的頻率下降下來,這個頻率能夠由你決定。想象一下你在一條流動的小溪中設置了一個關卡,原本一小時流量有10立方米,可是由於你的節流致使流量變成了5立方米,這樣咱們就稱爲節流。
所以,