第十六章:JavaScript


第十六章:JavaScript

QtQt QuickQML

Mozilla Developer Network
表面来看,JavaScript是一门非常普通的语言,与其它语言差别不大:

function countDown() {
  for(var i=0; i<10; i++) {
    console.log('index: ' + i)
  }
}

function countDown2() {
  var i=10;
  while( i>0 ) {
    i--;
  }
}

但一定要注意,JS有函数作用域(变量生命周期),而C++ 有块作用域(详见 函数及函数域 )
if ... else,break,continue这些表达式也如预期那样工作。不限于整型值,switch case 也可以比较其它类型:

function getAge(name) {
  // switch over a string
  switch(name) {
  case "father":
    return 58;
  case "mother":
    return 56;
  }
  return unknown;
}

JS认为几种值可以视为false(如,false0""undefinednull)。比如,函数默认返回undefined。要验证false,得使用===严格相等操作符。==等值操作符会先做类型转换再来判断是否相等。如果可能的话,使用更快和更好的===严格相等操作符,它将判定一致性(详见比较操作符)。
在底层,javascript 有自己的处理方式。例如数组:

function doIt() {
  var a = [] // empty arrays
  a.push(10) // addend number on arrays
  a.push("Monkey") // append string on arrays
  console.log(a.length) // prints 2
  a[0] // returns 10
  a[1] // returns Monkey
  a[2] // returns undefined
  a[99] = "String" // a valid assignment
  console.log(a.length) // prints 100
  a[98] // contains the value undefined
}

对于习惯了C++ 或Java等面向对象语言的人来说,JS的运行机制有很大不同。JS不是纯粹的面向对象的语言,它是被称为基于原型的语言。每个对象都有原型对象。每个对象都是基于它的原型对象创建的。可以在Douglas Crockford编写的Javascript的优点一书中详细了解相关知识。
要测试JS的代码片段可以使用在线的JS 控制台 或构建一小段的QML代码:

import QtQuick 2.5

Item {
  function runJS() {
    console.log("Your JS code goes here");
  }
  Component.onCompleted: {
    runJS();
  }
}

JavaScript 手册
这里有一些QML与JS一起使用的简短的例子。关于在QML中如何使用JS,它会给你一些思路。

打印 QML 项目的所有键

Item {
    id: root
    Component.onCompleted: {
        var keys = Object.keys(root);
        for(var i=0; ivar key = keys[i];
            // prints all properties, signals, functions from object
            console.log(key + ' : ' + root[key]);
        }
    }
}

对象与JSON字符串间的相互转换

Item {
    property var obj: {
        key: 'value'
    }

    Component.onCompleted: {
        var data = JSON.stringify(obj);
        console.log(data);
        var obj = JSON.parse(data);
        console.log(obj.key); // > 'value'
    }
}

当前日期

Item {
    Timer {
        id: timeUpdater
        interval: 100
        running: true
        repeat: true
        onTriggered: {
            var d = new Date();
            console.log(d.getSeconds());
        }
    }
}

通过名字调用函数

Item {
    id: root

    function doIt() {
        console.log("doIt()")
    }

    Component.onCompleted: {
        // Call using function execution
        root["doIt"]();
        var fn = root["doIt"];
        // Call using JS call method (could pass in a custom this object and arguments)
        fn.call()
    }
}

创建一个JS控制台

我们将通过一个小例子来创建一个JS控制台。我们需要一个输入框,用户可以在其中输入他的 JS 表达式,预期应该有一个输出结果列表。由于这应该更像一个桌面应用程序,我们使用 Qt Quick Controls 模块。

注意
这个项目中的 JS 控制台对测试非常有益。增强的 Quake-Terminal 效果,也能够给客户留下深刻印象。要正确地使用它,需要控制 JS 控制台计算的范围,例如当前可见的屏幕,主要数据模型,单例核心对象,或以上所有因素。



我们用Qt Creator来创建Qt Quick UI project类型的工程,以使用Qt Quick 控件。给工程起个名为JSConsole。向导完成后,我们已经拥有了一个应用的基本结构,这个应用有一个窗体、一个退出菜单。
为了能输入,我们使用TextField和一个Button,以对输入内容进行计算。表达式的计算结果使用一个有列表模型ListModel的列表视图ListView来展示,两个标签来展示表达式和对其的计算结果。
我们的应用程序会分为两个文件:

  • JSConsole.qml:应用的主要视图
  • jsconsole.js:用于计算用户表达式的脚本库

JSConsole.qml

Application window 程序窗体

// JSConsole.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import "jsconsole.js" as Util

ApplicationWindow {
    id: root
    
    title: qsTr("JSConsole")
    width: 640
    height: 480
    visible: true

    menuBar: MenuBar {
        Menu {
            title: qsTr("File")
            MenuItem {
                text: qsTr("Exit")
                onTriggered: Qt.quit()
            }
        }
    }

Form

ColumnLayout {
    anchors.fill: parent
    anchors.margins: 9
    RowLayout {
        Layout.fillWidth: true
        TextField {
            id: input
            Layout.fillWidth: true
            focus: true
            onAccepted: {
                // call our evaluation function on root
                root.jsCall(input.text)
            }
        }
        Button {
            text: qsTr("Send")
            onClicked: {
                // call our evaluation function on root
                root.jsCall(input.text)
            }
        }
    }
    Item {
        Layout.fillWidth: true
        Layout.fillHeight: true
        Rectangle {
            anchors.fill: parent
            color: '#333'
            border.color: Qt.darker(color)
            opacity: 0.2
            radius: 2
        }

        ScrollView {
            id: scrollView
            anchors.fill: parent
            anchors.margins: 9
            ListView {
                id: resultView
                model: ListModel {
                    id: outputModel
                }
                delegate: ColumnLayout {
                    id: delegate
                    required property var model
                    width: ListView.view.width
                    Label {
                        Layout.fillWidth: true
                        color: 'green'
                        text: "> " + delegate.model.expression
                    }
                    Label {
                        Layout.fillWidth: true
                        color: delegate.model.error === "" ? 'blue' : 'red'
                        text: delegate.model.error === "" ? "" + delegate.model.result : delegate.model.error
                    }
                    Rectangle {
                        height: 1
                        Layout.fillWidth: true
                        color: '#333'
                        opacity: 0.2
                    }
                }
            }
        }
    }
}

调用脚本库

计算函数jsCall并非由其本身完成表达式的计算的,为清晰易读起见,这部分逻辑被移动到了JS模块(jsconsole.js)。

import "jsconsole.js" as Util
function jsCall(exp) {
    const data = Util.call(exp)
    // insert the result at the beginning of the list
    outputModel.insert(0, data)
}

注意
为了安全起见,我们不使用 JS 中的 eval 函数,因为这将允许用户修改本地作用域。我们使用 Function 构造函数在运行时创建一个 JS 函数,并将我们的作用域作为 this 变量传入。由于每次创建函数时它不充当闭包并存储自己的范围,因此我们需要使用 this.a = 10将值存储在函数的作用域内。此作用域由脚本设置为作用域变量。

jsconsole.js

// jsconsole.js
.pragma library

const scope = {
    // our custom scope injected into our function evaluation
}

function call(msg) {
    const exp = msg.toString()
    console.log(exp)
    const data = {
        expression : msg,
        result: "",
        error: ""
    }
    try {
        const fun = new Function('return (' + exp + ')')
        data.result = JSON.stringify(fun.call(scope), null, 2)
        console.log('scope: ' + JSON.stringify(scope, null, 2), 'result: ' + data.result)
    } catch(e) {
        console.log(e.toString())
        data.error = e.toString()
    }
    return data
}

调用函数返回的数据是一个带有返回值、表达式和错误属性的 JS 对象:data: { expression: "", result: "", error: "" }。我们可以直接在 ListModel 中使用这个 JS 对象,然后从委托中访问它,例如delegate.model.expression 为我们提供了输入的表达式。