【问题标题】:Is it possible to call function.apply without changing the context?是否可以在不更改上下文的情况下调用 function.apply ?
【发布时间】:2012-09-09 02:22:47
【问题描述】:

在一些 Javascript 代码(特别是 node.js)中,我需要在不更改上下文的情况下调用具有一组未知参数的函数。例如:

function fn() {
    var args = Array.prototype.slice.call(arguments);
    otherFn.apply(this, args);
}

上面的问题是,当我调用apply 时,我通过传递this 作为第一个参数来更改上下文。我想将args 传递给被调用的函数而不更改被调用函数的上下文。我基本上想这样做:

function fn() {
    var args = Array.prototype.slice.call(arguments);
    otherFn.apply(<otherFn's original context>, args);
}

编辑:添加关于我的具体问题的更多细节。我正在创建一个客户端类,其中包含一个套接字(socket.io)对象以及与连接有关的其他信息。我正在通过客户端对象本身公开套接字的事件侦听器。

class Client
  constructor: (socket) ->
    @socket    = socket
    @avatar    = socket.handshake.avatar
    @listeners = {}

  addListener: (name, handler) ->
    @listeners[name] ||= {}
    @listeners[name][handler.clientListenerId] = wrapper = =>
      # append client object as the first argument before passing to handler
      args = Array.prototype.slice.call(arguments)
      args.unshift(this)
      handler.apply(this, args)  # <---- HANDLER'S CONTEXT IS CHANGING HERE :(

    @socket.addListener(name, wrapper)

  removeListener: (name, handler) ->
    try
      obj = @listeners[name]
      @socket.removeListener(obj[handler.clientListenerId])
      delete obj[handler.clientListenerId]

请注意,clientListenerId 是一个自定义唯一标识符属性,与the answer found here 基本相同。

【问题讨论】:

  • 您是在问如何获取对全局上下文的引用?
  • 您是否尝试过将第一个参数留空?只要它不是必需的参数,就应该有效。
  • 听起来你需要在将参数应用到绑定函数之前使用Function.prototype.bind
  • @Matt:函数永远不属于对象。如果您只有一个函数引用,那么调用该函数会将this 设置为全局对象,除非它被.bind 绑定到另一个值。如果是这样,那么将任何值作为this 传递都不会改变它。如果函数是“对象的方法”,则还需要引用该对象。
  • @Matt 我认为您将无法从您的 Client 类中执行此操作。在调用addListener 时,对处理程序上下文的引用已经丢失。 调用 addListener 的代码可以在调用之前对相应的对象执行bind 函数。那应该可以正常工作。但是一旦上下文丢失,它就完全消失了。对不起。

标签: javascript node.js function


【解决方案1】:

如果我理解正确的话:

                          changes context
                   |    n     |      y       |
accepts array    n |  func()  | func.call()  |
of arguments     y | ???????? | func.apply() |

PHP 有一个函数,call_user_func_array。不幸的是,JavaScript 在这方面缺乏。看起来您使用 eval() 模拟了这种行为。

Function.prototype.invoke = function(args) {
    var i, code = 'this(';
    for (i=0; i<args.length; i++) {
        if (i) { code += ',' }
        code += 'args[' + i + ']';
    }
    eval(code + ');');
}

是的,我知道。没有人喜欢eval()。它缓慢而危险。但是,在这种情况下,您可能不必担心跨站点脚本,至少,因为所有变量都包含在函数中。真的,JavaScript 没有用于此的本机函数实在是太糟糕了,但我想我们有 eval 是针对这种情况的。

证明它有效:

function showArgs() {
    for (x in arguments) {console.log(arguments[x]);}
}

showArgs.invoke(['foo',/bar/g]);
showArgs.invoke([window,[1,2,3]]);

Firefox 控制台输出:

--
[12:31:05.778] "foo"
[12:31:05.778] [object RegExp]
[12:31:05.778] [object Window]
[12:31:05.778] [object Array]

【讨论】:

  • 这是唯一真正回答问题的响应。其他人都认为 OP 要么想要 access 到上下文(他们没有),要么它没有上下文,或者他们只是不知道Function.apply (他们显然是这样做的)。谢谢你是唯一一个理解这个问题并说不的人,没有什么比 PHP 的 call_user_func_array 更好的了。
【解决方案2】:

简单地说,只需将 this 分配给您想要的,即 otherFn

function fn() {
    var args = Array.prototype.slice.call(arguments);
    otherFn.apply(otherFn, args);
}

【讨论】:

  • 我认为这正是问题作者所需要的。
  • 如果使用了bind,你可能不知道otherFn的上下文是什么。
【解决方案3】:

'this' 对函数上下文的引用。这才是重点。

如果你的意思是在这样的不同对象的上下文中调用它:

otherObj.otherFn(args)

然后只需将该对象替换为上下文:

otherObj.otherFn.apply(otherObj, args);

应该是这样的。

【讨论】:

  • 问题是在我可以访问otherFn的时候,我不知道它属于什么对象。如果我只有对函数的引用,有没有办法确定它的当前绑定?
  • 也许我在找Function.constructor...
  • 没有。如果你有一个原始函数,你无法知道它是从哪里来的。它可以绑定到任何东西,或者什么都没有。
  • 我认为 Function 构造函数对你没有任何好处。它可能有助于创建一个新函数,但仍然无法帮助您恢复丢失的上下文。
  • 如果你失去了上下文,它真的消失了。您能否编辑帖子以提供有关该功能来自何处的更多背景信息?
【解决方案4】:

如果你将函数绑定到一个对象并且在任何地方都使用绑定的函数,你可以使用 null 调用 apply,但仍然会得到正确的上下文

var Person = function(name){
    this.name = name;
}
Person.prototype.printName = function(){
    console.log("Name: " + this.name);
}

var bob = new Person("Bob");

bob.printName.apply(null); //window.name
bob.printName.bind(bob).apply(null); //"Bob"

【讨论】:

    【解决方案5】:

    解决调用函数时 JavaScript 中可能发生的上下文更改的一种方法是,如果您需要它们能够在 @987654321 的上下文中操作,则使用作为对象构造函数一部分的方法@ 并不意味着父对象,通过有效地创建一个本地私有变量来存储原始的 this 标识符。

    我承认 - 就像大多数关于 JavaScript 范围的讨论一样 - 这并不完全清楚,所以这里是我如何做到这一点的示例:

    function CounterType()
    {
        var counter=1;
        var self=this; // 'self' will now be visible to all
    
        var incrementCount = function()
        {
            // it doesn't matter that 'this' has changed because 'self' now points to CounterType()
            self.counter++;
        };
    
    }
    
    function SecondaryType()
    {
        var myCounter = new CounterType();
        console.log("First Counter : "+myCounter.counter); // 0
        myCounter.incrementCount.apply(this); 
        console.log("Second Counter: "+myCounter.counter); // 1
    }
    

    【讨论】:

      【解决方案6】:

      这些天你可以使用rest parameters:

      function fn(...args) {
          otherFn(...args);
      }
      

      唯一的缺点是,如果你想在fn中使用一些特定的参数,你必须从args中提取它:

      function fn(...args) {
          let importantParam = args[2]; //third param
          // ...
          otherFn(...args);
      }
      

      这是一个可以尝试的示例(ES 下一个版本会保持简短):

      // a one-line "sum any number of arguments" function
      const sum = (...args) => args.reduce((sum, value) => sum + value);
      
      // a "proxy" function to test:
      var pass = (...args) => sum(...args);
      console.log(pass(1, 2, 15));

      【讨论】:

      • 是的,这是一个很棒的现代解决方案,当时还没有。感谢分享。 :)
      【解决方案7】:

      我不会接受这个作为答案,因为我仍然希望有更合适的东西。但这是我目前根据对这个问题的反馈使用的方法。

      对于将调用Client.prototype.addListenerClient.prototype.removeListener 的任何类,我确实将以下代码添加到它们的构造函数中:

      class ExampleClass
        constructor: ->
          # ...
          for name, fn of this
            this[name] = fn.bind(this) if typeof(fn) == 'function'
      
         message: (recipient, body) ->
           # ...
      
         broadcast: (body) ->
           # ...
      

      在上面的示例中,messagebroadcast 在实例化时将始终绑定到新的 ExampleClass 原型对象,从而允许我原始问题中的 addListener 代码工作。

      我相信你们中的一些人想知道为什么我不只是做以下这样的事情:

      example = new ExampleClass
      client.addListener('message', example.bind(example))
      # ...
      client.removeListener('message', example.bind(example))
      

      问题是每次调用.bind( ) 时,它都是一个新对象。所以这意味着以下情况是正确的:

      example.bind(example) != example.bind(example)
      

      因此,removeListener 永远不会成功,因此我在实例化对象时绑定该方法一次。

      【讨论】:

      • 投反对票:为什么投反对票?我刚刚分享了我为解决自己的问题所做的工作,很高兴知道您的推理。
      • 我也会。虽然您的问题是基于对 this 工作原理的误解,但您的回答是完全正确的。在您希望自己传递函数但将所有调用都视为特定的“父”对象。
      【解决方案8】:

      由于您似乎想要使用 Javascript 1.8.5 中定义的 bind 函数,并且能够检索您传递绑定函数的原始 this 对象,我建议重新定义 Function.prototype.bind功能:

      Function.prototype.bind = function (oThis) {
          if (typeof this !== "function") {
              throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
          }
      
          var aArgs = Array.prototype.slice.call(arguments, 1),
              fToBind = this,
              fNOP = function () {},
              fBound = function () {
                  return fToBind.apply(this instanceof fNOP && oThis
                  ? this
                  : oThis,
                  aArgs.concat(Array.prototype.slice.call(arguments)));
              };
      
          fNOP.prototype = this.prototype;
          fBound.prototype = new fNOP();
      
          /** here's the additional code **/
          fBound.getContext = function() {
              return oThis;
          };
          /**/
      
          return fBound;
      };
      

      现在您可以检索调用 bind 函数的原始上下文:

      function A() {
          return this.foo+' '+this.bar;
      }
      
      var HelloWorld = A.bind({
          foo: 'hello',
          bar: 'world',
      });
      
      HelloWorld(); // returns "hello world";
      HelloWorld.getContext(); // returns {foo:"hello", bar:"world"};
      

      【讨论】:

      • 在不指定上下文的情况下无法使用 .apply() 确实是 Javascript 的主要设计缺陷(知道在 Python 中使用 *args / **kwargs 是多么容易和频繁)。将函数参数传递给 .bind() 的好主意。谢谢。
      • 用原生函数搞混似乎是个坏主意。
      【解决方案9】:

      过了很久才想起这个问题。现在回想起来,我认为我在这里真正想要完成的事情类似于 React 库如何使用其自动绑定。

      本质上,每个函数都是一个被调用的封装绑定函数:

      function SomeClass() {
      };
      
      SomeClass.prototype.whoami = function () {
        return this;
      };
      
      SomeClass.createInstance = function () {
        var obj = new SomeClass();
      
        for (var fn in obj) {
          if (typeof obj[fn] == 'function') {
            var original = obj[fn];
      
            obj[fn] = function () {
              return original.apply(obj, arguments);
            };
          }
        }
      
        return obj;
      };
      
      var instance = SomeClass.createInstance();
      instance.whoami() == instance;            // true
      instance.whoami.apply(null) == instance;  // true
      

      【讨论】:

        【解决方案10】:

        只需将属性直接推送到函数的对象并使用它自己的“上下文”调用它。

        function otherFn() {
            console.log(this.foo+' '+this.bar); // prints: "hello world" when called from rootFn()
        }
        
        otherFn.foo = 'hello';
        otherFn.bar = 'world';
        
        function rootFn() {
            // by the way, unless you are removing or adding elements to 'arguments',
            // just pass the arguments object directly instead of casting it to Array
            otherFn.apply(otherFn, arguments);
        }
        

        【讨论】:

        • 如果函数是对象的原型函数,这似乎仍然是个问题。然后属性在对象上,而不是函数本身。传递 otherFn 作为上下文不允许我访问对象的属性。 (我试过这个没有成功。)
        • 不完全。更像这样:jsfiddle.net/K79pp/2 你可以看到对foo()bar() 的引用找不到,因为函数被用作上下文,而实际上我们希望otherClass 被用作上下文。
        • 如果我知道对象是什么能够提供它作为上下文,我会这样做。但是,如果您查看我的原始问题,当我可以访问 handler 函数时,我不知道原始父对象是什么。
        • 我认为您误解了 javascript 上下文的整个概念。每个函数都是一个独立的对象,存储对其作用域链的永久引用。 context 只是一个对象:如果您想应用具有特定上下文的函数,您可以使用context.fn(args)fn.apply(context, args)。由于上下文只是对象,如果您不维护对一个的显式引用那么垃圾收集器将处理它
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-21
        相关资源
        最近更新 更多