ECMAScript 是 JavaScript 语言的标准;浏览器、Node.js 和 Qt QML 则为这门语言提供不同的宿主环境。共享语言语法,不代表共享 DOM、文件接口、网络接口或所有新版本特性。写 QML 脚本时,应同时查语言标准和当前 Qt 版本。
本文 QML 例子按 Qt 6.10 编写,并在 Qt 6.10.2 上验证。该版本的 Qt 文档以 ECMAScript 第 7 版为基础,同时支持部分后来加入的语法,例如空值合并、可选链和数值分隔符。原稿直接把 Promise/async/await 并列为通用 QML 功能不准确:本机 Qt 6.10.2 引擎支持 Promise,但不接受 async function、await 或 BigInt 字面量,也没有 BigInt 构造函数。
与 C、C++、Python 比较时分清对象与名字
跳转到“与 C、C++、Python 比较时分清对象与名字”| 项目 | JavaScript/ECMAScript | C 与 C++ | Python |
|---|---|---|---|
| 变量与类型 | let、const、var 绑定名字;值具有运行时类型 | 通常静态确定类型,也有类型推导等机制 | 名字可重新绑定到不同类型的对象;不是“变量类型永远不能变” |
| 转换 | 某些运算会隐式转换;=== 与 == 规则不同 | 也存在隐式转换,具体规则依语言和类型 | 部分运算有数值转换,但不会因写加号就自动把任意数字当字符串拼接 |
| 语句与注释 | 常用分号;自动分号插入有具体规则;//、/* ... */ | 常用分号和花括号;支持行注释与块注释 | 换行和缩进组织语句;# 是注释,三引号首先是字符串语法,可用于文档字符串 |
| 函数 | function、函数表达式、箭头函数;函数是对象 | C 有函数与函数指针,C++ 还有成员函数和 lambda 等 | def、lambda,函数也是对象 |
| 类与对象 | class 是原型对象系统上的语法 | C 没有 C++ 的 class;C++ 有类与继承 | 有类和继承机制 |
| 数组与映射 | Array 是对象;普通对象与 Map 的用途不同 | C 数组/结构体;C++ 另有 std::array、std::vector、映射容器等 | 列表、元组、字典等对象 |
| 作用域 | let/const 有块作用域;var 主要依函数或脚本/模块环境 | 块、函数、命名空间等规则依语言而定 | 普通 if/for 不创建新的局部作用域;函数、模块、类和推导式有各自规则 |
| 资源管理 | 垃圾回收管理对象内存;外部资源仍可能需要显式关闭 | C 常显式管理资源;C++ 常用 RAII、容器和智能指针 | 垃圾回收与引用管理不替代文件等资源的 with 或显式关闭 |
| 模块 | ECMAScript 模块使用 import/export;QML 也有自己的导入语法 | #include 是预处理包含;现代 C++ 还支持模块,不能统称为同一机制 | 常用 import |
“动态/静态”“强/弱类型”只概括部分特征,不能代替某个运算的具体规则。JavaScript 的 const 限制名字重新绑定,并不冻结对象内部;Python 名字也可从一个整数重新绑定到一个字符串。
类型、条件、循环和函数的完整例子
跳转到“类型、条件、循环和函数的完整例子”ECMAScript 的原始值包括 undefined、null、布尔值、字符串、数值、Symbol,以及较新标准中的 BigInt;对象是另一大类,数组和函数都属于对象。typeof null 的结果是历史遗留的 "object",因此不能单凭这个结果把它当成普通可用对象。数组判断可用 Array.isArray。
以下程序只使用本篇 Qt 基线支持的语法。可以保存为 basics.js 交给 JavaScript 引擎求值;最后一行返回检查结果。函数声明 add 和箭头函数 addArrow 使用不同名字,避免原稿把两个 add 声明放在同一作用域后发生冲突。
function runBasics() { function check(ok, message) { if (!ok) throw new Error(message); }
let a = 10; const b = 20; var c = 30; const record = { name: "Tom", age: 18 }; record.age = 19; check(record.age === 19, "const does not freeze object properties");
let choice; if (a > 5) { choice = "greater"; } else if (a === 5) { choice = "equal"; } else { choice = "less"; } check(choice === "greater", "condition");
let total = 0; for (let i = 0; i < 10; i++) total += i; let cursor = 0; while (cursor < 3) cursor++; const values = [1, 2, 3, 4]; let arrayTotal = 0; for (const value of values) arrayTotal += value; check(total === 45 && cursor === 3 && arrayTotal === 10, "loops");
function add(x, y) { return x + y; } const addArrow = (x, y) => x + y; check(add(a, b) === 30 && addArrow(b, c) === 50, "functions");
let outer = 1; { let outer = 2; var functionScoped = 3; check(outer === 2, "block binding"); } check(outer === 1 && functionScoped === 3, "scope"); check(1 == "1" && !(1 === "1"), "coercing versus strict equality"); check(typeof undefined === "undefined" && typeof null === "object", "null boundary"); check(typeof Symbol("x") === "symbol" && Array.isArray(values), "types");
class Animal { constructor(name) { this.name = name; } speak() { return this.name + " makes a noise."; } } class Dog extends Animal { speak() { return this.name + " barks."; } } const dog = new Dog("Milo"); check(dog instanceof Dog && dog instanceof Animal, "inheritance"); check(dog.speak() === "Milo barks.", "override");
const { name, age } = record; const [first, second] = values; const unique = new Set([1, 1, 2]); const map = new Map([["name", name], ["age", age]]); const keys = []; for (const key in record) { if (Object.prototype.hasOwnProperty.call(record, key)) keys.push(key); } check(first + second === 3 && unique.size === 2, "destructure and set"); check(map.get("name") === "Tom" && keys.indexOf("age") >= 0, "map and own keys"); return `${name}: ${age}; total=${arrayTotal}; ${dog.speak()}`;}
runBasics();结果为 Tom: 19; total=10; Milo barks.。for...of 读取可迭代对象产生的值;for...in 枚举可枚举字符串属性名,也可能涉及原型链,因此这里检查自有属性。普通对象不是天然的 for...of 可迭代对象,不能把两种循环机械互换。
箭头函数还有词法 this 等语义,不只是省几个字的普通函数。类继承、构造调用和属性查询仍应按对象协议理解,不能把 JavaScript 的 class 当成 C++ 对象布局的直接对应物。
QML 属性绑定与 JavaScript 对象变化
跳转到“QML 属性绑定与 JavaScript 对象变化”QML 脚本适合处理界面事件、计算属性和调用已暴露的 C++ 对象。不能仅因浏览器教程能运行某段代码,就假定 QML 存在 window、document 或 Node.js 的 require、文件系统接口。
下面保存为 JavaScriptDemo.qml。settings 是一个 var 属性,其内部普通对象成员被修改时,不会自动等价于整个 QML 属性发出变更通知;用一个新对象替换 settings 会触发对应绑定重新求值。
import QtQml
QtObject { id: root property var settings: ({ count: 1 }) property string display: "count=" + root.settings.count property bool promiseDone: false property int promiseValue: 0 property string promiseError: ""
function mutateMember() { root.settings.count += 1 } function replaceObject() { root.settings = { count: root.settings.count + 1 } } function startPromise() { root.promiseDone = false root.promiseError = "" Promise.resolve(5) .then(function(value) { root.promiseValue = value * 2 root.promiseDone = true }) .catch(function(error) { root.promiseError = String(error) }) }}创建后 display 为 count=1。调用 mutateMember() 后内部数值变为 2,但展示绑定不会因此自动得到一次 settings 变更通知;再调用 replaceObject(),新数值为 3,display 更新为 count=3。复杂共享数据可采用明确的 QML 属性、列表模型或 C++ 通知接口,避免把普通 JavaScript 对象内部变化当成自动通知系统。
调用 startPromise() 后,.then() 的回调由事件循环处理,完成时 promiseValue 为 10。Promise 的存在不意味着同步计算自动移到工作线程,也不保证宿主支持 async/await;长时间循环仍会阻塞执行它的线程。
模块与版本检查
跳转到“模块与版本检查”QML 中的 import QtQuick 导入 QML 模块,不是 JavaScript 的 import { name } from "module" 语句。QML 可以通过 import "logic.js" as Logic 使用脚本文件,也支持相应的 ECMAScript 模块资源;文件扩展名、导出方式和共享状态规则应按 JavaScript 资源导入选择,不能把浏览器或 Node 项目原封不动贴进 QML。
本地 Qt 6.10.2 实测 Promise、类继承、箭头函数、模板字符串、解构、Map、Set、for...of 可用;BigInt 字面量和 async/await 解析失败。后者可以作为学习现代 ECMAScript 的主题,但应在明确支持它们的宿主中练习。升级 Qt 后也应复核自己的目标环境,避免把一个版本的结论当成永久语言限制。
语言规则见 ECMA-262 第 7 版,Qt 范围见 Qt 6.10 JavaScript 宿主环境和 JavaScript 与 QML。原笔记推荐的 MDN JavaScript 指南可作为语言学习资料,涉及宿主 API 时仍需区别浏览器与 QML。