QJSEngine评估的结果不包含函数

时间:2015-08-04 17:33:59

标签: javascript c++ qt qtscript qjsengine

我正在将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());

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

1 个答案:

答案 0 :(得分:5)

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

javascript-evaluate-result

您无法调用foo()的{​​{1}}函数,因为它不存在。你可以做的是通过全局对象调用它:

javascript-call

这与您的C ++代码看到的相同。因此,要访问和调用undefined功能,您需要通过foo()的{​​{3}}功能访问它:

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 (!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来调用多个脚本,以这种方式编写JavaScript代码会导致问题,特别是如果其中的函数具有相同的名称。具体来说,您评估的对象将永远留在全局命名空间中。

在您评估代码之前,

QJSEngineQScriptEngineQScriptContext形式提供了解决此问题的新方法,之后又push()。但是,globalObject()

解决此问题的一种方法是为每个脚本创建一个新的pop()。我没试过,我不确定它会有多贵。

no such API exists in QJSEngine,但我不太明白它如何与每个脚本的多个函数一起使用。

在与同事交谈后,我了解了一种使用documentation looked like it might hint at another way around it解决问题的方法:

QJSEngine

此代码的输出为:

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

您可以在我刚刚链接到的文章中详细了解此方法。以下是它的摘要:

  • 声明一个对象,只要定义需要“导出”到C ++的内容,就可以添加属性。
  • “模块函数”将其接口对象作为参数(Result of call: "foo" ),允许函数外部的代码创建它并将其存储在 变量(exports)。

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

  

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

如果您想进一步了解,请按照文章进行操作。但是,只要您使用唯一的对象名称,就可以了。

以下是“现实生活”脚本如何更改以适应此解决方案的示例:

之前

(this.object = {})

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 = {}); 脚本可以包含具有相同名称(Caractivate等)的函数,而不会影响destroy的函数。

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

  

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

所有需要做的就是将文件重命名为QJSEngine扩展名,然后将代码转换为:

.mjs

调用其中一个函数的C ++看起来像这样:

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