จะผูกอาร์กิวเมนต์ของฟังก์ชันโดยไม่ผูกสิ่งนี้ได้อย่างไร?


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?

ฉันจะหลีกเลี่ยงผลข้างเคียงของการผูกขอบเขตของฟังก์ชัน (เช่นการตั้งค่า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?

ฉันยังใหม่สำหรับเรื่องนี้ขออภัยในความสับสน

ขอบคุณ!


1
การผูกใหม่จะไม่เพียงพอthisหรือไม่? var b = c.bind(this, 1,2,3);
kojiro

เหตุใดจึงมีปัญหาที่จะมีค่าแรกbind()เป็นnull? ดูเหมือนจะทำงานได้ดีใน FF
Russ

7
หากฟังก์ชันมีขอบเขตนี้อยู่แล้วการใช้ null เป็นอาร์กิวเมนต์แรกของการโยงจะทำให้สิ่งต่าง ๆ ยุ่งเหยิง (เช่นผูกเมธอดอ็อบเจ็กต์กับขอบเขตส่วนกลาง)
Imbue

1
ฉันไม่ชัดเจนจริงๆว่าคุณกำลังพยายามทำอะไร คุณไม่สามารถthisยกเลิกการผูกมัดใน JavaScript ได้อย่างสมบูรณ์ มันก็จะหมายถึงบางสิ่งบางอย่าง ดังนั้นการผูกthisกับthisฟังก์ชันการปิดล้อมจึงสมเหตุสมผลมาก
kojiro

ฉันค่อนข้างใหม่ที่ JS ฉันแก้ไขคำถามเพื่อให้ชัดเจนยิ่งขึ้น อาจเป็นไปได้ว่าสิ่งที่ฉันต้องการเป็นไปไม่ได้ ขอบคุณ
Imbue

คำตอบ:


32

คุณสามารถทำได้ แต่ควรหลีกเลี่ยงการคิดว่าเป็นการ "ผูกมัด" เนื่องจากเป็นคำที่ใช้ในการตั้งค่า "this" บางทีอาจคิดว่าเป็นการ "ตัด" อาร์กิวเมนต์ให้เป็นฟังก์ชัน?

สิ่งที่คุณทำคือสร้างฟังก์ชั่นที่มีอาร์กิวเมนต์ที่ต้องการในตัวผ่านการปิด:

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

หวังว่าฉันจะได้ไวยากรณ์ที่นั่น สิ่งที่ทำคือสร้างฟังก์ชันที่เรียกว่า withWrappedArguments () (เพื่ออวดอ้างว่าเป็นฟังก์ชันที่ไม่ระบุตัวตนที่กำหนดให้กับตัวแปร) ที่คุณสามารถเรียกได้ทุกเมื่อทุกที่และจะดำเนินการกับ actualArg1Value และ actualArg2Value เสมอและสิ่งอื่น ๆ ที่คุณต้องการใส่ ที่นั่น นอกจากนี้คุณยังสามารถให้มันยอมรับข้อโต้แย้งเพิ่มเติมในเวลาที่โทรได้หากคุณต้องการ เคล็ดลับคือวงเล็บหลังวงเล็บปีกกาปิดสุดท้าย สิ่งเหล่านี้ทำให้ฟังก์ชันภายนอกถูกเรียกใช้งานทันทีพร้อมกับค่าที่ส่งผ่านและสร้างฟังก์ชันภายในที่สามารถเรียกใช้ในภายหลัง จากนั้นค่าที่ผ่านจะถูกตรึงในเวลาที่สร้างฟังก์ชัน

นี่คือสิ่งที่การผูกทำได้อย่างมีประสิทธิภาพ แต่ด้วยวิธีนี้เป็นที่ชัดเจนว่าอาร์กิวเมนต์ที่ห่อไว้นั้นเป็นเพียงการปิดตัวแปรในเครื่องและไม่จำเป็นต้องเปลี่ยนพฤติกรรมของสิ่งนี้


16
โปรดทราบว่าปกติเรียกว่า "แกงกะหรี่"
M3D

@ M3D ตัวพิมพ์ใหญ่ที่ทำให้มันดูแปลก ๆ
Andrew

สิ่งนี้จะเป็นประโยชน์มากขึ้นหากมีตัวอย่างวิธีการใช้งาน
Andrew

en.wikipedia.org/wiki/Curryingลิงก์สำหรับทุกคนที่สนใจแม้ว่าเวอร์ชัน TLDR จะเป็น "แทนที่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ยกเว้นว่าอาร์กิวเมนต์เท่านั้นที่ถูกผูกไว้ แต่ไม่ใช่บริบท ( 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ระบุในการเรียกใช้และอาร์กิวเมนต์พิเศษจะถูกต่อท้ายหลังจากอาร์กิวเมนต์ที่ถูกผูกไว้ ค่าที่ส่งคืนจะถูกส่งต่อ


11
นำ es6 ไปที่ตารางยังคงใช้ var :(
Daniel Kobe

7
@DanielKobe ขอบคุณ! ตัวดำเนินการกระจายและพารามิเตอร์ส่วนที่เหลือถูกนำมาใช้ใน Firefox มานานletแล้วและconstยังไม่สามารถใช้งานได้ในตอนที่ฉันโพสต์ครั้งแรก อัปเดตแล้ว
GOTO 0

นี่เป็นสิ่งที่ยอดเยี่ยมและใช้ได้กับฟังก์ชันโทรกลับ! นอกจากนี้ยังสามารถเปลี่ยนรอบ ๆ เพื่อต่อท้าย args แทนที่จะใส่ไว้ข้างหน้า
DenisM

1
เป็นที่น่าสังเกตว่าสิ่งนี้ทำให้เกิดการปิดทุกครั้งที่เรียก bindArgs () ซึ่งหมายความว่าประสิทธิภาพไม่ดีเท่ากับการใช้ฟังก์ชันการผูกเนทีฟ ()
Joshua Walsh

17

ในพื้นเมืองbindวิธีthisค่าในฟังก์ชั่นผลที่จะหายไป อย่างไรก็ตามคุณสามารถเข้ารหัส shim ทั่วไปได้อย่างง่ายดายเพื่อไม่ให้ใช้อาร์กิวเมนต์สำหรับบริบท:

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); …}
Bergi

@ 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
Bergi

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(){})

ลิงก์ไปยังบทความของ John Resig: http://ejohn.org/blog/partial-functions-in-javascript/


ฟังก์ชันนี้ไม่ทำงานคุณไม่สามารถเรียกใช้ฟังก์ชันที่ใช้บางส่วนมากกว่าหนึ่งครั้งได้
Bergi

ไม่แน่ใจว่าฉันทำตาม คุณสามารถอธิบายเพิ่มเติมเกี่ยวกับเรื่องนี้หรือโพสต์ตัวอย่าง jsbin ได้หรือไม่?
Kevin Ennis

ดูตัวอย่างนี้ นอกจากนี้partialความต้องการของคุณจะต้องถูกเรียกด้วยundefinedอาร์กิวเมนต์เพื่อให้ทำงานได้ไม่ใช่สิ่งที่คาดหวังและใช้ฟังก์ชันที่ใช้พารามิเตอร์ตามจำนวนที่กำหนด
Bergi

เควินขอบคุณสำหรับคำตอบ ใกล้เคียงกับที่ฉันต้องการแล้ว แต่ฉันคิดว่าของthisยังคงยุ่งเหยิง ลองดูที่: jsbin.com/ifoqoj/4/edit
ธาตุ

โอ้โห gotchya ตัวอย่างนั้นทำให้ชัดเจนขึ้นมากว่าคุณกำลังมองหาอะไร
Kevin Ennis

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);
}

โดยทั่วไปฟังก์ชันที่คุณต้องการส่งผ่านพารามิเตอร์จะกลายเป็นพร็อกซีที่คุณสามารถเรียกใช้เพื่อส่งผ่านตัวแปรและจะส่งกลับฟังก์ชันที่คุณต้องการทำสิ่งต่างๆ

หวังว่านี่จะช่วยได้!


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


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 เมื่อบริบทเป็นโมฆะหรือไม่ได้กำหนดฟังก์ชันพร็อกซีจะถูกเรียกด้วยอ็อบเจ็กต์นี้เหมือนกับที่เรียกพร็อกซีด้วย สิ่งนี้ทำให้สามารถใช้ $ .proxy () เพื่อใช้อาร์กิวเมนต์ของฟังก์ชันบางส่วนโดยไม่ต้องเปลี่ยนบริบท

ตัวอย่าง:

$.proxy(this.myFunction, 
        undefined /* leaving the context empty */, 
        [precededArg1, precededArg2]);

-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));
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.