當 bind 遇上 apply

,發表於2017-09-26

在《你不知道的 JavaScript(中卷)》之“非同步和效能” 2.4小節(P175),有一個 asyncify() 函式,該函式的目的是為了將任意被包裝函式變為非同步(無論原函式是非同步的還是同步的)。函式如下:

function asyncify(fn) {
    var orig_fn = fn,
        intv = setTimeout( function(){
            intv = null;
            if (fn) fn();
        }, 0 )
    ;

    fn = null;

    return function() {
        // 觸發太快,在`intv`計時器觸發來
        // 表示非同步回合已經過去之前?
        if (intv) {
            fn = orig_fn.bind.apply(
                orig_fn,
                // 將包裝函式的`this`加入`bind(..)`呼叫的
                // 引數,同時currying其他所有的傳入引數
                [this].concat( [].slice.call( arguments ) )
            );
        }
        // 已經是非同步
        else {
            // 呼叫原版的函式
            orig_fn.apply( this, arguments );
        }
    };
}

在函式的後半段,有這樣一些語句:

// firing too quickly, before `intv` timer has fired to
// indicate async turn has passed?
if (intv) {
  fn = orig_fn.bind.apply(
    orig_fn,
    // add the wrapper's `this` to the `bind(..)`
    // call parameters, as well as currying any
    // passed in parameters
    [this].concat([].slice.call(arguments))
  );
}

bind() 函式和 apply() 函式組合在一起的形式有些難以理解。好在在這個 issue 中有人向 @getify 大神提出了這個問題,@getify 是這麼回答的:

apply(..) 會呼叫上下文環境中的函式(在這裡就是 bind(..)),但是傳入的引數個數不定。

傳遞給 apply(..) 的第一個引數是繫結該函式呼叫(再次提示,在這裡就是 bind(..))的 this 物件。按照常規寫法,一般會這麼寫 orig_fn. bind(..),也就是說 bind(..)this 指向函式 orig_fn。因此當我們呼叫 apply(..) 的時候,orig_fn 就是這裡要用到的 this 物件。

接下來我們構造經由 apply(..) 傳遞給 bind(..) 的引數陣列。由於 bind(..) 的第一個引數為在 orig_fn 呼叫中用到的 this ,所以使用 [this] 將構造的引數陣列中的第一個引數設定為 this 。之後通過將 arguments 物件轉化為一個真正的陣列的形式新增外界傳入的其餘引數(呼叫 return function() {.. 返回的函式所獲得的引數),。然後將兩個陣列合並。除了傳遞給 bind(..) 的第一個引數,其餘的引數都會作為柯里化引數(預設值)。

bind(..) 呼叫(再次提示,經由 apply(..) 呼叫,由此可以以陣列的形式傳入不同數目的引數)的結果是一個經過硬繫結的、柯里化的函式,但是並沒有經過呼叫。我們將其儲存到變數 fn 裡面。

之後,這個經過硬繫結的、柯里化的函式通過 fn() 的形式呼叫即可,就像程式碼中用到的那樣 if(fn) fn()

在這裡的關鍵點是,bind 函式是通過 apply(..) 呼叫的,而 bind 自身所需要的 this 物件是一個函式(函式也是物件;在這裡即 orig_fn)。通常我們會這麼使用 bind

fn.bind(obj);

函式 fn 中的 this 會被繫結到物件 obj 上,而 bind 中的 this 的繫結物件是 fn 。注意這裡不要混淆。


無獨有偶,在《你不知道的 JavaScript(中卷)》之“非同步和效能”中 3.8.2 節,有一個 spread 函式:

function spread(fn) {
    return Function.apply.bind( fn, null );
}

該函式的應用場景是:

Promise.all(
    foo( 10, 20 )
)
.then(
    spread( function(x,y){
        console.log( x, y );    // 200 599
    } )
);

由於 then(..) 中的回撥函式的引數值是一個陣列,而 apply(..) 中的第二個引數也是陣列,所以這個 spread 函式的目的是對引數形式進行轉化。

bind(..) 返回的是一個經過柯里化的 apply 函式,該函式中的 this 指向 fn 函式(物件)。而第二個引數 null 則是傳遞給 apply 的第一個引數(apply 的呼叫者的 this 需要繫結的物件),null 意味著會繫結到全域性物件。

可以這麼理解: 按照常規寫法,傳給 then(..) 的回撥函式的呼叫方式是這樣的:

fn.apply(null, [..])

也就是說,apply 中的 this 指向的是 fn 物件(參見《你不知道的 JavaScript(上卷)》之 “this和物件原型”),而 Function.apply.bind( fn, null ) 是它的顯示繫結形式。 (完)

參考資料:

  1. 《你不知道的 JavaScript(上卷)》之 “this和物件原型”
  2. 《你不知道的 JavaScript(中卷)》之 “非同步與效能”
  3. https://github.com/getify/You-Dont-Know-JS/issues/381
  4. http://www.ituring.com.cn/article/125690

相關文章