Javascript有类似Ruby的method_missing特性吗?

55

在Ruby中,我认为您可以调用未定义的方法,同时捕获调用的方法名称,并在运行时处理此方法。

JavaScript能做同样的事情吗?


我不确定你是否在提及函数原型,如果是的话,也许这个链接会有用?https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function 说实话,我不知道那个参考链接是否有用,因为我只在C++中看到过函数原型。 - Joel
可能是Javascript中缺少Capture方法并执行一些逻辑?的重复问题。 - Paul Sweatte
8个回答

45

method_missing 并不适用于 JavaScript,其原因与 Python 相同:在这两种语言中,方法只是函数的属性;而且对象通常具有不可调用的公共属性。相比之下,Ruby 的对象公共接口完全由方法组成。

JavaScript 需要的是一个钩子来捕获对丢失属性的访问,无论它们是方法还是其他类型的属性。Python 已经实现了这一点:可以参考特殊方法 __getattr__

Mozilla 提出的 __noSuchMethod__ 提案,在这种充满不一致性的语言中引入了另一个不一致性。

JavaScript 的未来是 Proxy 机制(也在 ECMAscript Harmony 中),它更接近 Python 协议的 自定义属性访问,而不是 Ruby 的 method_missing


3
请注意,JavaScript 的语义与 Python 不同且更加棘手。在 Python 中,f=obj.m;f(x) 等同于 obj.m(x)。但在 JavaScript 中, obj.m(x) 会将 this 设置为 obj,而 f=obj.m;f(x) 则不会设置。 - ehabkost
Amen:“Mozilla提出的__noSuchMethod__建议在这种充满不一致性的语言中引入了另一个不一致性。” - user2398029
我不了解以前的情况,但是现在Python有一个非常方便的__missing__方法。 - aliqandil
__missing__ 方法仅用于映射,以添加逻辑来处理映射中缺少键的情况。例如,实现一个使用字符串键但不区分大小写的映射非常有用。它与 method_missing 没有任何关系。 - Luciano Ramalho

31
您所介绍的Ruby特性被称为“method_missing”http://rubylearning.com/satishtalim/ruby_method_missing.htm。这是一个全新的功能,仅在一些浏览器中存在,比如Firefox(在spider monkey Javascript引擎中)。在SpiderMonkey中,它被称为"__noSuchMethod__" https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/NoSuchMethod。请阅读Yehuda Katz的这篇文章http://yehudakatz.com/2008/08/18/method_missing-in-javascript/获取更多即将到来的实现细节。

感谢这些文章,我以为我的奇怪问题会让我感到困惑 - 就像我经常在这里问元编程问题时一样 :) - 我很高兴js大师们正在思考它。 - user310291
19
耶胡达·卡茨的文章出自2008年。布兰登·艾奇自2010年起一直倡导代理API(Proxy API)(http://jsconf.eu/2010/speaker/be_proxy_objects.html)。Mozilla提出的__noSuchMethod__ API非标准且没有未来。 - Luciano Ramalho

6
你可以使用 Proxy 类。
var myObj = {
    someAttr: 'foo'
};

var p = new Proxy(myObj, {
    get: function (target, methodOrAttributeName) {
        // target is the first argument passed into new Proxy, aka. target is myObj

        // First give the target a chance to handle it
        if (Object.keys(target).indexOf(methodOrAttributeName) !== -1) {
            return target[methodOrAttributeName];
        }

        // If the target did not have the method/attribute return whatever we want

        // Explicitly handle certain cases
        if (methodOrAttributeName === 'specialPants') {
            return 'trousers';
        }

        // return our generic method_missing function
        return function () {
            // Use the special "arguments" object to access a variable number arguments
            return 'For show, myObj.someAttr="' + target.someAttr + '" and "'
                   + methodOrAttributeName + '" called with: [' 
                   + Array.prototype.slice.call(arguments).join(',') + ']';
        }
    }
});

console.log(p.specialPants);
// outputs: trousers

console.log(p.unknownMethod('hi', 'bye', 'ok'));
// outputs: 
// For show, myObj.someAttr="foo" and "unknownMethod" called with: [hi,bye,ok]

关于

在替换myObj的位置使用p

要小心使用get,因为它会截取p的所有属性请求。因此,p.specialPants()会导致错误,因为specialPants返回一个字符串而不是函数。

unknownMethod实际上等价于以下内容:

var unk = p.unkownMethod;
unk('hi', 'bye', 'ok');

这是因为在JavaScript中,函数也是对象。

额外福利

如果您知道预期的参数数量,可以像正常声明一样在返回的函数中声明它们。
例如:

...
get: function (target, name) {
    return function(expectedArg1, expectedArg2) {
...

4

2
代理目前在Chrome 21及以上版本中使用实验性的Javascript标志实现。请参阅此网站以获取有关当前支持的ECMAScript Harmony功能的最新信息: http://kangax.github.io/es5-compat-table/es6/ - stephenbez
1
@jörg-w-mittag,未来就快到了 :) - JackDev
1
@HappyHamburger:我对ES6非常兴奋,特别是Proper Tail Calls、letconst、简洁的函数语法以及Proxies。 - Jörg W Mittag
@jörg-w-mittag - 你认为新规范中类的实现怎么样? - JackDev
@HappyHamburger:我更多地将ES用作Scheme和Self的混合体,而不是浏览器中的Java克隆,所以我并不太在意类。然而,由于它们只是语法糖,它们并不会改变ES的核心。这与ES4类非常不同,后者基本上是除了原型之外的全新继承构造。 - Jörg W Mittag

3
我创建了一个用于JavaScript的库,让你可以在JavaScript中使用method_missing功能:https://github.com/ramadis/unmiss 它使用ES6代理来工作。以下是使用ES6类继承的示例。但是您也可以使用装饰器来实现相同的结果。
import { MethodMissingClass } from 'unmiss'

class Example extends MethodMissingClass {
    methodMissing(name, ...args) {
        console.log(`Method ${name} was called with arguments: ${args.join(' ')}`);
    }
}

const instance = new Example;
instance.what('is', 'this');

> Method what was called with arguments: is this

1

不,JavaScript没有直接类似于Ruby的method_missing钩子的元编程能力。解释器只是引发一个错误,被调用的代码可以捕获,但无法被访问的对象无法检测到。这里有一些关于在运行时定义函数的答案,但那不是同一件事情。您可以进行大量的元编程,更改对象的特定实例,定义函数,执行像备忘和装饰器之类的功能性操作。但是没有动态元编程缺失功能,就像Ruby或Python中那样。


1
我来到这个问题是因为我正在寻找一种方法,如果第一个对象上没有该方法,则跌落到另一个对象。它不如您所要求的那么灵活-例如,如果两者都缺少某个方法,则会失败。
我在想为我拥有的一个小型库做到这一点,该库有助于以使它们更易于测试的方式配置extjs对象。我有单独的调用来实际获取要交互的对象,并认为这可能是将这些调用粘合在一起的一种好方法,通过有效地返回增强类型来实现。
我可以想到两种方法来做到这一点:
原型
您可以使用原型来完成此操作-因为如果不在实际对象上,则会将所有内容都传递到原型中。如果您想要下降到使用this关键字的函数集-显然,您的对象不会知道或关心其他人知道的东西-则似乎这样做行不通。
如果它全部是您自己的代码并且您没有使用this和构造函数...出于很多原因这是个好主意,那么您可以像这样做:
    var makeHorse = function () {
        var neigh = "neigh";

        return {
            doTheNoise: function () {
                return neigh + " is all im saying"
            },
            setNeigh: function (newNoise) {
                neigh = newNoise;
            }
        }
    };

    var createSomething = function (fallThrough) {
        var constructor = function () {};
        constructor.prototype = fallThrough;
        var instance = new constructor();

        instance.someMethod = function () {
            console.log("aaaaa");
        };
        instance.callTheOther = function () {
            var theNoise = instance.doTheNoise();
            console.log(theNoise);
        };

        return instance;
    };

    var firstHorse = makeHorse();
    var secondHorse = makeHorse();
    secondHorse.setNeigh("mooo");

    var firstWrapper = createSomething(firstHorse);
    var secondWrapper = createSomething(secondHorse);
    var nothingWrapper = createSomething();

    firstWrapper.someMethod();
    firstWrapper.callTheOther();
    console.log(firstWrapper.doTheNoise());

    secondWrapper.someMethod();
    secondWrapper.callTheOther();
    console.log(secondWrapper.doTheNoise());

    nothingWrapper.someMethod();
    //this call fails as we dont have this method on the fall through object (which is undefined)
    console.log(nothingWrapper.doTheNoise());

这不适用于我的用例,因为extjs的开发人员不仅错误地使用了“this”,而且还建立了一个基于原型和“this”使用的整个疯狂的经典继承类型系统。
这实际上是我第一次使用原型/构造函数,我有点困惑,不能只设置原型 - 你还必须使用构造函数。对象中有一个神奇的字段(至少在Firefox中)称为__proto,基本上是真正的原型。看起来实际的原型字段只在构建时使用…多么令人困惑!

复制方法

这种方法可能更昂贵,但在我的看法中更加优雅,并且也适用于使用this的代码(例如,您可以使用它来包装库对象)。它也适用于使用函数式/闭包风格编写的内容 - 我只是用this /构造函数说明它可以与这样的内容一起使用。

以下是修改:

    //this is now a constructor
    var MakeHorse = function () {
        this.neigh = "neigh";
    };

    MakeHorse.prototype.doTheNoise = function () {
        return this.neigh + " is all im saying"
    };
    MakeHorse.prototype.setNeigh = function (newNoise) {
        this.neigh = newNoise;
    };

    var createSomething = function (fallThrough) {
        var instance = {
            someMethod : function () {
                console.log("aaaaa");
            },
            callTheOther : function () {
                //note this has had to change to directly call the fallThrough object
                var theNoise = fallThrough.doTheNoise();
                console.log(theNoise);
            }
        };

        //copy stuff over but not if it already exists
        for (var propertyName in fallThrough)
            if (!instance.hasOwnProperty(propertyName))
                instance[propertyName] = fallThrough[propertyName];

        return instance;
    };

    var firstHorse = new MakeHorse();
    var secondHorse = new MakeHorse();
    secondHorse.setNeigh("mooo");

    var firstWrapper = createSomething(firstHorse);
    var secondWrapper = createSomething(secondHorse);
    var nothingWrapper = createSomething();

    firstWrapper.someMethod();
    firstWrapper.callTheOther();
    console.log(firstWrapper.doTheNoise());

    secondWrapper.someMethod();
    secondWrapper.callTheOther();
    console.log(secondWrapper.doTheNoise());

    nothingWrapper.someMethod();
    //this call fails as we dont have this method on the fall through object (which is undefined)
    console.log(nothingWrapper.doTheNoise());

我实际上预计需要在其中使用 bind,但似乎并不必要。


-1

据我所知没有,但是您可以通过将函数初始化为null,然后再替换实现来模拟它。

var foo = null;
var bar = function() { alert(foo()); } // Appear to use foo before definition

// ...

foo = function() { return "ABC"; } /* Define the function */
bar(); /* Alert box pops up with "ABC" */

这个技巧类似于C#中实现递归lambda的技巧,如此处所述。

唯一的缺点是,如果在定义之前使用foo,你将会得到一个错误,因为尝试调用null作为函数,而不是更详细的错误消息。但是你期望在定义之前使用函数会得到某些错误消息。


1
仍然不是我想要的,因为它必须纯粹在运行时完成,而在你的例子中,你必须在设计时定义foo,而我可能在那个时候甚至不知道foo的名称。 - user310291
“你可能会期望在使用一个未定义的函数时得到一些错误信息” - 不,你不会。这就是方法缺失的全部意义所在。 - James Moore
你不需要在顶部将foo初始化为null。声明会被提升。只要在调用bar之前设置即可。无论如何,这并没有真正回答OP的问题。 - sstur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接