【问题标题】:Result of QJSEngine evaluation doesn't contain a functionQJSEngine 评估结果不包含函数
【发布时间】:2015-08-04 17:33:59
【问题描述】:

我正在将 QScriptEngine 代码迁移到 QJSEngine,并且遇到了一个问题,即在评估脚本后我无法调用函数:

#include <QCoreApplication>
#include <QtQml>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QJSEngine engine;
    QJSValue evaluationResult = engine.evaluate("function foo() { return \"foo\"; }");

    if (evaluationResult.isError()) {
        qWarning() << evaluationResult.toString();
        return 1;
    }

    if (!evaluationResult.hasProperty("foo")) {
        qWarning() << "Script has no \"foo\" function";
        return 1;
    }

    if (!evaluationResult.property("foo").isCallable()) {
        qWarning() << "\"foo\" property of script is not callable";
        return 1;
    }

    QJSValue callResult = evaluationResult.property("foo").call();
    if (callResult.isError()) {
        qWarning() << "Error calling \"foo\" function:" << callResult.toString();
        return 1;
    }

    qDebug() << "Result of call:" << callResult.toString();

    return 0;
}

这个脚本的输出是:

 Script has no "activate" function

当我使用QScriptEngine时可以调用相同的函数:

 scriptEngine->currentContext()->activationObject().property("foo").call(scriptEngine->globalObject());

为什么函数不作为评估结果的属性存在,我该如何调用它?

【问题讨论】:

    标签: javascript c++ qt qtscript qjsengine


    【解决方案1】:

    该代码将导致foo() 被评估为全局范围内的函数声明。由于您不调用它,因此生成的QJSValueundefined。您可以通过在浏览器中打开 JavaScript 控制台并编写同一行来查看相同的行为:

    你不能调用undefined的函数foo(),因为它不存在。你可以做的是通过全局对象调用它:

    这与您的 C++ 代码所看到的相同。因此,要访问和调用foo()函数,需要通过QJSEngineglobalObject()函数访问:

    #include <QCoreApplication>
    #include <QtQml>
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
    
        QJSEngine engine;
        QJSValue evaluationResult = engine.evaluate("function foo() { return \"foo\"; }");
    
        if (evaluationResult.isError()) {
            qWarning() << evaluationResult.toString();
            return 1;
        }
    
        if (!engine.globalObject().hasProperty("foo")) {
            qWarning() << "Script has no \"foo\" function";
            return 1;
        }
    
        if (!engine.globalObject().property("foo").isCallable()) {
            qWarning() << "\"foo\" property of script is not callable";
            return 1;
        }
    
        QJSValue callResult = engine.globalObject().property("foo").call();
        if (callResult.isError()) {
            qWarning() << "Error calling \"foo\" function:" << callResult.toString();
            return 1;
        }
    
        qDebug() << "Result of call:" << callResult.toString();
    
        return 0;
    }
    

    这段代码的输出是:

    Result of call: "foo"
    

    这与您发布的使用QScriptEngine 的行大致相同。

    这种方法的好处是您无需触摸脚本即可使其工作。

    不利的一面是,如果您打算重复使用相同的 QJSEngine 来调用多个脚本,尤其是其中的函数具有相同的名称时,以这种方式编写 JavaScript 代码可能会导致问题。具体来说,您评估的对象将永远存在于全局命名空间中。

    QScriptEngineQScriptContext 的形式解决了这个问题:push() 在评估代码之前有一个新的上下文,然后是pop()。但是,no such API exists in QJSEngine

    解决此问题的一种方法是为每个脚本创建一个新的QJSEngine。我没试过,不知道会贵多少。

    documentation looked like it might hint at another way around it,但我不太明白它如何与每个脚本的多个函数一起工作。

    在与同事交谈后,我了解到一种使用an object as an interface 解决问题的方法:

    #include <QCoreApplication>
    #include <QtQml>
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
    
        QJSEngine engine;
        QString code = QLatin1String("( function(exports) {"
            "exports.foo = function() { return \"foo\"; };"
            "exports.bar = function() { return \"bar\"; };"
        "})(this.object = {})");
    
        QJSValue evaluationResult = engine.evaluate(code);
        if (evaluationResult.isError()) {
            qWarning() << evaluationResult.toString();
            return 1;
        }
    
        QJSValue object = engine.globalObject().property("object");
        if (!object.hasProperty("foo")) {
            qWarning() << "Script has no \"foo\" function";
            return 1;
        }
    
        if (!object.property("foo").isCallable()) {
            qWarning() << "\"foo\" property of script is not callable";
            return 1;
        }
    
        QJSValue callResult = object.property("foo").call();
        if (callResult.isError()) {
            qWarning() << "Error calling \"foo\" function:" << callResult.toString();
            return 1;
        }
    
        qDebug() << "Result of call:" << callResult.toString();
    
        return 0;
    }
    

    这段代码的输出是:

    Result of call: "foo"
    

    您可以在我刚刚链接到的文章中详细了解这种方法。总结如下:

    • 声明一个对象,您可以在定义需要“导出”到 C++ 的内容时为其添加属性。
    • “模块函数”将其接口对象作为参数 (exports),允许函数外部的代码创建它并将其存储在 一个变量 ((this.object = {}))。

    但是,正如文章所述,这种方法仍然使用全局范围:

    前面的模式通常被用于浏览器的 JavaScript 模块使用。该模块将声明一个全局变量并将其代码包装在一个函数中,以便拥有自己的私有命名空间。但是,如果多个模块碰巧声明了相同的名称,或者如果您想同时加载一个模块的两个版本,这种模式仍然会导致问题。

    如果您想更进一步,请按照文章进行到底。不过,只要您使用唯一的对象名称,就可以了。

    这是一个示例,说明“现实生活”脚本将如何改变以适应此解决方案:

    之前

    function activate(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
        gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
    }
    
    function equipped(thisEntity, ownerEntity) {
        var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
        sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
    
        var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
        physicsComponent.width = sceneItemComponent.sceneItem.width;
        physicsComponent.height = sceneItemComponent.sceneItem.height;
    }
    
    function unequipped(thisEntity, ownerEntity) {
        var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
        sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
    
        var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
        physicsComponent.width = sceneItemComponent.sceneItem.width;
        physicsComponent.height = sceneItemComponent.sceneItem.height;
    }
    
    function destroy(thisEntity, gameController) {
    }
    

    之后

    ( function(exports) {
        exports.activate = function(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
            gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
        }
    
        exports.equipped = function(thisEntity, ownerEntity) {
            var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
            sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
    
            var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
            physicsComponent.width = sceneItemComponent.sceneItem.width;
            physicsComponent.height = sceneItemComponent.sceneItem.height;
        }
    
        exports.unequipped = function(thisEntity, ownerEntity) {
            var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
            sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
    
            var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
            physicsComponent.width = sceneItemComponent.sceneItem.width;
            physicsComponent.height = sceneItemComponent.sceneItem.height;
        }
    
        exports.destroy = function(thisEntity, gameController) {
        }
    })(this.Pistol = {});
    

    Car 脚本可以具有同名的函数(activatedestroy 等),而不会影响 Pistol 的函数。


    从 Qt 5.12 开始,QJSEngine 支持正确的 JavaScript 模块:

    对于更大的功能,您可能希望将代码和数据封装到模块中。模块是一个包含脚本代码、变量等的文件,并使用导出语句来描述其与应用程序其余部分的接口。在 import 语句的帮助下,一个模块可以引用其他模块的功能。这允许以安全的方式从较小的连接构建块构建脚本应用程序。相比之下,使用 evaluate() 的方法存在内部变量或函数从一个 evaluate() 调用意外污染全局对象并影响后续评估的风险。

    只需将文件重命名为具有.mjs 扩展名,然后像这样转换代码:

    export function activate(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
        gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
    }
    
    export function equipped(thisEntity, ownerEntity) {
        var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
        sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
    
        var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
        physicsComponent.width = sceneItemComponent.sceneItem.width;
        physicsComponent.height = sceneItemComponent.sceneItem.height;
    }
    
    export function unequipped(thisEntity, ownerEntity) {
        var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
        sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
    
        var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
        physicsComponent.width = sceneItemComponent.sceneItem.width;
        physicsComponent.height = sceneItemComponent.sceneItem.height;
    }
    
    export function destroy(thisEntity, gameController) {
    }
    

    调用这些函数之一的 C++ 如下所示:

    QJSvalue module = engine.importModule("pistol.mjs");
    QJSValue function = module.property("activate");
    QJSValue result = function.call(args);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-23
      • 1970-01-01
      • 1970-01-01
      • 2011-07-17
      • 2015-11-03
      • 1970-01-01
      相关资源
      最近更新 更多