深入理解bind函數

轉自:http://davidshariff.com/blog/borrowing-methods-in-javascript/

翻譯:https://www.jb51.net/article/90126.htm

Borrowing Methods in JavaScript

In JavaScript, sometimes it’s desirable to reuse a function or method on a different object other than the object or prototype it was defined on. By using call(), apply() and bind(), we can easily borrow methods from different objects without having to inherit from them – a useful tool in a professional JavaScript developer’s toolbox.

Prerequisite

This article assumes you already have a working knowledge of call()apply() and bind() and their differences.

Methods from native prototypes

In JavaScript, almost everything you touch is an object except for primitives such as string, number and booleans which are immutable. An Array is a type of object suited to traversing and mutating an ordered list of data, and comes with useful methods on its prototype such as slice, join, push and pop.

A common use case we see with objects is to “borrow” methods from an array as they are both list type data structures. The most common borrowed method is Array.prototype.slice.

function myFunc() {

    // error, arguments is an array like object, not a real array
    arguments.sort();

    // "borrow" the Array method slice from its prototype, which takes an array like object (key:value)
    // and returns a real array
    var args = Array.prototype.slice.call(arguments);

    // args is now a real Array, so can use the sort() method from Array
    args.sort();

}

myFunc('bananas', 'cherries', 'apples');

Borrowing methods is possible due to call and apply allowing us to invoke functions in a different context and is a great way to reuse existing functionality without having to make one object extend from another. An array actually has many methods defined on its prototype that are considered generically reusable, two further examples of which are join and filter:

// takes a string "abc" and produces "a|b|c
Array.prototype.join.call('abc', '|');

// takes a string and removes all non vowels
Array.prototype.filter.call('abcdefghijk', function(val) {
    return ['a', 'e', 'i', 'o', 'u'].indexOf(val) !== -1;
}).join('');

思考:爲什麼字符串對象可以調用array原型上的方法,難道字符串也是類數組對象嗎?
     受到https://www.cnblogs.com/lyhero11/p/5244834.html的啓發,給了類數組一個概念
    【任何一個具有length屬性以及非負整數(可以不連續)屬性的對象】.因此string也是一種類數組對象,自        
     然可以調用array源性方法。

擴展:var o ={"0":"a",1:"b"};o.length=2;Array.prototype.slice.call(o)//輸出["a","b"];

As you can see it’s not just objects who benefit by borrowing methods from an array, strings can too. However, since generic methods are defined on the prototype having to write String.prototype or Array.prototype each time we want to borrow a method is verbose and fast becomes tiresome. An alternative, valid approach that is effectively the same is to use Literals.

Borrowing Methods using Literals

A Literal is is a syntactic language construct that follows the rules of JavaScript and is explained by MDNas:

You use literals to represent values in JavaScript. These are fixed values, not variables, that you literally provide in your script

 

Literals allow us to access prototype methods in short form:

[].slice.call(arguments);
[].join.call('abc', '|');
''.toUpperCase.call(['lowercase', 'words', 'in', 'a', 'sentence']).split(',');

This is less verbose but still looks a bit ugly having to operate on [] and "" directly to borrow their methods. We can shorten this even further by storing a reference to the literal and its method as a variable:

var slice = [].slice; 
slice.call(arguments);

var join = [].join;
join.call('abc', '|');

var toUpperCase = ''.toUpperCase;
toUpperCase.call(['lowercase', 'words', 'in', 'a', 'sentence']).split(',');

With a reference to the borrowed method, we can simply invoke it using call() and enjoy all the benefits of reusability. Continuing in the spirit of reducing verboseness, let’s see if we can borrow a method and not have to write call() or apply() each time we want to invoke it:

var slice = Function.prototype.call.bind(Array.prototype.slice);
slice(arguments);

var join = Function.prototype.call.bind(Array.prototype.join);
join('abc', '|');

var toUpperCase = Function.prototype.call.bind(String.prototype.toUpperCase);
toUpperCase(['lowercase', 'words', 'in', 'a', 'sentence']).split(',');

As you can see, you can now statically bind “borrowed” methods from many different native prototypes using Function.prototype.call.bind, but how does var slice = Function.prototype.call.bind(Array.prototype.slice) actually work?

Understanding Function.prototype.call.bind

Function.prototype.call.bind looks a little complicated at first but it’s super useful to understand how it works.

  • Function.prototype.call is a reference to “call” a function and set its “this” value to be used inside said function.
  • Remember “bind” returns a new function that always remembers its “this” value. Therefore, .bind(Array.prototype.slice)returns a new function with its “this” permanently set to the Array.prototype.slice function.

By combining both the above, we now have new function that will invoke the “call” function with its “this” bounded to the “slice” function. Invoking slice() simply becomes a reference to the previous bounded method.

Methods from custom objects

Inheritance is great but often developers resort to it when they want to reuse some common functionality between objects or modules. If you’re using inheritance solely for code reuse you’re probably doing something wrong, and in most situations simply borrowing a method will get you a long way.

So far we’ve only talked about borrowing native methods, but it’s possible to borrow any method! Take the following code to calculate a players score game score:

var scoreCalculator = {
    getSum: function(results) {
        var score = 0;
        for (var i = 0, len = results.length; i < len; i++) {
            score = score + results[i];
        }
        return score;
    },
    getScore: function() {
        return scoreCalculator.getSum(this.results) / this.handicap;
    }
};

var player1 = {
    results: [69, 50, 76],
    handicap: 8
};

var player2 = {
    results: [23, 4, 58],
    handicap: 5
};

var score = Function.prototype.call.bind(scoreCalculator.getScore);

// Score: 24.375
console.log('Score: ' + score(player1));

// Score: 17
console.log('Score: ' + score(player2));

Although the above example is contrived, it's easy to see just like native methods how user defined methods can be easily borrowed too.

Wrapping up

Call, bind and apply allow us to change the way functions are invoked and are typically used when borrowing a function. The majority of developers are familiar with borrowing native methods but less so with user defined.

In the last few years functional programming in JavaScript has been on the rise and I expect short cutting how to borrow methods using Function.prototype.call.bind will become more common.

補充理解:Function.prototype.call.bind

下面是call實現的僞代碼

Function.prototype.call = function(thisArg, arg1, arg2, ...) {
  /*** 注意:this指向調用call的那個對象或函數 ***/

  // 1. 調用內部的IsCallable(this)檢查是否可調用,返回false則拋TypeError
  if (![[IsCallable]](this)) throw new TypeError()
  
  // 2. 創建一個空列表
  // 3. 將arg1及後面的入參保存到argList中
  var argList = [].slice.call(arguments, 1)

  // 4. 調用內部的[[Call]]來執行targetFn函數,newThis爲執行函數的this綁定
  return targetFn.[[Call]] (this = newThis, arguments = args);//[[Call]]是function的內部方法
}

Function.prototype.call.bind(Array.prototype.slice)也就是僞代碼實現的function經過bind,也就將function中的this設置爲了Array.prototype.slice。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章