js基礎-重新認識call,apply,bind[不看後悔系列]

函數原型鏈中的 apply,call 和 bind 方法是 JavaScript 中相當重要的概念,與 this 關鍵字密切相關,相當一部分人對它們的理解還是比較淺顯,所謂js基礎紮實,繞不開這些基礎常用的API,這次讓我們來徹底掌握它們吧!

目錄

  1. call,apply,bind的基本介紹

  2. call/apply/bind的核心理念:借用方法

  3. call和apply的應用場景

  4. bind的應用場景

  5. 中高級面試題:手寫call/apply、bind


call,apply,bind的基本介紹

語法:

  
    
  
  
  
  1. fun.call(thisArg, param1, param2, ...)

  2. fun.apply(thisArg, [param1,param2,...])

  3. fun.bind(thisArg, param1, param2, ...)

返回值:

call/apply: fun執行的結果 bind:返回 fun的拷貝,並擁有指定的 this值和初始參數

參數

thisArg(可選):

  1. fun的 this指向 thisArg對象

  2. 非嚴格模式下:thisArg指定爲null,undefined,fun中的this指向window對象.

  3. 嚴格模式下: fun的 this爲 undefined

  4. 值爲原始值(數字,字符串,布爾值)的this會指向該原始值的自動包裝對象,如 String、Number、Boolean

param1,param2(可選): 傳給 fun的參數。

  1. 如果param不傳或爲 null/undefined,則表示不需要傳入任何參數.

  2. apply第二個參數爲數組,數組內的值爲傳給 fun的參數。

調用 callapplybind的必須是個函數

call、apply和bind是掛在Function對象上的三個方法,只有函數纔有這些方法。

只要是函數就可以,比如: Object.prototype.toString就是個函數,我們經常看到這樣的用法: Object.prototype.toString.call(data)

作用:

改變函數執行時的this指向,目前所有關於它們的運用,都是基於這一點來進行的。

如何不弄混call和aaply

弄混這兩個API的不在少數,不要小看這個問題,記住下面的這個方法就好了。

apply是以 a開頭,它傳給 fun的參數是 Array,也是以 a開頭的。

區別:

call與apply的唯一區別

傳給 fun的參數寫法不同:

  • apply是第2個參數,這個參數是一個數組:傳給 fun參數都寫在數組中。

  • call從第2~n的參數都是傳給 fun的。

call/apply與bind的區別

執行

  • call/apply改變了函數的this上下文後馬上執行該函數

  • bind則是返回改變了上下文後的函數,不執行該函數

返回值:

  • call/apply 返回 fun的執行結果

  • bind返回fun的拷貝,並指定了fun的this指向,保存了fun的參數。

返回值這段在下方bind應用中有詳細的示例解析。

call/apply/bind的核心理念:借用方法

看到一個非常棒的例子:

生活中:

平時沒時間做飯的我,週末想給孩子燉個醃篤鮮嚐嚐。但是沒有適合的鍋,而我又不想出去買。所以就問鄰居借了一個鍋來用,這樣既達到了目的,又節省了開支,一舉兩得。

程序中:

A對象有個方法,B對象因爲某種原因也需要用到同樣的方法,那麼這時候我們是單獨爲 B 對象擴展一個方法呢,還是借用一下 A 對象的方法呢?

當然是借用 A 對象的方法啦,既達到了目的,又節省了內存。

這就是call/apply/bind的核心理念:借用方法

藉助已實現的方法,改變方法中數據的this指向,減少重複代碼,節省內存。

call和apply的應用場景:

這些應用場景,多加體會就可以發現它們的理念都是:借用方法

  1. 判斷數據類型:

Object.prototype.toString用來判斷類型再合適不過,借用它我們幾乎可以判斷所有類型的數據:

  
    
  
  
  
  1. function isType(data, type) {

  2. const typeObj = {

  3. '[object String]': 'string',

  4. '[object Number]': 'number',

  5. '[object Boolean]': 'boolean',

  6. '[object Null]': 'null',

  7. '[object Undefined]': 'undefined',

  8. '[object Object]': 'object',

  9. '[object Array]': 'array',

  10. '[object Function]': 'function',

  11. '[object Date]': 'date', // Object.prototype.toString.call(new Date())

  12. '[object RegExp]': 'regExp',

  13. '[object Map]': 'map',

  14. '[object Set]': 'set',

  15. '[object HTMLDivElement]': 'dom', // document.querySelector('#app')

  16. '[object WeakMap]': 'weakMap',

  17. '[object Window]': 'window', // Object.prototype.toString.call(window)

  18. '[object Error]': 'error', // new Error('1')

  19. '[object Arguments]': 'arguments',

  20. }

  21. let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()獲取數據類型

  22. let typeName = typeObj[name] || '未知類型' // 匹配數據類型

  23. return typeName === type // 判斷該數據類型是否爲傳入的類型

  24. }

  25. console.log(

  26. isType({}, 'object'), // true

  27. isType([], 'array'), // true

  28. isType(new Date(), 'object'), // false

  29. isType(new Date(), 'date'), // true

  30. )

  1. 類數組借用數組的方法:

類數組因爲不是真正的數組所有沒有數組類型上自帶的種種方法,所以我們需要去借用數組的方法。

比如借用數組的push方法:

  
    
  
  
  
  1. var arrayLike = {

  2. 0: 'OB',

  3. 1: 'Koro1',

  4. length: 2

  5. }

  6. Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2');

  7. console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}

  1. apply獲取數組最大值最小值:

apply直接傳遞數組做要調用方法的參數,也省一步展開數組,比如使用 Math.max、 Math.min來獲取數組的最大值/最小值:

  
    
  
  
  
  1. const arr = [15, 6, 12, 13, 16];

  2. const max = Math.max.apply(Math, arr); // 16

  3. const min = Math.min.apply(Math, arr); // 6

  1. 繼承

ES5的繼承也都是通過借用父類的構造方法來實現父類方法/屬性的繼承:

  
    
  
  
  
  1. // 父類

  2. function supFather(name) {

  3. this.name = name;

  4. this.colors = ['red', 'blue', 'green']; // 複雜類型

  5. }

  6. supFather.prototype.sayName = function (age) {

  7. console.log(this.name, 'age');

  8. };

  9. // 子類

  10. function sub(name, age) {

  11. // 借用父類的方法:修改它的this指向,賦值父類的構造函數裏面方法、屬性到子類上

  12. supFather.call(this, name);

  13. this.age = age;

  14. }

  15. // 重寫子類的prototype,修正constructor指向

  16. function inheritPrototype(sonFn, fatherFn) {

  17. sonFn.prototype = Object.create(fatherFn.prototype); // 繼承父類的屬性以及方法

  18. sonFn.prototype.constructor = sonFn; // 修正constructor指向到繼承的那個函數上

  19. }

  20. inheritPrototype(sub, supFather);

  21. sub.prototype.sayAge = function () {

  22. console.log(this.age, 'foo');

  23. };

  24. // 實例化子類,可以在實例上找到屬性、方法

  25. const instance1 = new sub("OBKoro1", 24);

  26. const instance2 = new sub("小明", 18);

  27. instance1.colors.push('black')

  28. console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}

  29. console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}

類似的應用場景還有很多,就不贅述了,關鍵在於它們借用方法的理念,不理解的話多看幾遍。

call、apply,該用哪個?、

call,apply的效果完全一樣,它們的區別也在於

  • 參數數量/順序確定就用call,參數數量/順序不確定的話就用apply

  • 考慮可讀性:參數數量不多就用apply,參數數量比較多的話,把參數整合成數組,使用apply。

  • 參數集合已經是一個數組的情況,用apply,比如上文的獲取數組最大值/最小值。

參數數量/順序不確定的話就用apply,比如以下示例:

  
    
  
  
  
  1. const obj = {

  2. age: 24,

  3. name: 'OBKoro1',

  4. }

  5. const obj2 = {

  6. age: 777

  7. }

  8. callObj(obj, handle)

  9. callObj(obj2, handle)

  10. // 根據某些條件來決定要傳遞參數的數量、以及順序

  11. function callObj(thisAge, fn) {

  12. let params = []

  13. if (thisAge.name) {

  14. params.push(thisAge.name)

  15. }

  16. if (thisAge.age) {

  17. params.push(thisAge.age)

  18. }

  19. fn.apply(thisAge, params) // 數量和順序不確定 不能使用call

  20. }

  21. function handle(...params) {

  22. console.log('params', params) // do some thing

  23. }

bind的應用場景:

1. 保存函數參數:

首先來看下一道經典的面試題:

  
    
  
  
  
  1. for (var i = 1; i <= 5; i++) {

  2. setTimeout(function test() {

  3. console.log(i) // 依次輸出:6 6 6 6 6

  4. }, i * 1000);

  5. }

造成這個現象的原因是等到 setTimeout異步執行時, i已經變成6了。

關於js事件循環機制不理解的同學,可以看我這篇博客:Js 的事件循環(Event Loop)機制以及實例講解

那麼如何使他輸出: 1,2,3,4,5呢?

方法有很多:

  • 閉包, 保存變量

  
    
  
  
  
  1. for (var i = 1; i <= 5; i++) {

  2. (function (i) {

  3. setTimeout(function () {

  4. console.log('閉包:', i); // 依次輸出:1 2 3 4 5

  5. }, i * 1000);

  6. }(i));

  7. }

在這裏創建了一個閉包,每次循環都會把 i的最新值傳進去,然後被閉包保存起來。

  • bind

  
    
  
  
  
  1. for (var i = 1; i <= 5; i++) {

  2. // 緩存參數

  3. setTimeout(function (i) {

  4. console.log('bind', i) // 依次輸出:1 2 3 4 5

  5. }.bind(null, i), i * 1000);

  6. }

實際上這裏也用了閉包,我們知道bind會返回一個函數,這個函數也是閉包

它保存了函數的this指向、初始參數,每次 i的變更都會被bind的閉包存起來,所以輸出1-5。

具體細節,下面有個手寫bind方法,研究一下,就能搞懂了。

  • let

用 let聲明 i也可以輸出1-5: 因爲 let是塊級作用域,所以每次都會創建一個新的變量,所以 setTimeout每次讀的值都是不同的,詳解。

2. 回調函數this丟失問題:

這是一個常見的問題,下面是我在開發VSCode插件處理 webview通信時,遇到的真實問題,一開始以爲VSCode的API哪裏出問題,調試了一番才發現是 this指向丟失的問題。

  
    
  
  
  
  1. class Page {

  2. constructor(callBack) {

  3. this.className = 'Page'

  4. this.MessageCallBack = callBack //

  5. this.MessageCallBack('發給註冊頁面的信息') // 執行PageA的回調函數

  6. }

  7. }

  8. class PageA {

  9. constructor() {

  10. this.className = 'PageA'

  11. this.pageClass = new Page(this.handleMessage) // 註冊頁面 傳遞迴調函數 問題在這裏

  12. }

  13. // 與頁面通信回調

  14. handleMessage(msg) {

  15. console.log('處理通信', this.className, msg) // 'Page' this指向錯誤

  16. }

  17. }

  18. new PageA()

回調函數 this爲何會丟失?

顯然聲明的時候不會出現問題,執行回調函數的時候也不可能出現問題。

問題出在傳遞迴調函數的時候:

  
    
  
  
  
  1. this.pageClass = new Page(this.handleMessage)

因爲傳遞過去的 this.handleMessage是一個函數內存地址,沒有上下文對象,也就是說該函數沒有綁定它的 this指向。

那它的 this指向於它所應用的綁定規則:

  
    
  
  
  
  1. class Page {

  2. constructor(callBack) {

  3. this.className = 'Page'

  4. // callBack() // 直接執行的話 由於class 內部是嚴格模式,所以this 實際指向的是 undefined

  5. this.MessageCallBack = callBack // 回調函數的this 隱式綁定到class page

  6. this.MessageCallBack('發給註冊頁面的信息')

  7. }

  8. }

既然知道問題了,那我們只要綁定回調函數的 this指向爲 PageA就解決問題了。

回調函數this丟失的解決方案

  1. bind綁定回調函數的 this指向:

這是典型bind的應用場景, 綁定this指向,用做回調函數。

  
    
  
  
  
  1. this.pageClass = new Page(this.handleMessage.bind(this)) // 綁定回調函數的this指向

PS:這也是爲什麼 react的 render函數在綁定回調函數的時候,也要使用bind綁定一下 this的指向,也是因爲同樣的問題以及原理。

  1. 箭頭函數綁定this指向

箭頭函數的this指向定義的時候外層第一個普通函數的this,在這裏指的是class類: PageA

這塊內容,可以看下我之前寫的博客:詳解箭頭函數和普通函數的區別以及箭頭函數的注意事項、不適用場景

  
    
  
  
  
  1. this.pageClass = new Page(() => this.handleMessage()) // 箭頭函數綁定this指向

中高級面試題-手寫call/apply、bind:

在大廠的面試中,手寫實現call,apply,bind(特別是bind)一直是比較高頻的面試題,在這裏我們也一起來實現一下這幾個函數。

你能手寫實現一個 call嗎?

思路

  1. 根據call的規則設置上下文對象,也就是 this的指向。

  2. 通過設置 context的屬性,將函數的this指向隱式綁定到context上

  3. 通過隱式綁定執行函數並傳遞參數。

  4. 刪除臨時屬性,返回函數執行結果

  
    
  
  
  
  1. Function.prototype.myCall = function (context, ...arr) {

  2. if (context === null || context === undefined) {

  3. // 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window)

  4. context = window

  5. } else {

  6. context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象

  7. }

  8. context.testFn = this; // 函數的this指向隱式綁定到context上

  9. let result = context.testFn(...arr); // 通過隱式綁定執行函數並傳遞參數

  10. delete context.testFn; // 刪除上下文對象的屬性

  11. return result; // 返回函數執行結果

  12. };

判斷函數的上下文對象:

很多人判斷函數上下文對象,只是簡單的以 context是否爲false來判斷,比如:

  
    
  
  
  
  1. // 判斷函數上下文綁定到`window`不夠嚴謹

  2. context = context ? Object(context) : window;

  3. context = context || window;

經過測試,以下三種爲false的情況,函數的上下文對象都會綁定到 window上:

  
    
  
  
  
  1. // 網上的其他綁定函數上下文對象的方案: context = context || window;

  2. function handle(...params) {

  3. this.test = 'handle'

  4. console.log('params', this, ...params) // do some thing

  5. }

  6. handle.elseCall('') // window

  7. handle.elseCall(0) // window

  8. handle.elseCall(false) // window

而 call則將函數的上下文對象會綁定到這些原始值的實例對象上:

所以正確的解決方案,應該是像我上面那麼做:

  
    
  
  
  
  1. // 正確判斷函數上下文對象

  2. if (context === null || context === undefined) {

  3. // 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window)

  4. context = window

  5. } else {

  6. context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象

  7. }

你能手寫實現一個 apply嗎?

思路:

  1. 傳遞給函數的參數處理,不太一樣,其他部分跟 call一樣。

  2. apply接受第二個參數爲類數組對象, 這裏用了JavaScript權威指南中判斷是否爲類數組對象的方法。

  
    
  
  
  
  1. Function.prototype.myApply = function (context) {

  2. if (context === null || context === undefined) {

  3. context = window // 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window)

  4. } else {

  5. context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象

  6. }

  7. // JavaScript權威指南判斷是否爲類數組對象

  8. function isArrayLike(o) {

  9. if (o && // o不是null、undefined等

  10. typeof o === 'object' && // o是對象

  11. isFinite(o.length) && // o.length是有限數值

  12. o.length >= 0 && // o.length爲非負值

  13. o.length === Math.floor(o.length) && // o.length是整數

  14. o.length < 4294967296) // o.length < 2^32

  15. return true

  16. else

  17. return false

  18. }

  19. context.testFn = this; // 隱式綁定this指向到context上

  20. const args = arguments[1]; // 獲取參數數組

  21. let result

  22. // 處理傳進來的第二個參數

  23. if (args) {

  24. // 是否傳遞第二個參數

  25. if (!Array.isArray(args) && !isArrayLike(args)) {

  26. throw new TypeError('myApply 第二個參數不爲數組並且不爲類數組對象拋出錯誤');

  27. } else {

  28. args = Array.from(args) // 轉爲數組

  29. result = context.testFn(...args); // 執行函數並展開數組,傳遞函數參數

  30. }

  31. } else {

  32. result = context.testFn(); // 執行函數

  33. }

  34. delete context.testFn; // 刪除上下文對象的屬性

  35. return result; // 返回函數執行結果

  36. };

你能手寫實現一個 bind嗎?

劃重點

手寫 bind是大廠中的一個高頻的面試題,如果面試的中高級前端,只是能說出它們的區別,用法並不能脫穎而出,理解要有足夠的深度才能抱得offer歸!

思路

  1. 拷貝源函數:

  • 通過變量儲存源函數

  • 使用 Object.create複製源函數的prototype給fToBind

  • 返回拷貝的函數

  • 調用拷貝的函數:

    • new調用判斷:通過 instanceof判斷函數是否通過 new調用,來決定綁定的 context

    • 綁定this+傳遞參數

    • 返回源函數的執行結果

      
        
      
      
      
    1. Function.prototype.myBind = function (objThis, ...params) {

    2. const thisFn = this; // 存儲源函數以及上方的params(函數參數)

    3. let fToBind = function () {

    4. const isNew = this instanceof fToBind // this是否是fToBind的實例 也就是返回的fToBind是否通過new調用

    5. const context = isNew ? this : Object(objThis) // new調用就綁定到this上,否則就綁定到傳入的objThis上

    6. return thisFn.apply(context, params); // 用apply調用源函數綁定this的指向並傳遞參數,返回執行結果

    7. };

    8. fToBind.prototype = Object.create(thisFn.prototype); // 複製源函數的prototype給fToBind

    9. return fToBind; // 返回拷貝的函數

    10. };

    小結

    本來以爲這篇會寫的很快,結果斷斷續續的寫了好幾天,終於把這三個API相關知識介紹清楚了,希望大家看完之後,面試的時候再遇到這個問題,就可以海陸空全方位的裝逼了^_^

    覺得我的博客對你有幫助的話,就給我點個Star吧!

    前端進階積累、公衆號、GitHub、wx:OBkoro1、郵箱:[email protected]

    以上2019/8/30


    本文分享自微信公衆號 - OBKoro1前端進階積累(gh_8af2fb8e54a9)。
    如有侵權,請聯繫 [email protected] 刪除。
    本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章