Как связать аргументы функции без привязки этого?

116

Как в Javascript привязать аргументы к функции без привязки thisпараметра?

Например:

//Example function.
var c = function(a, b, c, callback) {};

//Bind values 1, 2, and 3 to a, b, and c, leave callback unbound.
var b = c.bind(null, 1, 2, 3); //How can I do this without binding scope?

Как я могу избежать побочного эффекта, связанного с привязкой области действия функции (например, setting this= null)?

Редактировать:

Извините за путаницу. Я хочу связать аргументы, а затем иметь возможность вызвать связанную функцию позже и заставить ее вести себя точно так, как если бы я вызвал исходную функцию и передал ей связанные аргументы:

var x = 'outside object';

var obj = {
  x: 'inside object',
  c: function(a, b, c, callback) {
    console.log(this.x);
  }
};

var b = obj.c.bind(null, 1, 2, 3);

//These should both have exact same output.
obj.c(1, 2, 3, function(){});
b(function(){});

//The following works, but I was hoping there was a better way:
var b = obj.c.bind(obj, 1, 2, 3); //Anyway to make it work without typing obj twice?

Я все еще новичок в этом, извините за путаницу.

Спасибо!

Imbue
источник
1
Было бы недостаточно просто переплетать this? var b = c.bind(this, 1,2,3);
kojiro
Почему проблематично иметь первое значение bind()be null? Кажется, отлично работает в FF.
Russ
7
Если функция уже имеет эту привязку, использование null в качестве первого аргумента bind приведет к путанице (например, привяжет метод объекта к глобальной области видимости).
Imbue
1
Мне действительно непонятно, что вы пытаетесь сделать. Вы не можете thisполностью освободить JavaScript. Это всегда что- то значит . Таким образом, привязка thisк thisзакрывающей функции имеет большой смысл.
kojiro
Я новичок в JS. Я отредактировал вопрос, чтобы было понятнее. Может быть, то, что я хочу, невозможно. Спасибо.
Imbue

Ответы:

32

Вы можете это сделать, но лучше не думать об этом как об «привязке», поскольку именно этот термин используется для установки значения «это». Возможно, подумайте об этом как об «обертывании» аргументов в функцию?

Что вы делаете, так это создаете функцию, в которую встроены желаемые аргументы с помощью замыканий:

var withWrappedArguments = function(arg1, arg2)
    {
        return function() { ... do your stuff with arg1 and arg2 ... };
    }(actualArg1Value, actualArg2Value);

Надеюсь, я понял синтаксис прямо здесь. Что он делает, так это создает функцию с именем withWrappedArguments () (чтобы быть педантичным, это анонимная функция, назначенная переменной), которую вы можете вызывать в любое время в любом месте и всегда будет действовать с actualArg1Value и actualArg2Value, и всем остальным, что вы хотите добавить там. Вы также можете заставить его принимать дополнительные аргументы во время вызова, если хотите. Секрет в круглых скобках после последней закрывающей скобки. Это вызывает немедленное выполнение внешней функции с переданными значениями и создание внутренней функции, которая может быть вызвана позже. Затем переданные значения замораживаются во время создания функции.

По сути, это то, что делает bind, но таким образом явно видно, что обернутые аргументы являются просто замыканиями для локальных переменных, и нет необходимости изменять поведение this.

Ян Нартович
источник
16
Обратите внимание, что это обычно называется «каррированием».
M3D
@ M3D Использование заглавных букв делает это так странно.
Эндрю
Это было бы намного полезнее, если бы был приведен пример того, как его использовать.
Эндрю
en.wikipedia.org/wiki/Ссылка для всех, кому интересно, хотя версия TL; DR: «Вместо того, f(x,y)чтобы мы хотели f(x)(y)или g=f(x); g(y). Поэтому мы бы изменились f=(x,y)=>x+yна f=(x)=>(y)=>x+y».
M3D
26

В ES6 это легко сделать с помощью параметров rest в сочетании с оператором распространения .

Таким образом, мы можем определить функцию, bindArgsкоторая работает как bind, за исключением того, что связаны только аргументы, но не context ( this).

Function.prototype.bindArgs =
    function (...boundArgs)
    {
        const targetFunction = this;
        return function (...args) { return targetFunction.call(this, ...boundArgs, ...args); };
    };

Затем для указанной функции fooи объекта objинструкция

return foo.call(obj, 1, 2, 3, 4);

эквивалентно

let bar = foo.bindArgs(1, 2);
return bar.call(obj, 3, 4);

где привязаны только первый и второй аргументы, в barто время как используется контекст, objуказанный в вызове, а дополнительные аргументы добавляются после связанных аргументов. Возвращаемое значение просто пересылается.

GOTO 0
источник
11
приносит es6 в таблицу, по-прежнему использует var :(
Дэниел Кобе
7
@DanielKobe Спасибо! Операторы распространения и параметры отдыха были реализованы в Firefox задолго до этого letи constеще не были доступны к тому времени, когда я впервые опубликовал это. Сейчас он обновлен.
GOTO 0
Это замечательно и работает для функций обратного вызова! Также можно изменить, добавляя аргументы, а не добавляя их.
DenisM
1
Стоит отметить, что это создает закрытие каждый раз, когда вызывается bindArgs (), что означает, что производительность не так хороша, как при использовании встроенной функции bind ().
Джошуа Уолш
17

В родном bindметодеthis значение в функции результата теряется. Однако вы можете легко перекодировать общую прокладку, чтобы не использовать аргумент для контекста:

Function.prototype.arg = function() {
    if (typeof this !== "function")
        throw new TypeError("Function.prototype.arg needs to be called on a function");
    var slice = Array.prototype.slice,
        args = slice.call(arguments), 
        fn = this, 
        partial = function() {
            return fn.apply(this, args.concat(slice.call(arguments)));
//                          ^^^^
        };
    partial.prototype = Object.create(this.prototype);
    return partial;
};
Берги
источник
4
Не нравится тот факт, что вам нужно играть с прототипом, Functionно это сэкономило мне много времени и сэкономило некоторые уродливые обходные пути. Спасибо
jackdbernier
3
@jackdbernier: Не обязательно, но я считаю, что это более интуитивно понятный метод вроде bind. Вы легко можете превратить его вfunction bindArgs(fn){ …, args = slice.call(arguments, 1); …}
Берги
@ Qantas94Heavy: Это то , что вы всегда должны делать , когда вы заботитесь о thisбыть a. Вы bind, конечно, можете использовать вместо этого. Тем не менее, OP явно не заботится о thisзначении и хочет несвязанную частичную функцию.
Bergi
@Bergi: Я предполагал, что цель отсутствия настройки thisзаключается в том, что функция использует this, но они не хотели связывать ее в тот момент. Кроме того, бит в вопросе OP //These should both have exact same output.противоречит другим его частям.
Qantas 94 Heavy
1
@KubaWyrostek: Да, это для того, чтобы функции частичного конструктора работали, вроде как подклассы (хотя я не рекомендую это делать). Актуал bindвообще не работает с конструкторами, у связанных функций нет .prototype. Нет,Function.prototype !== mymethod.prototype
Берги
7

Еще одна крошечная реализация для развлечения:

function bindWithoutThis(cb) {
    var bindArgs = Array.prototype.slice.call(arguments, 1);

    return function () {
        var internalArgs = Array.prototype.slice.call(arguments, 0);
        var args = Array.prototype.concat(bindArgs, internalArgs);
        return cb.apply(this, args);
    };
}

Как пользоваться:

function onWriteEnd(evt) {}
var myPersonalWriteEnd = bindWithoutThis(onWriteEnd, "some", "data");
Дмитрий Сорин
источник
Похоже, было бы правильнее передать для этого "null" при вызове apply ()
eddiewould
6
var b = function() {
    return c(1,2,3);
};
Майк
источник
3
На все эти шумные и неоднозначные ответы это точный и полезный ответ. Поэтому +1. Может быть, вы могли бы уточнить стиль ответа, чтобы сделать его более привлекательным для невооруженного глаза в этом причудливом шумном уголке джунглей. const curry = (fn, ...curryArgs) => (...args) => fn(...curryArgs, args)
Joehannes
2

Сложно сказать, что именно вы в конечном итоге хотите сделать, потому что пример произвольный, но вы можете посмотреть на частичные (или каррирование): http://jsbin.com/ifoqoj/1/edit

Function.prototype.partial = function(){
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function(){
    var arg = 0;
    for ( var i = 0; i < args.length && arg < arguments.length; i++ )
      if ( args[i] === undefined )
        args[i] = arguments[arg++];
    return fn.apply(this, args);
  };
};

var c = function(a, b, c, callback) {
  console.log( a, b, c, callback )
};

var b = c.partial(1, 2, 3, undefined);

b(function(){})

Ссылка на статью Джона Ресига: http://ejohn.org/blog/partial-functions-in-javascript/

Кевин Эннис
источник
Эта функция не работает, вы не можете вызвать частично примененную функцию более одного раза.
Берги
Не уверен, что понимаю. Можете ли вы немного рассказать об этом или опубликовать пример jsbin?
Кевин Эннис
См. Этот пример . Кроме того, для работы вам partialнужно вызывать undefinedаргументы, а не то, что можно было бы ожидать, и нарушать функции, которые принимают произвольное количество параметров.
Берги
Кевин, спасибо за ответ. Это близко к тому, что я хочу, но я думаю, что thisобъект все еще испорчен. Взгляните на: jsbin.com/ifoqoj/4/edit
Imbue
Оооо, попался. Этот пример проясняет то, что вы ищете.
Кевин Эннис
2

Возможно, вы хотите привязать ссылку на это в последний, но ваш код: -

var c = function(a, b, c, callback) {};
var b = c.bind(null, 1, 2, 3); 

Уже применена привязка, например эта, а позже вы не можете ее изменить. Я предлагаю использовать ссылку также в качестве такого параметра: -

var c = function(a, b, c, callback, ref) {  
    var self = this ? this : ref; 
    // Now you can use self just like this in your code 
};
var b = c.bind(null, 1, 2, 3),
    newRef = this, // or ref whatever you want to apply inside function c()
    d = c.bind(callback, newRef);
Дипак Диксит
источник
1

Используйте protagonist!

var geoOpts = {...};

function geoSuccess(user){  // protagonizes for 'user'
  return function Success(pos){
    if(!pos || !pos.coords || !pos.coords.latitude || !pos.coords.longitude){ throw new Error('Geolocation Error: insufficient data.'); }

    var data = {pos.coords: pos.coords, ...};

    // now we have a callback we can turn into an object. implementation can use 'this' inside callback
    if(user){
      user.prototype = data;
      user.prototype.watch = watchUser;
      thus.User = (new user(data));
      console.log('thus.User', thus, thus.User);
    }
  }
}

function geoError(errorCallback){  // protagonizes for 'errorCallback'
  return function(err){
    console.log('@DECLINED', err);
    errorCallback && errorCallback(err);
  }
}

function getUserPos(user, error, opts){
  nav.geo.getPos(geoSuccess(user), geoError(error), opts || geoOpts);
}

По сути, функция, которой вы хотите передать параметры, становится прокси-сервером, который вы можете вызвать для передачи переменной, и она возвращает функцию, которую вы действительно хотите делать.

Надеюсь это поможет!

Cody
источник
1

Анонимный пользователь разместил эту дополнительную информацию:

Основываясь на том, что уже было предоставлено в этом посте, самое элегантное решение, которое я видел, - это карриовать ваши аргументы и контекст:

function Class(a, b, c, d){
    console.log('@Class #this', this, a, b, c, d);
}

function Context(name){
    console.log('@Context', this, name);
    this.name = name;
}

var context1 = new Context('One');
var context2 = new Context('Two');

function curryArguments(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function bindContext() {
      var additional = Array.prototype.slice.call(arguments, 0);
      return fn.apply(this, args.concat(additional));
    };
}

var bindContext = curryArguments(Class, 'A', 'B');

bindContext.apply(context1, ['C', 'D']);
bindContext.apply(context2, ['Y', 'Z']);
Баретт
источник
1

Что ж, для примера, который вы дали, это подойдет

var b= function(callback){
        return obj.c(1,2,3, callback);
};

Если вы хотите гарантировать вложение параметров:

var b= (function(p1,p2,p3, obj){
        var c=obj.c;
        return function(callback){
                return c.call(obj,p1,p2,p3, callback);
        }
})(1,2,3,obj)

Но если это так, вам следует просто придерживаться своего решения:

var b = obj.c.bind(obj, 1, 2, 3);

Это лучший способ.

Джозеф Гарроне
источник
1

Так просто?

var b = (cb) => obj.c(1,2,3, cb)
b(function(){}) // insidde object

Более общее решение:

function original(a, b, c) { console.log(a, b, c) }
let tied = (...args) => original(1, 2, ...args)

original(1,2,3) // 1 2 3
tied(5,6,7) // 1 2 5

Qwerty
источник
1

Используя LoDash, вы можете использовать _.partialфункцию.

const f  = function (a, b, c, callback) {}

const pf = _.partial(f, 1, 2, 3)  // f has first 3 arguments bound.

pf(function () {})                // callback.
Даниэле Орландо
источник
0

Почему бы не использовать обертку вокруг функции, чтобы сохранить это как миф?

function mythis() {
  this.name = "mythis";
  mythis = this;
  function c(a, b) {
    this.name = "original";
    alert('a=' + a + ' b =' + b + 'this = ' + this.name + ' mythis = ' + mythis.name);
    return "ok";
  }    
  return {
    c: c
  }
};

var retval = mythis().c(0, 1);
Оливье Арчер
источник
-1

В jQuery 1.9 появилась именно эта функция с функцией прокси.

Начиная с jQuery 1.9, когда контекст имеет значение null или undefined, прокси-функция будет вызываться с тем же объектом this, с которым был вызван прокси. Это позволяет использовать $ .proxy () для частичного применения аргументов функции без изменения контекста.

Пример:

$.proxy(this.myFunction, 
        undefined /* leaving the context empty */, 
        [precededArg1, precededArg2]);
gosua
источник
-5

Пример использования JQuery:

вместо:

for(var i = 0;i<3;i++){
    $('<input>').appendTo('body').click(function(i){
        $(this).val(i); // wont work, because 'this' becomes 'i'
    }.bind(i));
}

использовать это:

for(var i = 0;i<3;i++){
    $('<input>').appendTo('body').click(function(e){
        var i = this;
        $(e.originalEvent.target).val(i);
    }.bind(i));
}

источник