摘要:返回值這段在下方應用中有詳細的示例解析。回調函數丟失的解決方案綁定回調函數的指向這是典型的應用場景綁定指向,用做回調函數。
函數原型鏈中的 apply,call 和 bind 方法是 JavaScript 中相當重要的概念,與 this 關鍵字密切相關,相當一部分人對它們的理解還是比較淺顯,所謂js基礎扎實,繞不開這些基礎常用的API,這次讓我們來徹底掌握它們吧!
目錄call,apply,bind的基本介紹
call/apply/bind的核心理念:借用方法
call和apply的應用場景
bind的應用場景
中高級面試題:手寫call/apply、bind
call,apply,bind的基本介紹 語法:fun.call(thisArg, param1, param2, ...) fun.apply(thisArg, [param1,param2,...]) fun.bind(thisArg, param1, param2, ...)返回值:
call/apply:fun執行的結果
bind:返回fun的拷貝,并擁有指定的this值和初始參數
thisArg(可選):
fun的this指向thisArg對象
非嚴格模式下:thisArg指定為null,undefined,fun中的this指向window對象.
嚴格模式下:fun的this為undefined
值為原始值(數字,字符串,布爾值)的this會指向該原始值的自動包裝對象,如 String、Number、Boolean
param1,param2(可選): 傳給fun的參數。
如果param不傳或為 null/undefined,則表示不需要傳入任何參數.
apply第二個參數為數組,數組內的值為傳給fun的參數。
調用call/apply/bind的必須是個函數call、apply和bind是掛在Function對象上的三個方法,只有函數才有這些方法。
只要是函數就可以,比如: Object.prototype.toString就是個函數,我們經常看到這樣的用法:Object.prototype.toString.call(data)
作用:改變函數執行時的this指向,目前所有關于它們的運用,都是基于這一點來進行的。
如何不弄混call和apply弄混這兩個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的應用場景:這些應用場景,多加體會就可以發現它們的理念都是:借用方法
判斷數據類型:
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獲取數組最大值最小值:
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,該用哪個?、call,apply的效果完全一樣,它們的區別也在于
參數數量/順序確定就用call,參數數量/順序不確定的話就用apply。
考慮可讀性:參數數量不多就用call,參數數量比較多的話,把參數整合成數組,使用apply。
參數集合已經是一個數組的情況,用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 }bind的應用場景: 1. 保存函數參數:
首先來看下一道經典的面試題:
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的最新值傳進去,然后被閉包保存起來。
bind
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每次讀的值都是不同的,詳解。
2. 回調函數this丟失問題:這是一個常見的問題,下面是我在開發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指向定義的時候外層第一個普通函數的this,在這里指的是class類:PageA
這塊內容,可以看下我之前寫的博客:詳解箭頭函數和普通函數的區別以及箭頭函數的注意事項、不適用場景
this.pageClass = new Page(() => this.handleMessage()) // 箭頭函數綁定this指向中高級面試題-手寫call/apply、bind:
在大廠的面試中,手寫實現call,apply,bind(特別是bind)一直是比較高頻的面試題,在這里我們也一起來實現一下這幾個函數。
你能手寫實現一個call嗎?思路
根據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
返回拷貝的函數
調用拷貝的函數:
new調用判斷:通過instanceof判斷函數是否通過new調用,來決定綁定的context
綁定this+傳遞參數
返回源函數的執行結果
Function.prototype.myBind = function (objThis, ...params) { const thisFn = this; // 存儲源函數以及上方的params(函數參數) // 對返回的函數 secondParams 二次傳參 let fToBind = function (...secondParams) { console.log("secondParams",secondParams,...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的指向并傳遞參數,返回執行結果 }; fToBind.prototype = Object.create(thisFn.prototype); // 復制源函數的prototype給fToBind return fToBind; // 返回拷貝的函數 };小結
本來以為這篇會寫的很快,結果斷斷續續的寫了好幾天,終于把這三個API相關知識介紹清楚了,希望大家看完之后,面試的時候再遇到這個問題,就可以海陸空全方位的裝逼了^_^
覺得我的博客對你有幫助的話,就給我點個Star吧!前端進階積累、公眾號、GitHub、wx:OBkoro1、郵箱:obkoro1@foxmail.com
以上2019/8/30
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106374.html
摘要:通過面試者的答案可以得知他都知道哪些開發語言。這個問題的答案能夠知道求職者有沒有可靠的資源,這些資源在未來開展項目時可能會派上用場。對這個問題的誠實回答可以幫助面試官為求職者提供合適的團隊。 翻譯:瘋狂的技術宅原文:https://www.indeed.com/hire/i... 不管你是面試官還是求職者,里面的思路都能讓你獲益匪淺。 你用 CSS 多久了? 解析: 這個問題可以讓面...
摘要:先說下我面試情況,我一共面試了家公司。篇在我面試的眾多公司里,只有同城的面問到相關問題,其他公司壓根沒問。我自己回答的是自己開發組件面臨的問題。完全不用擔心對方到時候打電話核對的問題。 2019的5月9號,離發工資還有1天的時候,我的領導親切把我叫到辦公室跟我說:阿郭,我們公司要倒閉了,錢是沒有的啦,為了不耽誤你,你趕緊出去找工作吧。聽到這話,我虎軀一震,這已經是第2個月沒工資了。 公...
摘要:返回的綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器。同時,將第一個參數以外的其他參數,作為提供給原函數的預設參數,這也是基本的顆粒化基礎。 今天想談談一道前端面試題,我做面試官的時候經常喜歡用它來考察面試者的基礎是否扎實,以及邏輯、思維能力和臨場表現,題目是:模擬實現ES5中原生bind函數。也許這道題目已經不再新鮮,部分讀者也會有思路來解答。社區上關于原生bind的研...
摘要:返回的綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器。同時,將第一個參數以外的其他參數,作為提供給原函數的預設參數,這也是基本的顆粒化基礎。 今天想談談一道前端面試題,我做面試官的時候經常喜歡用它來考察面試者的基礎是否扎實,以及邏輯、思維能力和臨場表現,題目是:模擬實現ES5中原生bind函數。也許這道題目已經不再新鮮,部分讀者也會有思路來解答。社區上關于原生bind的研...
摘要:返回的綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器。同時,將第一個參數以外的其他參數,作為提供給原函數的預設參數,這也是基本的顆粒化基礎。 今天想談談一道前端面試題,我做面試官的時候經常喜歡用它來考察面試者的基礎是否扎實,以及邏輯、思維能力和臨場表現,題目是:模擬實現ES5中原生bind函數。也許這道題目已經不再新鮮,部分讀者也會有思路來解答。社區上關于原生bind的研...
閱讀 2433·2021-11-15 11:36
閱讀 1172·2019-08-30 15:56
閱讀 2243·2019-08-30 15:53
閱讀 1038·2019-08-30 15:44
閱讀 649·2019-08-30 14:13
閱讀 997·2019-08-30 10:58
閱讀 476·2019-08-29 15:35
閱讀 1293·2019-08-29 13:58