48

Sorry if I'm missing something obvious, but I can't figure out how to bind a specific (nth) argument of a function in javascript. Most of my functional programming I've learned has been from Scala so I'm not sure this is even possible in JS.

For example, I understand I can do the below to bind the 1st argument

var add = function (a, b) {
   return a + b;
};

add(1, 3); //returns 4

var addThree = add.bind(null, 3);  //this = null.  a = 3
addThree(4);                        //returns 7

But how can I bind the 2nd argument and leave the first as is. In other words how can I bind to 'b' only?

From what i can tell from mozilla - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind , the arguments are actually nested which makes it look like it has to be in specific order? (I'm very possibly reading this wrong)

Edit: I realize this is a sort of contrived example. I'm just trying to learn in case I eventually deal with something more complex than adding 2 numbers. I'm also trying to understand how the bind() arguments are actually working under the hood.

Justin Maat
  • 1,965
  • 3
  • 23
  • 33
  • You sure you don't mean `add.bind(null,3)`? `add.apply()` executes immediately with the arguments and given `this` – deitch Dec 30 '14 at 04:17
  • yeah i did mean bind. just updated, thanks :) – Justin Maat Dec 30 '14 at 04:18
  • 6
    It's not baked into the language, but check this article out http://ejohn.org/blog/partial-functions-in-javascript/ where he shows a way of doing it by specifying `undefined` for the parameters you want to fill in later. – Jeff Storey Dec 30 '14 at 04:19
  • So you want a right section? See the following answer: http://stackoverflow.com/a/25720884/783743 – Aadit M Shah Dec 30 '14 at 04:48

5 Answers5

45

Of course you can do it. Here's an ES6 solution using the spread operator (...), since it's a bit more compact.

// Bind arguments starting after however many are passed in.
function bind_trailing_args(fn, ...bound_args) {
    return function(...args) {
        return fn(...args, ...bound_args);
    };
}

If you'd prefer to specify the position at which binding starts:

// Bind arguments starting with argument number "n".
function bind_args_from_n(fn, n, ...bound_args) {
    return function(...args) {
        return fn(...args.slice(0, n-1), ...bound_args);
    };
}

IN ES5, you have to muck around with constructing argument lists.

// ES5 version: construct arguments lists yourself
function bind_trailing_args(fn) {
    var bound_args = [].slice.call(arguments, 1);
    return function() {
        var args = [].concat.call(arguments, bound_args);
        return fn.apply(this, args);
    };
}

Unlike the first two examples, this one handles this properly.

In the context of your example:

var addThree = bind_trailing_args(add, 3);
addThree(1) // calls add(1, 3)

You could also consider using one of the functional programming libraries available for JS, such as http://osteele.com/sources/javascript/functional/. The thing you want is called rcurry there.

  • Great answer illustrating the power of es-6! – Hrishi Dec 30 '14 at 04:58
  • This is interesting. I'll have to play around with it when I get ecmascript 6 working in my console. I'm curious what the implications on `this` is using the first 2 methods. Do you know if `this` loses scope due to closure? I thought semantically this looks different since obj.bind() takes the binding obj as it's first arg. – Justin Maat Dec 30 '14 at 14:21
  • So with ES6 no straightforward way to bind an arbitrary subset of the arguments that is, say, not starting nor ending the arguments list? – matanster Sep 06 '15 at 19:45
  • Just to make sure I'm understanding correctly, in ES5 example you could've just as well passed `null` instead of `this` for `return fn.apply(this, args);` since the function addThree isn't on an object? – PDN May 10 '16 at 20:52
  • In this case, you are right. In other cases, where bind is being called on a method, you would want to pass this. –  May 11 '16 at 02:24
  • Is this not leaking arguments? Check **[here for reference](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments)**. – Wilt Jun 21 '16 at 10:50
  • Yes, I think it is. –  Jun 21 '16 at 14:30
  • Looking into another similar question, I noticed that the functional.js library no longer includes that `rcurry` method, nor the other one that allowed an arbitrary binding pattern with "_" placeholders (I don't recall its name). – Pointy Aug 03 '17 at 13:15
40

You can use lodash's _.bind to achieve this:

var add = function(a, b) {
  document.write(a + b);
};

// Bind to first parameter (Nothing special here)
var bound = _.bind(add, null, 3);
bound(4);
// → 7

// Bind to second parameter by skipping the first one with "_"
var bound = _.bind(add, null, _, 4);
bound(3);
// → 7
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.9.3/lodash.min.js"></script>

I am usually against libraries and prefer coding my own utility functions, but an exception can easily be made for lodash. I would highly suggest you check its documentation whenever you have a "This must be in the language somewhere!" moment. It fills in a lot of blanks in JavaScript.

AlicanC
  • 863
  • 1
  • 8
  • 12
12

Well. I'll just throw this out there.

var add = function(a,b) {
  return a + b;
};

var addThree = function(a) {
  return add(a,3);
};

add(1,2);
addThree(4);

Maybe it will be ok for some.

Benjamin
  • 1,832
  • 1
  • 17
  • 27
  • This was my initial thinking but was hoping the partial application was part of the core language itself. Can I ask what the purpose of assigning the the function to a higherOrder object is? Can't we do the same without creating a new object for it? – Justin Maat Dec 30 '14 at 13:54
  • @JustinMaat I understand this was really not exactly what you were asking, but it did solve the problem in a different way. I'm used to not placing things in a global space. I'll switch it for this question. – Benjamin Dec 30 '14 at 20:15
  • Ok , yeah you're right the above would be in global space. I typically wrap my code too, just was confused bc the above seemed to imply the wrapper object was necessary to decorate the function. Anyway, I'll give you an upvote as this is an alternative. – Justin Maat Dec 30 '14 at 20:19
0

you can try this:

Function.prototype.bindThemAll = function bindThemAll(thisArg, ...boundArgs) 
  (boundArgs.fn = this, function(...args) boundArgs.fn.call(thisArg || this, ...boundArgs.map((el) => el || args.shift()), ...args));

function fn() console.log("fn:", arguments.callee, "this:", this, "args:", arguments)

var x = {a: 5};

var bfn = fn.bindThemAll(x, null, 2)
bfn(1,3)
x.bfn = fn.bindThemAll(null, null, 2)
x.bfn(1,3)

bound arguments with null or undefined will be replaced with the function arguments in order, the remaining arguments will be appended to the end. an object will be used if it is bound otherwise the current this will be used...

see console for results!

yochaim
  • 1
  • 1
0
// 'this' arg gets dropped because console.log doesn't log it's own context
test = bindArguments(console.log, 'this', 'one', undefined, 'three', undefined, 'REST-After-This:', Symbol.split, 'END-2', 'END-Argument')

> test('TWO-filled-undefslot')
>> one TWO-filled-undefslot three undefined REST-After-This: END-2 END-Argument

> test(null, undefined, 'rest#1', 'rest#2')
>> one null three undefined REST-After-This: rest#1 rest#2 END-2 END-Argument

> test('TWO-filled-undefslot', 'FOUR-filled-undefslot', 'rest#1', 'rest#2')
>> one 2-filled-undefslot three 4-filled-undefslot REST-After-This: rest#1 rest#2 END-2 END-Argument

  • The first arg following the function is always the this arg
  • I used Symbol.split to differentiate between leading and trailing args. It'd probably be better if leading & trailing arrays containing the arguments to bind were instead directly passed.
  • undefined values in the leading bound arguments get substituted with passed in arguments
  • (bonus) The bound function inherits all properties/fields of the function it's passed.
    • Remove copyProperties & REMOVE_DESCRIPTORS to omit behavior

UsingSymbol.split:

const bindArguments = (()=>{
  const { defineProperty, getOwnPropertyDescriptors, defineProperties } = Object;
  const defineValue = (target, key, value) => defineProperty(target, key, { value: value });

  const $targetFn  = Symbol.for('bindArguments::[[TargetFunction]]');
  const $thisArgV  = Symbol.for('bindArguments::[[BoundThis]]');
  const $headArgs  = Symbol.for('bindArguments::[[BoundArgs]].Leading');
  const $tailArgs  = Symbol.for('bindArguments::[[BoundArgs]].Trailing');
  const EMPTY_ARRAY = Object.freeze([]);
  const REMOVE_DESCRIPTORS = [$targetFn, $thisArgV, $headArgs, $tailArgs, 'name', 'toString', 'length'];
  const copyProperties = (target, source) => {
    const descriptors = getOwnPropertyDescriptors(source);
    for(let key in descriptors) {
      if (REMOVE_DESCRIPTORS.includes(key)) { continue; }
      defineProperty(target, key, descriptors[key]);
    }
  }
  const fillUndefinedPositions = (boundArgs, args) => {
    let idxUndefined = boundArgs.indexOf(undefined);
    if (idxUndefined >= 0) {
      boundArgs = [...boundArgs];
      while (idxUndefined >= 0 && args.length > 0) {
        boundArgs[idxUndefined] = args.shift();
        idxUndefined = boundArgs.indexOf(undefined, idxUndefined+1);
      }
    }
    return boundArgs;
  };
  const boundFuncProps = {
    name:    { configurable: true, get() { return 'bound ' + this[$targetFn].name; } },
    toString:{ configurable: true, get() { return this[$targetFn].toString;  } },
    length:  { configurable: true, get() { return Math.max(0, this[$targetFn].length - this[$headArgs].length); } }
  }
  const createBoundFunction = (targetFunction, boundThis, leadingArgs, trailingArgs) => {
    defineValue(boundFunction, $targetFn, targetFunction);
    defineValue(boundFunction, $thisArgV, boundThis);
    defineValue(boundFunction, $headArgs, leadingArgs.length > 0 ? leadingArgs : EMPTY_ARRAY);
    defineValue(boundFunction, $tailArgs, trailingArgs.length > 0 ? trailingArgs : EMPTY_ARRAY);
    defineProperties(boundFunction, boundFuncProps)
    copyProperties(boundFunction, targetFunction);
    return boundFunction;

    function boundFunction(...args) {
      const targetFn = boundFunction[$targetFn];
      const thisArgV = boundFunction[$thisArgV];
      const headArgsV = boundFunction[$headArgs];
      const tailArgs = boundFunction[$tailArgs];   
      const headArgs = headArgsV.length > 0 && args.length > 0 ? fillUndefinedPositions(headArgsV, args) : headArgsV;
      const thisArg = thisArgV === undefined || this instanceof boundFunction ? this : thisArgV;
      const result = targetFn.call(thisArg, ...headArgs, ...args, ...tailArgs);
      return result;
    };
  };

  return (fn, ...bindArgs) => {
    const argsLength = bindArgs.length;
    if (argsLength === 0) { return fn; }
    Function.prototype.bind
    const idxSplit = bindArgs.indexOf(Symbol.split);
    switch(idxSplit) {
      case -1:
        return createBoundFunction(fn, bindArgs[0], bindArgs.slice(1), EMPTY_ARRAY);
      case 0:
        return createBoundFunction(fn, undefined, EMPTY_ARRAY, bindArgs.slice(1));
      case 1:
        return createBoundFunction(fn, bindArgs[0], EMPTY_ARRAY, bindArgs.slice(2));
      default:
        return createBoundFunction(fn, bindArgs[0], bindArgs.slice(1, idxSplit), bindArgs.slice(idxSplit+1));
    }
  };
})();

Pass leading and trailing arguments directly.

test2 = bindArguments2(console.log, null, ['this', 'one', undefined, 'three', undefined, 'REST-After-This:'], ['END-2', 'END-Argument'])

> test2('TWO-filled-undefslot', 'FOUR-filled-undefslot', 'rest#1', 'rest#2')
>> this one 2-filled-undefslot three 4-filled-undefslot REST-After-This: rest#1 rest#2 END-2 END-Argument
const bindArguments2 = (()=>{
  const isArray = Array.isArray;
  const { defineProperty, getOwnPropertyDescriptors, defineProperties } = Object;
  const defineValue = (target, key, value) => defineProperty(target, key, { value: value });

  const $targetFn  = Symbol.for('bindArguments::[[TargetFunction]]');
  const $thisArgV  = Symbol.for('bindArguments::[[BoundThis]]');
  const $headArgs  = Symbol.for('bindArguments::[[BoundArgs]].Leading');
  const $tailArgs  = Symbol.for('bindArguments::[[BoundArgs]].Trailing');
  const EMPTY_ARRAY = Object.freeze([]);
  const REMOVE_DESCRIPTORS = [$targetFn, $thisArgV, $headArgs, $tailArgs, 'name', 'toString', 'length'];
  const copyProperties = (target, source) => {
    const descriptors = getOwnPropertyDescriptors(source);
    for(let key in descriptors) {
      if (REMOVE_DESCRIPTORS.includes(key)) { continue; }
      defineProperty(target, key, descriptors[key]);
    }
  }
  const fillUndefinedPositions = (boundArgs, args) => {
    let idxUndefined = boundArgs.indexOf(undefined);
    if (idxUndefined >= 0) {
      boundArgs = [...boundArgs];
      while (idxUndefined >= 0 && args.length > 0) {
        boundArgs[idxUndefined] = args.shift();
        idxUndefined = boundArgs.indexOf(undefined, idxUndefined+1);
      }
    }
    return boundArgs;
  };
  const boundFuncProps = {
    name:    { configurable: true, get() { return 'bound ' + this[$targetFn].name; } },
    toString:{ configurable: true, get() { return this[$targetFn].toString;  } },
    length:  { configurable: true, get() { return Math.max(0, this[$targetFn].length - this[$headArgs].length); } }
  }
  return bindArguments;

  function bindArguments(
    /**@type {(...args: any[])=>any}*/fn,
    /**@type {ThisParameterType<typeof fn>}*/thisArg,
    /**@type {Parameters<typeof fn>}*/leadingArgs,
    /**@type {DropEndWith<Parameters<typeof fn>, typeof leadingArgs>}*/trailingArgs
  ) {
    defineValue(boundFunction, $targetFn, fn);
    defineValue(boundFunction, $thisArgV, thisArg);
    defineValue(boundFunction, $headArgs, isArray(leadingArgs) && leadingArgs.length > 0 ? leadingArgs : EMPTY_ARRAY);
    defineValue(boundFunction, $tailArgs, isArray(trailingArgs) && trailingArgs.length > 0 ? trailingArgs : EMPTY_ARRAY);
    defineProperties(boundFunction, boundFuncProps)
    copyProperties(boundFunction, fn);
    return boundFunction;

    /**@type {(...args: DropBetween<Parameters<typeof fn>, typeof leadingArgs, typeof trailingArgs>>)=>ReturnType<typeof fn>}*/
    function boundFunction(...args) {
      const targetFn = boundFunction[$targetFn];
      const thisArgV = boundFunction[$thisArgV];
      const headArgsV = boundFunction[$headArgs];
      const tailArgs = boundFunction[$tailArgs];
      const headArgs = headArgsV.length > 0 && args.length > 0 ? fillUndefinedPositions(headArgsV, args) : headArgsV;
      const thisArg = thisArgV === undefined || this instanceof boundFunction ? this : thisArgV;
      const result = targetFn.call(thisArg, ...headArgs, ...args, ...tailArgs);
      return result;
    };
  };
})();

Derek Ziemba
  • 2,467
  • 22
  • 22