44

I'm reading a book about writing JavaScript frameworks and found this code snippet. But I don't understand how it works, especially the bind.bind usage? Does anyone have a clue?

var bind = Function.prototype.bind;
var apply = bind.bind(bind.apply);
var fn = apply([].concat);
var a = [1, 2, 3], b = [4, [5, 6], 7];
fn(a, b);
//output [1, 2, 3, 4, 5, 6, 7]

5 Answers 5

36

This takes me back to the days of solving and expanding equations.

1 . First, lets expand the first apply function:

var bind = Function.prototype.bind;
var apply = bind.bind(bind.apply);
var fn = apply([].concat);

Transforms to:

var apply = Function.prototype.bind.bind(Function.prototype.bind.apply);
var fn = apply([].concat)

2 . Secondly, we expand the fn function:

var fn = Function.prototype.bind.bind(Function.prototype.bind.apply)([].concat);

3 . We now invent a js algebra rule and replace the bind.bind...() invocation with the call invocation.

Actually we implement a replacement base on the notion that:

someFunction.bind(arg1)(arg2) <==> someFunction.call(arg1, arg2)

Therefore we can replace that and get:

var fn = Function.prototype.bind.call(Function.prototype.bind.apply, [].concat);

4 . For our second js algebra rule we devise that:

someFn.bind.call(target, ...) <==> target.bind(...).

someFn is not important here because we don't call bind() on it. We invoke call on the bind - replacing the this that was someFn and therefor it is replaced with target.

Therefore we replace the bind.call(target) with the target.bind alternative

var fn = Function.prototype.bind.apply.bind([].concat) 

5 . If the last permutation was also doing invocation () we could have done a replace like:

fn([1, 2], [3, 4]) <==> [].concat.apply([1, 2], [3, 4])

But we have only the bind without the invocation which we can replace and is equivalent to:

var fn = function (arg1, arg2) { 
    return [].concat.apply(arg1, arg2); 
}
// instead arg1 and arg2 we could use more accurate arguments logic also.

Final Result

var fn = Function.prototype.bind.apply.bind([].concat) 

// or

var fn = function (arg1, arg2) { 
    return [].concat.apply(arg1, arg2); 
}

The fn function takes the concat function and lets us us it in a functional style, without calling it from an object. Instead of concat binded to this of the caller, fn applies it on arg1 as this and arg2 as the other params to concatenation to arg1.

fn([1, 2], [3, [5, 6], 4])
// [1, 2, 3, 5, 6, 4]
Sign up to request clarification or add additional context in comments.

2 Comments

so anyfunction.apply.call([].concat,[1,2],[3,[4,5],6])<==>[].concat.apply([1,2],[3,[4,5],6])<==>[1,2].concat(3,[4,5],6).Nice algebra!
Thanks. That is basically it, yes. The last transformation you mentioned although - [].concat.apply(arg1, ...) <==> arg1.concat(...) is possible when the target has the same base function. For instance, the argument object that is like an array but does not have array functions. I relied on the fact that bind, call and apply exists on any function for those transformations.
12

Because Function.prototype.bind is itself a function, it inherits itself as a method.

Typically, bind is called as an instance method of a particular function, but we can rebind it to Function.prototype.apply and return a higher-order function.

A less terse way of writing this would be:

function apply (fn) {
  return function (a, b) {
    return fn.apply(a, b)
  }
}

Comments

3

We have to think a little about what bind actually does behind the scenes. To a rough approximation, it works like this:

function bind(fn, ...bound) {
    return (...other) => this.call(fn, ...bound, ...other);
}

(Remember that the lexical this refers to the function on which bind is called.) So taking

apply = bind.bind(bind.apply);

and manually expanding bind ourselves, we get:

apply = (...other) => bind.call(bind.apply, ...other);

We're only interested in passing a single argument, which is a function. Hopefully this also means that its apply property is the same as that of bind:

apply = (fn) => bind.call(fn.apply, fn);

But bind is (hopefully) itself also on fn.apply's prototype, so we can simplify further to:

apply = (fn) => fn.apply.bind(fn);

We're now in a position to expand bind again:

apply = (fn) => (...other) => fn.apply.call(fn, ...other);

This time, we want two arguments, and the call to call can also be simplified:

apply = (fn) => (obj, args) => fn.apply(obj, args);

We're now in a position to invoke apply on [].concat:

fn = (obj, args) => [].concat.apply(obj, args);

obj needs to be an array for this to work, so this simplifies to:

fn = (obj, args) => obj.concat(...args);

And in the provided example we end up with

[1, 2, 3].concat(4, [5, 6], 7)

which returns the expected result.

Comments

2

bind() method creates a new instance of the function to the value that was passed into bind().

Here we are calling Function.prototype.bind.apply(Function.prototype, [null].concat(arguments)); by breaking into separate executable code units.

Objective is to call apply; to pass the original function as the first param and then array of arguments.

This codes has different variables for each process involved to achieve bind with dynamic params.

var bind=Function.prototype.bind; 
  // the bind function is assigned to a var

var apply=bind.bind(bind.apply); 
  // a new function apply is created which is nothing but bind.apply

var fn=apply([].concat); 
  // the apply function is defined and now we have final function 
   that will concat array.

Last two lines are for executing the fn function by passing two parameters

var a=[1,2,3],b=[4,[5,6],7]; fn(a,b);

Comments

1

explanation via example

The technique below can be used to "uncurry this", which means to turn a method into a function where it's this is provided as the first argument instead of this being implicit based on what object a method is called on.

const map = new Map()
const set = Map.prototype.set
// ------------------------------
{ // STEP 0
  map.set(2, 'b') // normal method call
  // => depends on the `Map.prototype.set` not being corrupted to work
  // e.g. Map.prototype.set = undefined // breaks this line of code
}
// ------------------------------
{ // STEP 1
  // just as `map` in e.g. `map.set(...)`
  set.call(map, 2, 'b') // can pass `this` as param now (="uncurry this")
  // the first arg `map` becomes `this` of `set(...)`
  // => but depends on `Function.prototype.call` not being corrupted to work
  // e.g. Function.prototype.call = undefined // breaks this line of code
}
// ------------------------------
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
const call = Function.prototype.call
{ // STEP 2
  // just as `map` in e.g. `set.call(map, ...)`
  const set_call = call.bind(set) // think: "set.call(...)" in one function
  // the first arg `set` later becomes `this` of `set_call(...)`
  // to make `set.call` a **"single set_call function"**
  // => is the same as step 1, but we just use `bind`
  set_call(map, 2, 'b') // can pass `this` as a param now
  // => almost perfect, but every time we want to make another
  //    uncurried function e.g. `xyz_call(...)`
  //    instead of `xyz.call(...)
  //    we need to call `call.bind` again, which depends on prototypes
  // e.g. Function.prototype.bind = undefined
  // => breaks our "uncurried function making technique"
}
// ------------------------------
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind
const bind = Function.prototype.bind
{ // STEP 3
  // just as `set` in e.g. `call.bind(set)` to get `set_call`
  const call_bind = bind.bind(call) // corrupt prototype has no effect later
  // the first arg `call` later becomes `this` of `call_bind(...)`
  // to make `call.bind` a **"single call_bind function"**
  // =>
  // we make a "custom bound `bind`" with the first argument of `bind`
  // already set to `call`, hence `bind.bind(call)` => `call_bind`
  // => a `bind` where the "bind's `this`" is `call`
  // => as in `call.bind(...)` ... :-) yay!
  // ----------------------------------------
  // One more time (with different words):
  // This is basically the same as in step 2:
  // But instead of calling `call.bind(...) immediately on something
  // we bind `call` as the `this` for a function arg we provide later
  // so instead of `call.bind(set)` to make `set_call`,
  // we now can use our single helper function `call_bind(...)`
  const set_call = call_bind(set)
  set_call(map, 2, 'b')
}

// It might read funky :-), but it works in the exact same way:
// 1. While the former turned `set.call(...)` into a single function
// 2. The latter turns `call.bind(...)` into a single function
// =>
// `set_call` allows to "uncurry" `map.set`
// and we can now do `set_call(map,...)`
//
// `call_bind` allows to "uncurry" `call.bind`
// and we can now do `set_call = call_bind(set)`

// ------------------------------
// SUMMARY:

// So we went from
/*0.*/map.set(2, 'b')
// to 
/*2.*/set.call(map, 2, 'b')
// to
/*3.*/call.bind(set)(map, 2, 'b')
// to
/*4.*/bind.bind(call)(set)(map, 2, 'b')

// No prototype corruption will break anything now,
// because we have direct references to all functions
// and never have to call methods again
// which depend on prototypes not being broken.

// All we need to do once initially is:
{
  const { set } = Map.prototype
  const { bind, call } = Function.prototype
  const call_bind = bind.bind(call)
  // basically `call_bind` (a.k.a `uncurryThis`) is a single
  // uncorruptable function to create
  // as many uncurried functions later as we like :-)
  const map = new Map()
  // and now instead of
  map.set(2, 'b')
  // we can do without prototype dependent method calls
  const set_call = call_bind(set)
  set_call(map, 2, 'b')
}
// `set` and `map` and `set_call` and `call_bind` will always work
// no matter if prototypes change later or not.
// they do not depend on the prototypes anymore

For an even deeper dive, i can recommend: http://web.archive.org/web/20160805225710/http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.