函數原型鏈中的 apply,call 和 bind 方法是 JavaScript 中至關重要的概念,與 this 關鍵字密切相關,至關一部分人對它們的理解仍是比較淺顯,所謂js基礎紮實,繞不開這些基礎經常使用的API,此次讓咱們來完全掌握它們吧!前端
fun.call(thisArg, param1, param2, ...) fun.apply(thisArg, [param1,param2,...]) fun.bind(thisArg, param1, param2, ...)
call/apply:fun
執行的結果
bind:返回fun
的拷貝,並擁有指定的this
值和初始參數react
thisArg
(可選):git
fun
的this
指向thisArg
對象fun
的this
爲undefined
param1,param2
(可選): 傳給fun
的參數。github
fun
的參數。call
/apply
/bind
的必須是個函數call、apply和bind是掛在Function對象上的三個方法,只有函數纔有這些方法。web
只要是函數就能夠,好比: Object.prototype.toString
就是個函數,咱們常常看到這樣的用法:Object.prototype.toString.call(data)
面試
改變函數執行時的this指向,目前全部關於它們的運用,都是基於這一點來進行的。segmentfault
弄混這兩個API的不在少數,不要小看這個問題,記住下面的這個方法就行了。
apply
是以a
開頭,它傳給fun
的參數是Array
,也是以a
開頭的。數組
傳給fun
的參數寫法不一樣:瀏覽器
apply
是第2個參數,這個參數是一個數組:傳給fun
參數都寫在數組中。call
從第2~n的參數都是傳給fun
的。執行:緩存
返回值:
fun
的執行結果返回值這段在下方bind應用中有詳細的示例解析。
看到一個很是棒的例子:
生活中:
平時沒時間作飯的我,週末想給孩子燉個醃篤鮮嚐嚐。可是沒有適合的鍋,而我又不想出去買。因此就問鄰居借了一個鍋來用,這樣既達到了目的,又節省了開支,一箭雙鵰。
程序中:
A對象有個方法,B對象由於某種緣由也須要用到一樣的方法,那麼這時候咱們是單獨爲 B 對象擴展一個方法呢,仍是借用一下 A 對象的方法呢?
固然是借用 A 對象的方法啦,既達到了目的,又節省了內存。
這就是call/apply/bind的核心理念:借用方法。
藉助已實現的方法,改變方法中數據的this指向,減小重複代碼,節省內存。
這些應用場景,多加體會就能夠發現它們的理念都是:借用方法
Object.prototype.toString
用來判斷類型再合適不過,借用它咱們幾乎能夠判斷全部類型的數據:
function isType(data, type) { const typeObj = { '[object String]': 'string', '[object Number]': 'number', '[object Boolean]': 'boolean', '[object Null]': 'null', '[object Undefined]': 'undefined', '[object Object]': 'object', '[object Array]': 'array', '[object Function]': 'function', '[object Date]': 'date', // Object.prototype.toString.call(new Date()) '[object RegExp]': 'regExp', '[object Map]': 'map', '[object Set]': 'set', '[object HTMLDivElement]': 'dom', // document.querySelector('#app') '[object WeakMap]': 'weakMap', '[object Window]': 'window', // Object.prototype.toString.call(window) '[object Error]': 'error', // new Error('1') '[object Arguments]': 'arguments', } let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()獲取數據類型 let typeName = typeObj[name] || '未知類型' // 匹配數據類型 return typeName === type // 判斷該數據類型是否爲傳入的類型 } console.log( isType({}, 'object'), // true isType([], 'array'), // true isType(new Date(), 'object'), // false isType(new Date(), 'date'), // true )
類數組由於不是真正的數組全部沒有數組類型上自帶的種種方法,因此咱們須要去借用數組的方法。
好比借用數組的push方法:
var arrayLike = { 0: 'OB', 1: 'Koro1', length: 2 } Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2'); console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}
apply直接傳遞數組作要調用方法的參數,也省一步展開數組,好比使用Math.max
、Math.min
來獲取數組的最大值/最小值:
const arr = [15, 6, 12, 13, 16]; const max = Math.max.apply(Math, arr); // 16 const min = Math.min.apply(Math, arr); // 6
ES5的繼承也都是經過借用父類的構造方法來實現父類方法/屬性的繼承:
// 父類 function supFather(name) { this.name = name; this.colors = ['red', 'blue', 'green']; // 複雜類型 } supFather.prototype.sayName = function (age) { console.log(this.name, 'age'); }; // 子類 function sub(name, age) { // 借用父類的方法:修改它的this指向,賦值父類的構造函數裏面方法、屬性到子類上 supFather.call(this, name); this.age = age; } // 重寫子類的prototype,修正constructor指向 function inheritPrototype(sonFn, fatherFn) { sonFn.prototype = Object.create(fatherFn.prototype); // 繼承父類的屬性以及方法 sonFn.prototype.constructor = sonFn; // 修正constructor指向到繼承的那個函數上 } inheritPrototype(sub, supFather); sub.prototype.sayAge = function () { console.log(this.age, 'foo'); }; // 實例化子類,能夠在實例上找到屬性、方法 const instance1 = new sub("OBKoro1", 24); const instance2 = new sub("小明", 18); instance1.colors.push('black') console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24} console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}
相似的應用場景還有不少,就不贅述了,關鍵在於它們借用方法的理念,不理解的話多看幾遍。
call,apply的效果徹底同樣,它們的區別也在於
參數數量/順序不肯定的話就用apply,好比如下示例:
const obj = { age: 24, name: 'OBKoro1', } const obj2 = { age: 777 } callObj(obj, handle) callObj(obj2, handle) // 根據某些條件來決定要傳遞參數的數量、以及順序 function callObj(thisAge, fn) { let params = [] if (thisAge.name) { params.push(thisAge.name) } if (thisAge.age) { params.push(thisAge.age) } fn.apply(thisAge, params) // 數量和順序不肯定 不能使用call } function handle(...params) { console.log('params', params) // do some thing }
首先來看下一道經典的面試題:
for (var i = 1; i <= 5; i++) { setTimeout(function test() { console.log(i) // 依次輸出:6 6 6 6 6 }, i * 1000); }
形成這個現象的緣由是等到setTimeout
異步執行時,i
已經變成6了。
關於js事件循環機制不理解的同窗,能夠看我這篇博客:Js 的事件循環(Event Loop)機制以及實例講解
那麼如何使他輸出: 1,2,3,4,5呢?
方法有不少:
for (var i = 1; i <= 5; i++) { (function (i) { setTimeout(function () { console.log('閉包:', i); // 依次輸出:1 2 3 4 5 }, i * 1000); }(i)); }
在這裏建立了一個閉包,每次循環都會把i
的最新值傳進去,而後被閉包保存起來。
for (var i = 1; i <= 5; i++) { // 緩存參數 setTimeout(function (i) { console.log('bind', i) // 依次輸出:1 2 3 4 5 }.bind(null, i), i * 1000); }
實際上這裏也用了閉包,咱們知道bind會返回一個函數,這個函數也是閉包。
它保存了函數的this指向、初始參數,每次i
的變動都會被bind的閉包存起來,因此輸出1-5。
具體細節,下面有個手寫bind方法,研究一下,就能搞懂了。
let
用let
聲明i
也能夠輸出1-5: 由於let
是塊級做用域,因此每次都會建立一個新的變量,因此setTimeout
每次讀的值都是不一樣的,詳解。
這是一個常見的問題,下面是我在開發VSCode插件處理webview
通訊時,遇到的真實問題,一開始覺得VSCode的API哪裏出問題,調試了一番才發現是this
指向丟失的問題。
class Page { constructor(callBack) { this.className = 'Page' this.MessageCallBack = callBack // this.MessageCallBack('發給註冊頁面的信息') // 執行PageA的回調函數 } } class PageA { constructor() { this.className = 'PageA' this.pageClass = new Page(this.handleMessage) // 註冊頁面 傳遞迴調函數 問題在這裏 } // 與頁面通訊回調 handleMessage(msg) { console.log('處理通訊', this.className, msg) // 'Page' this指向錯誤 } } new PageA()
this
爲什麼會丟失?顯然聲明的時候不會出現問題,執行回調函數的時候也不可能出現問題。
問題出在傳遞迴調函數的時候:
this.pageClass = new Page(this.handleMessage)
由於傳遞過去的this.handleMessage
是一個函數內存地址,沒有上下文對象,也就是說該函數沒有綁定它的this
指向。
那它的this
指向於它所應用的綁定規則:
class Page { constructor(callBack) { this.className = 'Page' // callBack() // 直接執行的話 因爲class 內部是嚴格模式,因此this 實際指向的是 undefined this.MessageCallBack = callBack // 回調函數的this 隱式綁定到class page this.MessageCallBack('發給註冊頁面的信息') } }
既然知道問題了,那咱們只要綁定回調函數的this
指向爲PageA
就解決問題了。
回調函數this丟失的解決方案:
bind
綁定回調函數的this
指向:這是典型bind的應用場景, 綁定this指向,用作回調函數。
this.pageClass = new Page(this.handleMessage.bind(this)) // 綁定回調函數的this指向
PS: 這也是爲何react
的render
函數在綁定回調函數的時候,也要使用bind綁定一下this
的指向,也是由於一樣的問題以及原理。
箭頭函數的this指向定義的時候外層第一個普通函數的this,在這裏指的是class類:PageA
這塊內容,能夠看下我以前寫的博客:詳解箭頭函數和普通函數的區別以及箭頭函數的注意事項、不適用場景
this.pageClass = new Page(() => this.handleMessage()) // 箭頭函數綁定this指向
在大廠的面試中,手寫實現call,apply,bind(特別是bind)一直是比較高頻的面試題,在這裏咱們也一塊兒來實現一下這幾個函數。
call
嗎?思路
this
的指向。context
的屬性,將函數的this指向隱式綁定到context上Function.prototype.myCall = function (context, ...arr) { if (context === null || context === undefined) { // 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window) context = window } else { context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象 } const specialPrototype = Symbol('特殊屬性Symbol') // 用於臨時儲存函數 context[specialPrototype] = this; // 函數的this指向隱式綁定到context上 let result = context[specialPrototype](...arr); // 經過隱式綁定執行函數並傳遞參數 delete context[specialPrototype]; // 刪除上下文對象的屬性 return result; // 返回函數執行結果 };
不少人判斷函數上下文對象,只是簡單的以context
是否爲false來判斷,好比:
// 判斷函數上下文綁定到`window`不夠嚴謹 context = context ? Object(context) : window; context = context || window;
通過測試,如下三種爲false的狀況,函數的上下文對象都會綁定到window
上:
// 網上的其餘綁定函數上下文對象的方案: context = context || window; function handle(...params) { this.test = 'handle' console.log('params', this, ...params) // do some thing } handle.elseCall('') // window handle.elseCall(0) // window handle.elseCall(false) // window
而call
則將函數的上下文對象會綁定到這些原始值的實例對象上:
因此正確的解決方案,應該是像我上面那麼作:
// 正確判斷函數上下文對象 if (context === null || context === undefined) { // 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window) context = window } else { context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象 }
Symbol
臨時儲存函數儘管以前用的屬性是testFn
但不得不認可,仍是有跟上下文對象的原屬性衝突的風險,經網友提醒使用Symbol
就不會出現衝突了。
考慮兼容的話,仍是用盡可能特殊的屬性,好比帶上本身的ID:OBKoro1TestFn
。
apply
嗎?思路:
call
同樣。apply
接受第二個參數爲類數組對象, 這裏用了JavaScript權威指南中判斷是否爲類數組對象的方法。Function.prototype.myApply = function (context) { if (context === null || context === undefined) { context = window // 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window) } else { context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象 } // JavaScript權威指南判斷是否爲類數組對象 function isArrayLike(o) { if (o && // o不是null、undefined等 typeof o === 'object' && // o是對象 isFinite(o.length) && // o.length是有限數值 o.length >= 0 && // o.length爲非負值 o.length === Math.floor(o.length) && // o.length是整數 o.length < 4294967296) // o.length < 2^32 return true else return false } const specialPrototype = Symbol('特殊屬性Symbol') // 用於臨時儲存函數 context[specialPrototype] = this; // 隱式綁定this指向到context上 let args = arguments[1]; // 獲取參數數組 let result // 處理傳進來的第二個參數 if (args) { // 是否傳遞第二個參數 if (!Array.isArray(args) && !isArrayLike(args)) { throw new TypeError('myApply 第二個參數不爲數組而且不爲類數組對象拋出錯誤'); } else { args = Array.from(args) // 轉爲數組 result = context[specialPrototype](...args); // 執行函數並展開數組,傳遞函數參數 } } else { result = context[specialPrototype](); // 執行函數 } delete context[specialPrototype]; // 刪除上下文對象的屬性 return result; // 返回函數執行結果 };
bind
嗎?劃重點:
手寫bind
是大廠中的一個高頻的面試題,若是面試的中高級前端,只是能說出它們的區別,用法並不能脫穎而出,理解要有足夠的深度才能抱得offer歸!
思路
拷貝源函數:
Object.create
複製源函數的prototype給fToBind調用拷貝的函數:
instanceof
判斷函數是否經過new
調用,來決定綁定的context
prototype
的狀況Function.prototype.myBind = function (objThis, ...params) { const thisFn = this; // 存儲源函數以及上方的params(函數參數) // 對返回的函數 secondParams 二次傳參 let fToBind = function (...secondParams) { const isNew = this instanceof fToBind // this是不是fToBind的實例 也就是返回的fToBind是否經過new調用 const context = isNew ? this : Object(objThis) // new調用就綁定到this上,不然就綁定到傳入的objThis上 return thisFn.call(context, ...params, ...secondParams); // 用call調用源函數綁定this的指向並傳遞參數,返回執行結果 }; if (thisFn.prototype) { // 複製源函數的prototype給fToBind 一些狀況下函數沒有prototype,好比箭頭函數 fToBind.prototype = Object.create(thisFn.prototype); } return fToBind; // 返回拷貝的函數 };
prototype
箭頭函數沒有prototype
,這個我知道的,但是getInfo2
就是一個縮寫,爲何沒有prototype
。
谷歌/stack overflow
都沒有找到緣由,有大佬指點迷津一下嗎??
var student = { getInfo: function (name, isRegistered) { console.log('this1', this) }, getInfo2(name, isRegistered) { console.log('this2', this) // 沒有prototype }, getInfo3: (name, isRegistered) => { console.log('this3', this) // 沒有prototype } }
原本覺得這篇會寫的很快,結果斷斷續續的寫了好幾天,終於把這三個API相關知識介紹清楚了,但願你們看完以後,面試的時候再遇到這個問題,就能夠海陸空全方位的裝逼了^_^