不看不知道,一看嚇一跳,已經整整一個月沒有更新 underscore 原始碼解讀系列文章了。前面我們已經完成了 Object ,Array,Collection 上的擴充套件方法的原始碼剖析,本文開始來解讀 Function 上的擴充套件方法。
完整的 underscore 原始碼解讀系列文章請移步 https://github.com/hanzichi/underscore-analysis,覺得還闊以的話,給個 star 鼓勵下樓主唄 ^_^
bind 簡介
Ok,今天要講的正是 bind。關於 bind,可以先移步樓主以前寫的文章 ECMAScript 5(ES5) 中 bind 方法簡介備忘,有個基本的概念。
bind() 方法會建立一個新函式,當這個新函式被呼叫時,它的 this 值是傳遞給 bind() 的第一個引數, 它的引數是 bind() 的其他引數和其原本的引數。
語法是這樣樣子的:
1 |
fun.bind(thisArg[, arg1[, arg2[, ...]]]) |
- thisArg 當繫結函式被呼叫時,該引數會作為原函式執行時的 this 指向。當使用 new 操作符呼叫繫結函式時,該引數無效。
- arg1, arg2, … (可選)當繫結函式被呼叫時,這些引數加上繫結函式本身的引數會按照順序作為原函式執行時的引數。
引數
bind 的第一個引數會作為原函式執行時的 this 指向,不多說;而第二個開始的引數是可選的,當繫結函式被呼叫時,這些引數加上繫結函式本身的引數會按照順序作為原函式執行時的引數。怎麼理解?
1 2 3 4 5 6 |
function fn(a, b, c) { return a + b + c; } var _fn = fn.bind(null, 10); var ans = _fn(20, 30); // 60 |
fn 函式需要三個引數,_fn 函式將 10 作為預設的第一個引數,所以只需要傳入兩個引數即可,如果你不小心傳入了三個引數,放心,也只會取前兩個。
1 2 3 4 5 6 |
function fn(a, b, c) { return a + b + c; } var _fn = fn.bind(null, 10); var ans = _fn(20, 30, 40); // 60 |
這有啥用呢?如果某些函式,前幾個引數已經 “內定” 了,我們便可以用 bind 返回一個新的函式。也就是說,bind() 能使一個函式擁有預設的初始引數。這些引數(如果有的話)作為 bind() 的第二個引數跟在 this 後面,之後它們會被插入到目標函式的引數列表的開始位置,傳遞給繫結函式的引數會跟在它們的後面。
1 2 3 4 5 6 7 8 9 10 11 |
function list() { return Array.prototype.slice.call(arguments); } var list1 = list(1, 2, 3); // [1, 2, 3] // Create a function with a preset leading argument var leadingThirtysevenList = list.bind(undefined, 37); var list2 = leadingThirtysevenList(); // [37] var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3] |
new
使用 bind 返回的結果還是個 function,是個 function 就可以被 new 運算子呼叫,那麼結果呢?規範中說的很清楚了,當使用 new 操作符呼叫繫結函式時,bind 的第一個引數無效。
1 2 3 4 5 6 7 |
function Person(name, age) { this.name = name; this.age = age; } var _Person = Person.bind({}); var p = new _Person('hanzichi', 30); // Person {name: "hanzichi", age: 30} |
一般我們不會去這麼用,但是如果要寫個 bind 的 polyfill(http://caniuse.com/#search=bind),還是需要考慮用 new 呼叫的情況。
我們也可以設定預設值(參考上一小節),原先提供的那些引數仍然會被前置到建構函式呼叫的前面。
1 2 3 4 5 6 7 |
function Person(name, age) { this.name = name; this.age = age; } var _Person = Person.bind(null, 'hanzichi'); var p = new _Person(30); // Person {name: "hanzichi", age: 30} |
配合 setTimeout
什麼時候容易丟失 this 指向?恩,setTimeout 是一個場景,很容易把 this 指向 window,當然,setInterval 也是一樣。當使用物件的方法時,需要 this 引用物件,你可能需要顯式地把 this 繫結到回撥函式以便繼續使用物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var canvas = { render: function() { this.update(); this.draw(); }, update: function() { // ... }, draw: function() { // ... } }; window.setInterval(canvas.render, 1000 / 60); |
用 canvas 寫特效或者做遊戲時經常會碰到類似的問題。上面的程式碼是有問題的,render 方法中的 this 其實被指向了 window!我們可以用 bind,顯式地把 this 繫結到回撥函式以便繼續使用該物件。
1 |
window.setInterval(canvas.render.bind(canvas), 1000); |
類似的情況還有 dom 的事件監聽,一不小心可能 this 就被指向了 dom 元素。可以參考下以前做 bigrender 時寫的這部分程式碼 https://github.com/hanzichi/hanzichi.github.io/blob/master/2016/bigrender/js/bigrender.js#L179-L184。
tip
bind 還能做一些有意思的事情。
通常來說,將一個類陣列轉為陣列,我們會用 slice(ie9- 不支援)。參考 https://github.com/hanzichi/underscore-analysis/issues/14
1 2 3 4 |
var slice = Array.prototype.slice; // slice.apply(arguments); // slice(arguments, 1); |
bind 能讓呼叫變的更加簡單。
1 2 3 4 5 6 7 8 |
// same as "slice" in the previous example var unboundSlice = Array.prototype.slice; var slice = Function.prototype.call.bind(unboundSlice); // ... slice(arguments); // slice(arguments, 1); |
再舉個類似的例子,比如說我們要新增事件到多個節點,for 迴圈當然沒有任何問題,我們還可以 “剽竊” forEach
方法:
1 2 3 |
Array.prototype.forEach.call(document.querySelectorAll('input[type="button"]'), function(el){ el.addEventListener('click', fn); }); |
更進一步,我們可以用 bind 將函式封裝的更好:
1 2 3 4 5 6 |
var unboundForEach = Array.prototype.forEach , forEach = Function.prototype.call.bind(unboundForEach); forEach(document.querySelectorAll('input[type="button"]'), function (el) { el.addEventListener('click', fn); }); |
同樣類似的,我們可以將 x.y(z) 變成 y(x,z) 的形式:
1 2 3 4 5 6 7 8 9 10 11 12 |
var obj = { num: 10, getCount: function() { return this.num; } }; var unboundBind = Function.prototype.bind , bind = Function.prototype.call.bind(unboundBind); var getCount = bind(obj.getCount, obj); console.log(getCount()); // 10 |
再舉個例子。每隔一秒在控制檯列印 1-5,看起來是道考察閉包的經典題目。
1 2 3 4 5 6 7 |
for(var i = 1; i <= 5; i++) { !function(i) { setTimeout(function() { console.log(i); }, i * 1000); }(i); } |
ES6 下能用 let
:
1 2 3 4 5 |
for(let i = 1; i <= 5; i++) { setTimeout(function() { console.log(i); }, i * 1000); } |
也可以用 bind,瞬間逼格提升:
1 2 3 |
for(var i = 1; i <= 5; i++) { setTimeout(console.log.bind(console, i), i * 1000); } |
Read more
關於 bind 的介紹就到這裡,下一篇文章將結合 underscore 來講講如何實現一個 bind 的 polyfill。
- Function.prototype.bind()
- ECMAScript 5(ES5) 中 bind 方法簡介備忘
- Javascript 中的 Bind ,Call 以及 Apply
- Javascript 中 bind() 方法的使用與實現
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!