js語言中那些讓你抓狂又容易混淆的概念(建議收藏)

下面羅列的js語言中那些讓人抓狂混淆的概念,你遇到過幾個?node

建議收藏此文,每當要面試的時候提早拿出來溫習溫習鞏固鞏固,屢次下來,這些概念相信會永遠印在你的腦海中~~git

每一小節都會有一道對應的練習題供參考,若是你作出的答案和題目的答案同樣,那麼這一小節的內容相信你已經掌握了github

一、Array.prototype.slice和Array.prototype.splice

1.一、練習題

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']
複製代碼

1.二、總結

這兩個方法名字何其類似,想區分開來蠻費勁的,最後可以記住的就是口訣是:放個p,就是不同,話糙理不糙~接下去咱們好好總結一下兩者的區別面試

兩者共同點都是從數組中提取指定的一段範圍內的數據,可是有三個不一樣點:chrome

  • 傳參不同,splice的入參是 start & count。而slice的入參是start & end,可是記住end是開區間
  • splice是會將原數組改造掉,而slice是不會的
  • splice還有另一個用途,能夠實現替換或者插入,其形參的第二個參數以後的都是替換或者插入的值,好比:months.splice(1, 0, 'Feb')
    • count是 0 或者負數,則不移除元素。這種狀況下,至少應添加一個新元素
    • count大於0,那麼替換掉對應個數的值

二、call和apply和bind

2.一、練習題

"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: "吹風機"}
複製代碼

2.二、總結

call和apply是一對孿生兄弟,而bind是這對兄弟的」經紀人「。call和apply說是孿生兄弟一點都不假,由於他們實現的功能如出一轍,都是爲了被調用的函數指定好執行的上下文(也就是this),惟一的區別是傳參給被調用的函數的參數形式,call使用的是以數量取勝大法,有多少個參數就傳多少個參數,而apply則換了套路,以簡潔取勝,由於人家將全部參數都打包到一個數組裏了。express

注意:api

  • callapply的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的學習)

  1. call和apply是能夠相互替換的,這僅僅是取決於你傳遞參數使用數組方便仍是逗號分隔的參數列表方便。

  2. call和apply很容易混淆掉,有時候會忘掉apply是使用數組仍是列表,那麼有一個簡單的記住辦法那就是apply的a和array的a是一致的,這樣就記住了吧?

  3. bind稍微不一樣,由於它返回的是一個函數,能夠在任何你想要執行的時候執行,而前面兩個函數都是立馬執行的。所以整體來講bind的靈活性會比call和apply更好,適用的場景更多

三、this

3.一、練習題

// 使用"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: ƒ, …}
複製代碼

3.二、總結

this這個小妖精,曾經迷惑了多少人。這裏的講解主要是總結,也不須要生硬地背下來,畢竟是有規律可循的。如下解析借鑑於這篇文章,略有擴展:this 的值究竟是什麼?一次說清楚

文中的做者結合call,仍是把this的一些指向說的蠻清楚的(在此基礎上繼續擴展),總結出如下兩種狀況(涵蓋了95%以上的場景):

  1. 全部的函數調用(非箭頭函數)均可以歸一化到call形式的調用,具體轉換規則以下:
  • fn(args) => fn.call(undefined, args)
  • obj.fn(args) => obj.fn.call(obj, args),此處的obj既能夠是對象字面量,也能夠是使用new出來的實例
  • fn.call(thisArgs, args) => 無須轉換, this就是thisArgs
  • fn.bind(thisArgs, args) => 無須轉換,this就是thisArgs,由於最後仍是調用的apply
  1. 箭頭函數的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

4.一、練習題

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
複製代碼

4.二、總結

這兩個概念牽扯到了原型和原型鏈,兩者的區別以及須要注意的東西都體如今了下圖,但願你們對原型鏈有這麼一張圖的印象(也就是每當出現這種題目的話,腦子可以浮現對應的關係圖),再結合下面總結的5條規律,深化印象,從而真正掌握住:

特此總結的一些規律貼在這裏(下面提到的每一條規律都用特定的顏色在上圖中標註一一對應):

一、只要是構造函數(構造函數能夠是原生的也能夠是自定義的,看上圖就知道了)都會有prototype屬性,而且都是指向其原型對象

二、構造函數實例化後的實例都有__proto__屬性,並指向其構造函數的原型對象

三、構造函數都有__proto__屬性,統一指向了原生Function的原型對象

四、原型對象都有會一個constructor的屬性,而且都是指向其構造函數

五、原型對象都有會一個__proto__的屬性,而且都是指向Object的原型對象

六、有一種很特殊的狀況,那就是給原型對象從新賦值的時候,須要特別考慮,這一點沒有在上圖中體現,可是在練習題裏體現了(Fn.prototype = {})

五、typeof和instanceof

5.一、練習題

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 複製代碼

5.二、總結

5.2.一、typeof

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 的判斷。

5.2.二、instanceof

instanceof是二元操做符,用來判斷變量是否爲某個對象的實例,返回值爲truefalse。操做符左邊爲對象,右邊爲構造函數。

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__
    }
}
複製代碼

六、宏任務和微任務

6.一、練習題(nodejs環境, 版本node11以上)

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 當即回調
*/
複製代碼

6.二、總結

js把異步任務隊列分爲兩種:宏任務(macro task)和微任務(micro task),兩者的區別是執行時機的不一樣。

異步隊列是怎麼執行這兩者任務的?請看下圖

上圖給的信息有如下幾點:

  • 先執行微任務的隊列,再檢查宏任務的隊列
  • 在當前的微任務沒有執行完成時,是不會執行下一個宏任務的。
  • 每次執行完一個宏任務以後,要檢查微任務隊列是否又有任務須要執行了(這個體如今上面的練習題中的超時後加塞的微任務隊列)

那麼知道了執行的機制以後,剩下的一個問題就是任務類型的劃分,整理以下一表,結合上面的問題,相信你心中有了答案了~

事件 宏任務/微任務 瀏覽器 nodejs
I/O 宏任務
setTimeout 宏任務
setInterval 宏任務
setImmediate 宏任務
requestAnimationFrame 宏任務
process.nextTick 微任務
MutationObserver 微任務
Promise.then catch finally 微任務
EventEmitter 微任務

Tips

  1. async函數在await以前的代碼都是同步執行的,能夠理解爲await以前的代碼屬於new Promise時傳入的代碼,await以後的全部代碼都是在Promise.then中的回調
  2. node11版本以前的打印和這裏的不大同樣,緣由能夠看這裏的MacroTask and MicroTask execution order

七、Map和Weak Map

7.一、練習題

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)
複製代碼

7.二、總結

WeakMap結構與Map結構基本相似,惟一的區別是它只接受對象做爲鍵名(null除外),不接受其餘類型的值做爲鍵名,並且鍵名所指向的對象,不計入垃圾回收機制。

WeakMap的設計目的在於,鍵名是對象的弱引用(垃圾回收機制不將該引用考慮在內),因此其所對應的對象可能會被自動回收。當對象被回收後,WeakMap自動移除對應的鍵值對。 典型應用是,一個對應DOM元素的WeakMap結構,當某個DOM元素被清除,其所對應的WeakMap記錄就會自動被移除。基本上,WeakMap的專用場合就是,它的鍵所對應的對象,可能會在未來消失。WeakMap結構有助於防止內存泄漏。

WeakMap與Map在API上的區別主要是兩個,

  1. 沒有遍歷操做(即沒有key()、values()和entries()方法),也沒有size屬性;
  2. 沒法清空,即不支持clear方法。這與WeakMap的鍵不被計入引用、被垃圾回收機制忽略有關。

所以WeakMap只有四個方法可用:get()、set()、has()、delete()

而Map有9個方法可用:get()set()has()delete()clear()keys()values()entries()forEach()

七、express和koa

7.一、練習題

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...
複製代碼

7.二、總結

express框架和koa框架的中間件實現形式不同,前者使用回調的形式,後者採用async/await模式。回調形式在處理異步的中間件的時候沒可以很好處理執行流程,致使須要一些別的workaround。

更多區別參考:不再怕面試官問你express和koa的區別了

八、防抖和節流

8.一、練習題

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))
複製代碼

8.二、總結

兩者顧名思義,防抖(debounce)的含義即是爲了防止抖動形成的結果不許確,咱們在抖動的過程當中不去關注其中的變化,而是等到穩定的時候再處理結果。這種概念在硬件上一些電流或者電磁波圖有着不少的應用。在電流中通常會有毛刺,而咱們在計算結果的時候是不會去考慮這段異常的抖動,而是從總體上來評測結果,而在軟件上來實現防抖,即是在抖動的過程當中不去執行對應的動做,而是等到抖動結束趨於穩定的時候再來執行動做。

而節流(throttle)則是能夠形象地描述爲人爲地設置一個閘口,讓某一段時間內發生的時間的頻率下降下來,這個頻率能夠由你決定。想象一下你在一條流動的小溪中設置了一個關卡,原本一小時流量有10立方米,可是由於你的節流致使流量變成了5立方米,這樣咱們就稱爲節流。

所以,

  • 防抖是不管事件觸發多少次都忽略,直到最後一次才調用回調函數。
  • 節流是不管事件觸發多少次,回調函數都是按照配置的觸發間隔調用。

參考

  1. slice
  2. splice
  3. new
  4. this
  5. this 的值究竟是什麼?一次說清楚
  6. call&apply&bind的學習
  7. JavaScript的原型和原型鏈的前世此生(一)
  8. 防抖和節流的代碼分析
  9. 不再怕面試官問你express和koa的區別了
  10. 微任務、宏任務與Event-Loop
相關文章
相關標籤/搜索