【進階3-4期】深度解析bind原理、使用場景及模擬實現
本週的主題是 this全面解析 ,本計劃一共28期, 每期重點攻克一個面試重難點 ,如果你還不瞭解本進階計劃,文末點選檢視全部文章。
如果覺得本系列不錯,歡迎點贊、評論、轉發,您的支援就是我堅持的最大動力。
bind()
bind()
方法會建立一個新函式,當這個新函式被呼叫時,它的 this
值是傳遞給 bind()
的第一個引數,傳入bind方法的第二個以及以後的引數加上繫結函式執行時本身的引數按照順序作為原函式的引數來呼叫原函式。bind返回的繫結函式也能使用 new
操作符建立物件:這種行為就像把原函式當成構造器。提供的 this
值被忽略,同時呼叫時的引數被提供給模擬函式。(來自參考1)
語法: fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind
方法與 call / apply
最大的不同就是前者返回一個繫結上下文的 函式 ,而後兩者是 直接執行 了函式。
來個例子說明下
var value = 2; var foo = { value: 1 }; function bar(name, age) { return { value: this.value, name: name, age: age } }; bar.call(foo, "Jack", 20); // 直接執行了函式 // {value: 1, name: "Jack", age: 20} var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一個函式 bindFoo1(); // {value: 1, name: "Jack", age: 20} var bindFoo2 = bar.bind(foo, "Jack"); // 返回一個函式 bindFoo2(20); // {value: 1, name: "Jack", age: 20} 複製程式碼
通過上述程式碼可以看出 bind
有如下特性:
this
使用場景
1、業務場景
經常有如下的業務場景
var nickname = "Kitty"; function Person(name){ this.nickname = name; this.distractedGreeting = function() { setTimeout(function(){ console.log("Hello, my name is " + this.nickname); }, 500); } } var person = new Person('jawil'); person.distractedGreeting(); //Hello, my name is Kitty 複製程式碼
這裡輸出的 nickname
是全域性的,並不是我們建立 person
時傳入的引數,因為 setTimeout
在全域性環境中執行(不理解的檢視【進階3-1期】),所以 this
指向的是 window
。
這邊把 setTimeout
換成非同步回撥也是一樣的,比如介面請求回撥。
解決方案有下面兩種。
解決方案1:快取 this
值
var nickname = "Kitty"; function Person(name){ this.nickname = name; this.distractedGreeting = function() { var self = this; // added setTimeout(function(){ console.log("Hello, my name is " + self.nickname); // changed }, 500); } } var person = new Person('jawil'); person.distractedGreeting(); // Hello, my name is jawil 複製程式碼
解決方案2:使用 bind
var nickname = "Kitty"; function Person(name){ this.nickname = name; this.distractedGreeting = function() { setTimeout(function(){ console.log("Hello, my name is " + this.nickname); }.bind(this), 500); } } var person = new Person('jawil'); person.distractedGreeting(); // Hello, my name is jawil 複製程式碼
完美!
2、驗證是否是陣列
【進階3-3期】介紹了 call
的使用場景,這裡重新回顧下。
function isArray(obj){ return Object.prototype.toString.call(obj) === '[object Array]'; } isArray([1, 2, 3]); // true // 直接使用 toString() [1, 2, 3].toString(); // "1,2,3" "123".toString(); // "123" 123.toString(); // SyntaxError: Invalid or unexpected token Number(123).toString(); // "123" Object(123).toString(); // "123" 複製程式碼
可以通過 toString()
來獲取每個物件的型別,但是不同物件的 toString()
有不同的實現,所以通過 Object.prototype.toString()
來檢測,需要以 call() / apply()
的形式來呼叫,傳遞要檢查的物件作為第一個引數。
另一個 驗證是否是陣列 的方法,這個方案的 優點 是可以直接使用改造後的 toStr
。
var toStr = Function.prototype.call.bind(Object.prototype.toString); function isArray(obj){ return toStr(obj) === '[object Array]'; } isArray([1, 2, 3]); // true // 使用改造後的 toStr toStr([1, 2, 3]); // "[object Array]" toStr("123"); // "[object String]" toStr(123); // "[object Number]" toStr(Object(123)); // "[object Number]" 複製程式碼
上面方法首先使用 Function.prototype.call
函式指定一個 this
值,然後 .bind
返回一個新的函式,始終將 Object.prototype.toString
設定為傳入引數。其實等價於 Object.prototype.toString.call()
。
這裡有一個 前提 是 toString()
方法 沒有被覆蓋
Object.prototype.toString = function() { return ''; } isArray([1, 2, 3]); // false 複製程式碼
3、柯里化(curry)
只傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。
可以一次性地呼叫柯里化函式,也可以每次只傳一個引數分多次呼叫。
var add = function(x) { return function(y) { return x + y; }; }; var increment = add(1); var addTen = add(10); increment(2); // 3 addTen(2); // 12 add(1)(2); // 3 複製程式碼
這裡定義了一個 add
函式,它接受一個引數並返回一個新的函式。呼叫 add
之後,返回的函式就通過閉包的方式記住了 add
的第一個引數。所以說 bind
本身也是閉包的一種使用場景。
模擬實現
bind()
函式在 ES5 才被加入,所以並不是所有瀏覽器都支援, IE8
及以下的版本中不被支援,如果需要相容可以使用 Polyfill 來實現。
首先我們來實現以下四點特性:
this
模擬實現第一步
對於第 1 點,使用 call / apply
指定 this
。
對於第 2 點,使用 return
返回一個函式。
結合前面 2 點,可以寫出第一版,程式碼如下:
// 第一版 Function.prototype.bind2 = function(context) { var self = this; // this 指向呼叫者 return function () { // 實現第 2點 return self.apply(context); // 實現第 1 點 } } 複製程式碼
測試一下
// 測試用例 var value = 2; var foo = { value: 1 }; function bar() { return this.value; } var bindFoo = bar.bind2(foo); bindFoo(); // 1 複製程式碼
模擬實現第二步
對於第 3 點,使用 arguments
獲取引數陣列並作為 self.apply()
的第二個引數。
對於第 4 點,獲取返回函式的引數,然後同第3點的引數合併成一個引數陣列,並作為 self.apply()
的第二個引數。
// 第二版 Function.prototype.bind2 = function (context) { var self = this; // 實現第3點,因為第1個引數是指定的this,所以只擷取第1個之後的引數 // arr.slice(begin); 即 [begin, end] var args = Array.prototype.slice.call(arguments, 1); return function () { // 實現第4點,這時的arguments是指bind返回的函式傳入的引數 // 即 return function 的引數 var bindArgs = Array.prototype.slice.call(arguments); return self.apply( context, args.concat(bindArgs) ); } } 複製程式碼
測試一下:
// 測試用例 var value = 2; var foo = { value: 1 }; function bar(name, age) { return { value: this.value, name: name, age: age } }; var bindFoo = bar.bind2(foo, "Jack"); bindFoo(20); // {value: 1, name: "Jack", age: 20} 複製程式碼
模擬實現第三步
到現在已經完成大部分了,但是還有一個難點, bind
有以下一個特性
一個繫結函式也能使用new操作符建立物件:這種行為就像把原函式當成構造器,提供的 this 值被忽略,同時呼叫時的引數被提供給模擬函式。
來個例子說明下:
var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; var bindFoo = bar.bind(foo, 'Jack'); var obj = new bindFoo(20); // undefined // Jack // 20 obj.habit; // shopping obj.friend; // kevin 複製程式碼
上面例子中,執行結果 this.value
輸出為 undefined
,這不是全域性 value
也不是 foo
物件中的 value
,這說明 bind
的 this
物件失效了, new
的實現中生成一個新的物件,這個時候的 this
指向的是 obj
。(【進階3-1期】有介紹new的實現原理,下一期也會重點介紹)
這裡可以通過修改返回函式的原型來實現,程式碼如下:
// 第三版 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); // 註釋1 return self.apply( this instanceof fBound ? this : context, args.concat(bindArgs) ); } // 註釋2 fBound.prototype = this.prototype; return fBound; } 複製程式碼
- 註釋1:
- 當作為建構函式時,this 指向例項,此時
this instanceof fBound
結果為true
,可以讓例項獲得來自繫結函式的值,即上例中例項會具有habit
屬性。 - 當作為普通函式時,this 指向
window
,此時結果為false
,將繫結函式的 this 指向context
- 當作為建構函式時,this 指向例項,此時
- 註釋2: 修改返回函式的
prototype
為繫結函式的prototype
,例項就可以繼承繫結函式的原型中的值,即上例中obj
可以獲取到bar
原型上的friend
。
注意:這邊涉及到了原型、原型鏈和繼承的知識點,可以看下我之前的文章。
ofollow,noindex">JavaScript/">JavaScript常用八種繼承方案
模擬實現第四步
上面實現中 fBound.prototype = this.prototype
有一個缺點,直接修改 fBound.prototype
的時候,也會直接修改 this.prototype
。
來個程式碼測試下:
// 測試用例 var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; var bindFoo = bar.bind2(foo, 'Jack'); // bind2 var obj = new bindFoo(20); // 返回正確 // undefined // Jack // 20 obj.habit; // 返回正確 // shopping obj.friend; // 返回正確 // kevin obj.__proto__.friend = "Kitty"; // 修改原型 bar.prototype.friend; // 返回錯誤,這裡被修改了 // Kitty 複製程式碼
解決方案是用一個空物件作為中介,把 fBound.prototype
賦值為空物件的例項(原型式繼承)。
var fNOP = function () {};// 建立一個空物件 fNOP.prototype = this.prototype; // 空物件的原型指向繫結函式的原型 fBound.prototype = new fNOP();// 空物件的例項賦值給 fBound.prototype 複製程式碼
所以第四版就OK啦,程式碼如下:
// 第四版,已通過測試用例 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply( this instanceof fNOP ? this : context, args.concat(bindArgs) ); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; } 複製程式碼
模擬實現第五步
到這裡其實已經差不多了,但有一個問題是呼叫 bind
的不是函式,這時候需要丟擲異常。
if (typeof this !== "function") { throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); } 複製程式碼
所以完整版模擬實現程式碼如下:
// 第五版 Function.prototype.bind2 = function (context) { if (typeof this !== "function") { throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); } var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; } 複製程式碼
【進階3-2期】思考題解
// 1、賦值語句是右執行的,此時會先執行右側的物件 var obj = { // 2、say 是立即執行函式 say: function() { function _say() { // 5、輸出 window console.log(this); } // 3、編譯階段 obj 賦值為 undefined console.log(obj); // 4、obj是 undefined,bind 本身是 call實現, // 【進階3-3期】:call 接收 undefined 會繫結到 window。 return _say.bind(obj); }(), }; obj.say(); 複製程式碼
【進階3-3期】思考題解
call
的模擬實現如下,那有沒有什麼問題呢?
Function.prototype.call = function (context) { context = context || window; context.fn = this; var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } var result = eval('context.fn(' + args +')'); delete context.fn; return result; } 複製程式碼
當然是有問題的,其實這裡假設 context
物件本身沒有 fn
屬性,這樣肯定不行,我們必須保證 fn
屬性的唯一性。
ES3下模擬實現
解決方法也很簡單,首先判斷 context
中是否存在屬性 fn
,如果存在那就隨機生成一個屬性 fnxx
,然後迴圈查詢 context
物件中是否存在屬性 fnxx
。如果不存在則返回最終值。
一種 迴圈方案 實現程式碼如下:
function fnFactory(context) { var unique_fn = "fn"; while (context.hasOwnProperty(unique_fn)) { unique_fn = "fn" + Math.random(); // 迴圈判斷並重新賦值 } return unique_fn; } 複製程式碼
一種 遞迴方案 實現程式碼如下:
function fnFactory(context) { var unique_fn = "fn" + Math.random(); if(context.hasOwnProperty(unique_fn)) { // return arguments.callee(context); ES5 開始禁止使用 return fnFactory(context); // 必須 return } else { return unique_fn; } } 複製程式碼
模擬實現完整程式碼如下:
function fnFactory(context) { var unique_fn = "fn"; while (context.hasOwnProperty(unique_fn)) { unique_fn = "fn" + Math.random(); // 迴圈判斷並重新賦值 } return unique_fn; } Function.prototype.call = function (context) { context = context || window; var fn = fnFactory(context); // added context[fn] = this; // changed var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } var result = eval('context[fn](' + args +')'); // changed delete context[fn]; // changed return result; } // 測試用例在下面 複製程式碼
ES6下模擬實現
ES6有一個新的基本型別 Symbol
,表示 獨一無二 的值,用法如下。
const symbol1 = Symbol(); const symbol2 = Symbol(42); const symbol3 = Symbol('foo'); console.log(typeof symbol1); // "symbol" console.log(symbol3.toString()); // "Symbol(foo)" console.log(Symbol('foo') === Symbol('foo')); // false 複製程式碼
不能使用 new
命令,因為這是基本型別的值,不然會報錯。
new Symbol(); // TypeError: Symbol is not a constructor 複製程式碼
模擬實現完整程式碼如下:
Function.prototype.call = function (context) { context = context || window; var fn = Symbol(); // added context[fn] = this; // changed let args = [...arguments].slice(1); let result = context[fn](...args); // changed delete context[fn]; // changed return result; } // 測試用例在下面 複製程式碼
測試用例在這裡:
// 測試用例 var value = 2; var obj = { value: 1, fn: 123 } function bar(name, age) { console.log(this.value); return { value: this.value, name: name, age: age } } bar.call(null); // 2 console.log(bar.call(obj, 'kevin', 18)); // 1 // {value: 1, name: "kevin", age: 18} console.log(obj); // {value: 1, fn: 123} 複製程式碼
擴充套件一下
有兩種方案可以判斷物件中是否存在某個屬性。
var obj = { a: 2 }; Object.prototype.b = function() { return "hello b"; } 複製程式碼
- 1、
in
操作符
in
操作符會檢查屬性是否存在物件及其 [[Prototype]]
原型鏈中。
("a" in obj);// true ("b" in obj);// true 複製程式碼
- 2、
Object.hasOwnProperty(...)
方法
hasOwnProperty(...)
只會檢查屬性是否存在物件中, 不會 向上檢查其原型鏈。
obj.hasOwnProperty("a");//true obj.hasOwnProperty("b");//false 複製程式碼
注意以下幾點:
- 1、看起來
in
操作符可以檢查容器內是否有某個值,實際上檢查的是某個 屬性名 是否存在。對於陣列來說,4 in [2, 4, 6]
結果返回false
,因為[2, 4, 6]
這個陣列中包含的屬性名是0,1,2
,沒有4
。 - 2、所有普通物件都可以通過
Object.prototype
的委託來訪問hasOwnProperty(...)
,但是對於一些特殊物件(Object.create(null)
建立)沒有連線到Object.prototype
,這種情況必須使用Object.prototype.hasOwnProperty.call(obj, "a")
,顯示繫結到obj
上。 又是一個call
的用法 。
本期思考題
用 JS 實現一個無限累加的函式 add
,示例如下:
add(1); // 1 add(1)(2);// 3 add(1)(2)(3); // 6 add(1)(2)(3)(4); // 10 // 以此類推 複製程式碼
參考
不用 call 和 apply 方法模擬實現 ES5 的 bind 方法
MDN 之 Function.prototype.bind()
進階系列目錄
- 【進階1期】 呼叫堆疊
- 【進階2期】 作用域閉包
- 【進階3期】 this全面解析
- 【進階4期】 深淺拷貝原理
- 【進階5期】 原型Prototype
- 【進階6期】 高階函式
- 【進階7期】 事件機制
- 【進階8期】 Event Loop原理
- 【進階9期】 Promise原理
- 【進階10期】Async/Await原理
- 【進階11期】防抖/節流原理
- 【進階12期】模組化詳解
- 【進階13期】ES6重難點
- 【進階14期】計算機網路概述
- 【進階15期】瀏覽器渲染原理
- 【進階16期】webpack配置
- 【進階17期】webpack原理
- 【進階18期】前端監控
- 【進階19期】跨域和安全
- 【進階20期】效能優化
- 【進階21期】VirtualDom原理
- 【進階22期】Diff演算法
- 【進階23期】MVVM雙向繫結
- 【進階24期】Vuex原理
- 【進階25期】Redux原理
- 【進階26期】路由原理
- 【進階27期】VueRouter原始碼解析
- 【進階28期】ReactRouter原始碼解析
交流
進階系列文章彙總: github.com/yygmind/blo… ,內有優質前端資料,覺得不錯點個star。
我是木易楊,網易高階前端工程師,跟著我 每週重點攻克一個前端面試重難點 。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!
