摘要:第二階段被忽略的細節函數的屬性,用于表示函數的形參。第三階段被忽視的細節通過生成的構造函數。五本文涉及的知識點的用法的用法除操作符外的構造函數的用法下詭異的命名函數表達式技術六總結在這之前從來沒想過一個的會涉及這么多知識點,感謝給的啟發。
昨天邊參考es5-shim邊自己實現Function.prototype.bind,發現有不少以前忽視了的地方,這里就作為一個小總結吧。
一、Function.prototype.bind的作用其實它就是用來靜態綁定函數執行上下文的this屬性,并且不隨函數的調用方式而變化。
示例:
javascripttest("Function.prototype.bind", function(){ function orig(){ return this.x; }; var bound = orig.bind({x: "bind"}); equal(bound(), "bind", "invoke directly"); equal(bound.call({x: "call"}), "bind", "invoke by call"); equal(bound.apply({x: "apply"}), "bind", "invoke by apply"); });二、瀏覽器支持
Function.prototype.bind是ES5的API,所以坑爹的IE6/7/8均不支持,所以才有了自己實現的需求。
三、實現: 第一階段只要在百度搜Function.prototype.bind的實現,一般都能搜到這段代碼。
javascriptFunction.prototype.bind = Function.prototype.bind || function(){ var fn = this, presetArgs = [].slice.call(arguments); var context = presetArgs.shift(); return function(){ return fn.apply(context, presetArgs.concat([].slice.call(arguments))); }; };
它能恰好的實現Function.prototype.bind的功能定義,但通過看es5-shim源碼就會發現這種方式忽略了一些細節。
第二階段被忽略的細節1:函數的length屬性,用于表示函數的形參。
而第一階段的實現方式,調用bind所返回的函數的length屬性只能為0,而實際上應該為fn.length-presetArgs.length才對啊。所以es5-shim里面就通過bound.length=Math.max(fn.length-presetArgs.length, 0)的方式重設length屬性。
被忽略的細節2:函數的length屬性值是不可重寫的,使用現代瀏覽器執行下面的代碼驗證吧!
javascript test("function.length is not writable", function(){ function doStuff(){} ok(!Object.getOwnPropertyDescriptor(doStuff, "length").writable, "function.length is not writable"); });
因此es5-shim中的實現方式是無效的。既然不能修改length的屬性值,那么在初始化時賦值總可以吧,也就是定義函數的形參個數!于是我們可通過eval和new Function的方式動態定義函數來。
3. 被忽略的細節3:eval和new Function中代碼的執行上下文的區別。
簡單來說在函數體中調用eval,其代碼的執行上下文會指向當前函數的執行上下文;而new Function或Function中代碼的執行上下文將一直指向全局的執行上下文。
舉個栗子:
javascript var x = "global"; void function(){ var x = "local"; eval("console.log(x);"); // 輸出local (new Function("console.log(x);"))(); // 輸出global }();
因此這里我們要是用eval來動態定義函數了。
具體實現:
javascriptFunction.prototype.bind = Function.prototype.bind || function(){ var fn = this, presetArgs = [].slice.call(arguments); var context = presetArgs.shift(); var strOfThis = fn.toString(); // 函數反序列化,用于獲取this的形參 var fpsOfThis = /^function[^()]*((.*?))/i.exec(strOfThis)[1].trim().split(",");// 獲取this的形參 var lengthOfBound = Math.max(fn.length - presetArgs.length, 0); var boundArgs = lengthOfBound && fpsOfThis.slice(presetArgs.length) || [];// 生成bound的形參 eval("function bound(" + boundArgs.join(",") + "){" + "return fn.apply(context, presetArgs.concat([].slice.call(arguments)));" + "}"); return bound; };
現在成功設置了函數的length屬性了。不過還有些遺漏。
第三階段被忽視的細節4:通過Function.prototype.bind生成的構造函數。我在日常工作中沒這樣用過,不過這種情況確實需要考慮,下面我們先了解原生的Function.prototype.bind生成的構造函數的行為吧!請用現代化瀏覽器執行下面的代碼:
test("ctor produced by native Function.prototype.bind", function(){ var Ctor = function(x, y){ this.x = x; this.y = y; } var scope = {x: "scopeX", y: "scopeY"}; var Bound = Ctor.bind(scope); var ins = new Bound("insX", "insY"); ok(ins.x === "insX" && ins.y === "insY" && scope.x === "scopeX" && scope.y === "scopeY", "no presetArgs"); Bound = Ctor.bind(scope, "presetX"); ins = new Bound("insY", "insOther"); ok(ins.x === "presetX" && ins.y === "insY" && scope.x === "scopeX" && scope.y === "scopeY", "with presetArgs"); });
行為如下:
this屬性不會被綁定
預設實參有效
下面是具體實現
Function.prototype.bind = Function.prototype.bind || function(){ var fn = this, presetArgs = [].slice.call(arguments); var context = presetArgs.shift(); var strOfThis = fn.toString(); // 函數反序列化,用于獲取this的形參 var fpsOfThis = /^function[^()]*((.*?))/i.exec(strOfThis)[1].trim().split(",");// 獲取this的形參 var lengthOfBound = Math.max(fn.length - presetArgs.length, 0); var boundArgs = lengthOfBound && fpsOfThis.slice(presetArgs.length) || [];// 生成bound的形參 eval("function bound(" + boundArgs.join(",") + "){" + "if (this instanceof bound){" + "var self = new fn();" + "fn.apply(self, presetArgs.concat([].slice.call(arguments)));" + "return self;" + "}" + "return fn.apply(context, presetArgs.concat([].slice.call(arguments)));" + "}"); return bound; };
現在連構造函數作為使用方式都考慮到了,應該算是功德圓滿了吧!NO,上面的實現只是基礎的實現而已,并且隱藏一些bugs!
潛伏的bugs列表:
第四階段var self = new fn(),如果fn函數體存在實參為空則拋異常呢?
bound函數使用字符串拼接不利于修改和檢查,既不優雅又容易長蟲。
針對第三階段的問題,最后得到下面的實現方式
if(!Function.prototype.bind){ var _bound = function(){ if (this instanceof bound){ var ctor = function(){}; ctor.prototype = fn.prototype; var self = new ctor(); fn.apply(self, presetArgs.concat([].slice.call(arguments))); return self; } return fn.apply(context, presetArgs.concat([].slice.call(arguments))); } , _boundStr = _bound.toString(); Function.prototype.bind = function(){ var fn = this, presetArgs = [].slice.call(arguments); var context = presetArgs.shift();四、性能測試
var strOfThis = fn.toString(); // 函數反序列化,用于獲取this的形參 var fpsOfThis = /^function[^()]*((.*?))/i.exec(strOfThis)[1].trim().split(",");// 獲取this的形參 var lengthOfBound = Math.max(fn.length - presetArgs.length, 0); var boundArgs = lengthOfBound && fpsOfThis.slice(presetArgs.length) || [];// 生成bound的形參 // 通過函數反序列和字符串替換動態定義函數 var bound = eval("(0," + _boundStr.replace("function()", "function(" + boundArgs.join(",") + ")") + ")"); return bound; };
// 分別用impl1,impl2,impl3,impl4代表上述四中實現方式 var start, end, orig = function(){}; start = (new Date()).getTime(); Function.prototype.bind = impl1; for(var i = 0, len = 100000; i++ < len;){ orig.bind({})(); } end = (new Date()).getTime(); console.log((end-start)/1000); // 輸出1.387秒 start = (new Date()).getTime(); Function.prototype.bind = impl2; for(var i = 0, len = 100000; i++ < len;){ orig.bind({})(); } end = (new Date()).getTime(); console.log((end-start)/1000); // 輸出4.013秒 start = (new Date()).getTime(); Function.prototype.bind = impl3; for(var i = 0, len = 100000; i++ < len;){ orig.bind({})(); } end = (new Date()).getTime(); console.log((end-start)/1000); // 輸出4.661秒 start = (new Date()).getTime(); Function.prototype.bind = impl4; for(var i = 0, len = 100000; i++ < len;){ orig.bind({})(); } end = (new Date()).getTime(); console.log((end-start)/1000); // 輸出4.485秒
由此得知運行效率最快是第一階段的實現,而且證明通過eval動態定義函數確實耗費資源啊!!!
當然我們可以通過空間換時間的方式(Momoized技術)來緩存bind的返回值來提高性能,經測試當第四階段的實現方式加入緩存后性能測試結果為1.456,性能與第一階段的實現相當接近了。
eval的用法
new Function的用法
除new操作符外的構造函數的用法
JScript(IE6/7/8)下詭異的命名函數表達式
Momoized技術
六、總結在這之前從來沒想過一個Function.prototype.bind的polyfill會涉及這么多知識點,感謝es5-shim給的啟發。
我知道還會有更優雅的實現方式,歡迎大家分享出來!一起面對javascript的痛苦與快樂!
如果您覺得本文的內容有趣就掃一下吧!捐贈互勉!
??
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91524.html
摘要:昨天被人問到的的作用是什么這個倒還能回答出來,之后返回一個新的函數,這個函數可以保持傳遞的上下文。沒有完全實現規定的。比如規定了的和行為。 https://friskfly.github.io/2016/03/24/about-function-bind-in-js/ 昨天被人問到js的bind的作用是什么? 這個倒還能回答出來,bind 之后返回一個新的函數,這個函數可以保持傳遞的t...
摘要:綁定函數被調用時,也接受預設的參數提供給原函數。一個綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器。 說明 bind()方法創建一個新的函數, 當被調用時,將其this關鍵字設置為提供的值,在調用新函數時,在任何提供之前提供一個給定的參數序列。 語法 fun.bind(thisArg[, arg1[, arg2[, ...]]]) 參數 thisArg 當綁定函數被調用時...
摘要:被調用時,等參數將置于實參之前傳遞給被綁定的方法。它返回由指定的值和初始化參數改造的原函數拷貝。一個綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器。其實這個思路也是庫如何實現繼承的方法。他的函數如下最后一步是將的指回。 update: 2018-06-08 原文鏈接 為什么要自己去實現一個bind函數? bind()函數在 ECMA-262 第五版才被加入;它可能無法在所...
摘要:返回的綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器。同時,將第一個參數以外的其他參數,作為提供給原函數的預設參數,這也是基本的顆粒化基礎。 今天想談談一道前端面試題,我做面試官的時候經常喜歡用它來考察面試者的基礎是否扎實,以及邏輯、思維能力和臨場表現,題目是:模擬實現ES5中原生bind函數。也許這道題目已經不再新鮮,部分讀者也會有思路來解答。社區上關于原生bind的研...
摘要:返回的綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器。同時,將第一個參數以外的其他參數,作為提供給原函數的預設參數,這也是基本的顆粒化基礎。 今天想談談一道前端面試題,我做面試官的時候經常喜歡用它來考察面試者的基礎是否扎實,以及邏輯、思維能力和臨場表現,題目是:模擬實現ES5中原生bind函數。也許這道題目已經不再新鮮,部分讀者也會有思路來解答。社區上關于原生bind的研...
閱讀 1988·2021-11-19 09:40
閱讀 1931·2021-09-28 09:36
閱讀 2279·2021-09-22 10:02
閱讀 2724·2019-08-30 14:00
閱讀 1948·2019-08-29 15:31
閱讀 2893·2019-08-29 15:11
閱讀 2905·2019-08-29 13:04
閱讀 1080·2019-08-27 10:55