一、强制严格的类型检查
动态类型语言如JavaScript(简称JS)虽能提升开发效率,但也容易在运行时引发非预期错误。例如未检查的undefined值可能导致程序崩溃,这类问题若能在开发阶段发现将显著提升稳定性。TypeScript(TS)通过类型标注机制,使编译器能在编译时检测出多数类型错误,但其非强制类型系统仍存在局限。例如未标注类型的变量会阻碍完整编译检查。ArkTS通过强制静态类型系统克服这一缺陷,实施更严格的类型验证机制,从而最大限度减少运行时错误的发生。
- ArkTS要求类的所有属性在声明时或者在构造函数中显式地初始化。如果属性类型可以是undefined,其类型应在代码中精确标注。如:
name?: string
二、程序性能
为了确保程序的正确性,动态类型语言需要在运行时检查对象的类型。例如JavaScript不允许访问undefined的属性。检查一个值是否为undefined的唯一方法是在运行时进行类型检查。所有JavaScript引擎都会执行以下操作:如果一个值不是undefined,则可以访问其属性;如果尝试访问的值是undefined,则会抛出异常。虽然现代JavaScript引擎可以优化这类操作,但仍然存在一些无法消除的运行时检查,这会导致程序变慢。由于TypeScript代码总是先被编译成JavaScript代码,因此在TypeScript中也会遇到相同的问题。ArkTS解决了这个问题。通过启用静态类型检查,ArkTS代码将被编译成方舟字节码文件,而不是JavaScript代码。因此,ArkTS运行速度更快,更容易被进一步优化。
Null Safety
function notify(who: string, what: string) { console.info(`Dear ${who}, a message for you: ${what}`);}
notify('Jack', 'You look great today');
在大多数情况下,函数notify会接受两个string类型的变量作为输入,产生一个新的字符串。但是,如果将一些特殊值作为输入,例如notify(null, undefined),情况会怎么样呢?
程序仍会正常运行,输出预期值:Dear null, a message for you: undefined。虽然系统表现一切正常,但值得注意的是,为了保障该场景下程序的正确性,引擎在运行时会持续进行类型检查,其实现机制类似于以下伪代码所示:
function __internal_tostring(s: any): string { if (typeof s === 'string') return s; if (s === undefined) return 'undefined'; if (s === null) return 'null'; // ...}
试想一下,如果notify函数并非只是简单的日志打印,而是某些高负载场景下关键逻辑的一部分,那么在运行时频繁执行类似__internal_tostring的类型检查操作,势必会带来显著的性能开销。
如果可以保证在运行时,只有string类型的值(不会是其他类型的值,例如null或者undefined)可以被传入函数notify呢?在这种情况下,因为可以确保没有其他边界情况,像__internal_tostring的检查就是多余的了。在该场景下,这种机制被称为“null-safety”(空安全),其核心目的是确保null不能作为合法的字符串类型值。如果ArkTS支持这一特性,那么任何类型不匹配的代码都将在编译阶段被拦截,无法编译通过。
function notify(who: string, what: string) { console.info(`Dear ${who}, a message for you: ${what}`);}
notify('Jack', 'You look great today');notify(null, undefined); // 编译时错误
TS通过启用编译选项strictNullChecks实现此特性。虽然TS被编译成JS,但因为JS没有这个特性,所以严格null检查仅在编译时起效。从程序稳定性和性能的角度考虑,ArkTS将“null-safety”视为一个重要的特性。因此,ArkTS强制进行严格null检查,在ArkTS中上述代码将会编译失败。作为交换,此类代码为ArkTS引擎提供了更多信息和关于值的类型保证,有助于优化性能。
三 .ets代码兼容性
在API version 10之前的版本中,ArkTS(以.ets为扩展名的文件)在语法层面完全遵循标准的TypeScript规范。从API version 10 Release起,明确定义ArkTS的语法规则,同时,SDK增加了在编译流程中对.ets文件的ArkTS语法检查,通过编译告警或编译失败提示开发者适配新的ArkTS语法。
根据工程的compatibleSdkVersion,具体策略如下:
- compatibleSdkVersion >= 10 为标准模式。在该模式下,所有.ets文件必须严格遵循ArkTS语法规则,任何语法违规工程都会编译不通过,开发者需要修正所有语法问题后才能获得编译通过。
- compatibleSdkVersion < 10 为兼容模式。在该模式下,对.ets文件以warning形式提示违反ArkTS语法规则的所有代码。尽管违反ArkTS语法规则的工程在兼容模式下仍可编译成功,但需完全适配ArkTS语法后方可在标准模式下编译成功。
四、方舟运行时兼容TS/JS
在API version 11上,HarmonyOS SDK中的TypeScript版本为4.9.5,target字段为es2017。应用中支持使用ECMA2017及更高版本的语法进行TS/JS开发。
应用环境限制
- 强制使用严格模式(use strict)
- 禁止使用eval()
- 禁止使用with() {}
- 禁止以字符串为代码创建函数
- 禁止循环依赖
与标准TS/JS的差异
在标准的TS/JS中,JSON的数字格式要求小数点后必须跟随数字,例如 2.e3 这类科学计数法不被允许,会导致 SyntaxError。方舟运行时则支持这类科学计数法。
差异细节
-
使用let而非var
-
强制使用静态类型,ArkTS中禁止使用any类型, 使用具体的类型而非any或unknown,显式指定具体类型
-
禁止在运行时变更对象布局。为实现最佳性能,ArkTS要求在程序执行期间不能更改对象的布局。换句话说,ArkTS禁止以下行为:
- 向对象中添加新的属性或方法。
- 从对象中删除已有的属性或方法。
- 将任意类型的值赋值给对象属性。
-
限制运算符的语义
// 一元运算符`+`只能作用于数值类型: let t = +42; // 合法运算 let s = +'42'; // 编译时错误 -
对象的属性名必须是合法的标识符
-
不支持以#开头的私有字段
TypeScript
class C { #foo: number = 42}
ArkTS
class C { private foo: number = 42}
- 类型、命名空间的命名必须唯一
TypeScript
let X: stringtype X = number[] // 类型的别名与变量同名
ArkTS
let X: stringtype T = number[] // 为避免名称冲突,此处不允许使用X
- 不支持this类型
TypeScript
interface ListItem { getHead(): this}
class C { n: number = 0
m(c: this) { // ... }}
ArkTS
interface testListItem { getHead(): testListItem}
class C { n: number = 0
m(c: C) { // ... }}
-
不支持在constructor中声明字段
ArkTS禁止在构造函数中声明类字段,所有字段都必须在class作用域内显示声明。
TypeScript
class Person { constructor( protected ssn: string, private firstName: string, private lastName: string ) { this.ssn = ssn; this.firstName = firstName; this.lastName = lastName; }
getFullName(): string { return this.firstName + ' ' + this.lastName; }}
ArkTS
class Person { protected ssn: string private firstName: string private lastName: string
constructor(ssn: string, firstName: string, lastName: string) { this.ssn = ssn; this.firstName = firstName; this.lastName = lastName; }
getFullName(): string { return this.firstName + ' ' + this.lastName; }}
-
不支持索引访问类型
ArkTS不支持动态声明字段,不支持动态访问字段。只能访问已在类中声明或者继承可见的字段,访问其他字段将会造成编译时错误。
使用点操作符访问字段,例如(obj.field),不支持索引访问(obj['field'])。
ArkTS支持通过索引访问TypedArray(例如Int32Array)中的元素。
- 需要显式标注对象字面量的类型
在 ArkTS 中,需要显式标注对象字面量的类型,否则将导致编译时错误。在某些场景下,编译器可以根据上下文推断出字面量的类型。
在以下上下文中不支持使用字面量初始化类和接口:
- 初始化具有any、Object或object类型的任何对象
- 初始化带有方法的类或接口
- 初始化包含自定义含参数的构造函数的类
- 初始化带readonly字段的类
11. 数组字面量必须仅包含可推断类型的元素
ArkTS将数组字面量的类型推断为所有元素的联合类型。如果其中任何一个元素的类型无法推导,则在编译时会发生错误。
TypeScript
let a = [{n: 1, s: '1'}, {n: 2, s: '2'}];
ArkTS
class C { n: number = 0 s: string = ''}
let a1 = [{n: 1, s: '1'} as C, {n: 2, s: '2'} as C]; // a1的类型为“C[]”let a2: C[] = [{n: 1, s: '1'}, {n: 2, s: '2'}]; // a2的类型为“C[]”
- 使用箭头函数而非函数表达式
TypeScript
let f = function (s: string) { console.info(s);}
ArkTS
let f = (s: string) => { console.info(s);}
- 不支持使用类表达式,必须显式声明一个类
TypeScript
const Rectangle = class { constructor(height: number, width: number) { this.height = height; this.width = width; }
height; width;}
const rectangle = new Rectangle(0.0, 0.0);
ArkTS
class testRectangle { constructor(testHeight: number, testWidth: number) { this.testHeight = testHeight; this.testWidth = testWidth; }
testHeight: number; testWidth: number;}
const rectangle = new testRectangle(0.0, 0.0);
- 只有接口可以被implements,类不允许被implements。
TypeScript
class C { foo() {}}
class C1 implements C { foo() {}}
ArkTS
interface C { foo(): void}
class C1 implements C { foo() {}}
- 不支持修改对象的方法
ArkTS不支持修改对象的方法。在静态语言中,对象布局固定,类的所有实例共享同一个方法。
若需为特定对象添加方法,可封装函数或采用继承机制。
TypeScript
class C { foo() { console.info('foo'); }}
function bar() { console.info('bar');}
let c1 = new C();let c2 = new C();c2.foo = bar;
c1.foo(); // fooc2.foo(); // bar
ArkTS
class C { foo() { console.info('foo'); }}
class Derived extends C { foo() { console.info('Extra'); super.foo(); }}
function bar() { console.info('bar');}
let c1 = new C();let c2 = new C();c1.foo(); // fooc2.foo(); // foo
let c3 = new Derived();c3.foo(); // Extra foo
- 仅允许在表达式中使用typeof运算符
ArkTS仅支持在表达式中使用typeof运算符,不允许使用typeof作为类型。
TypeScript
let n1 = 42;let s1 = 'foo';console.info(typeof n1); // 'number'console.info(typeof s1); // 'string'let n2: typeof n1let s2: typeof s1
ArkTS
let n1 = 42;let s1 = 'foo';console.info(typeof n1); // 'number'console.info(typeof s1); // 'string'let n2: numberlet s2: string
- 不支持in运算符
在ArkTS中,对象布局在编译时已知且运行时无法修改,因此不支持in运算符。需要检查类成员是否存在时,使用instanceof代替。
TypeScript
class Person { name: string = ''}let p = new Person();
let b = 'name' in p; // true
ArkTS
class Person { name: string = ''}let p = new Person();
let b = p instanceof Person; // true,且属性name一定存在
- 不支持在catch语句标注类型
TypeScript的catch语句中,只能标注any或unknown类型。ArkTS不支持这些类型,应省略类型标注。
TypeScript
try { // ...} catch (a: unknown) { // 处理异常}
ArkTS
try { // ...} catch (a) { // 处理异常}
- 不支持for .. in
- 限制throw语句中表达式的类型
ArkTS只支持抛出Error类或其派生类的实例。禁止抛出其他类型的数据,例如number或string。
TypeScript
throw 4;throw '';throw new Error();
ArkTS
throw new Error();
- 限制省略函数返回类型标注
ArkTS在部分场景中支持对函数返回类型进行推断。当return语句中的表达式是对某个函数或方法进行调用,且该函数或方法的返回类型没有被显著标注时,会出现编译时错误。在这种情况下,请标注函数返回类型。
- 不支持在函数内声明函数
ArkTS不支持在函数内声明函数,改用lambda函数。
TypeScript
function addNum(a: number, b: number): void {
// 函数内声明函数 function logToConsole(message: string): void { console.info(message); }
let result = a + b;
// 调用函数 logToConsole('result is ' + result);}
ArkTS
function addNum(a: number, b: number): void { // 使用lambda函数代替声明函数 let logToConsole: (message: string) => void = (message: string): void => { console.info(message); }
let result = a + b;
logToConsole('result is ' + result);}
- 不支持在函数和类的静态方法中使用this
ArkTS中this只能在类的实例方法中使用,不支持在函数和类的静态方法中使用。
TypeScript
function foo(i: string) { this.count = i; // 只有在开启noImplicitThis选项时会产生编译时错误}
class A { count: string = 'a' m = foo}
let a = new A();console.info(a.count); // 打印aa.m('b');console.info(a.count); // 打印b
ArkTS
class A { count: string = 'a' m(i: string): void { this.count = i; }}
function main(): void { let a = new A(); console.info(a.count); // 打印a a.m('b'); console.info(a.count); // 打印b}
- 不支持生成器函数
目前ArkTS不支持生成器函数,可使用async或await机制处理并行任务。
TypeScript
function* counter(start: number, end: number) { for (let i = start; i <= end; i++) { yield i; }}
for (let num of counter(1, 5)) { console.info(num.toString());}
ArkTS
async function complexNumberProcessing(num: number): Promise<number> { // ... return num;}
async function foo() { for (let i = 1; i <= 5; i++) { await complexNumberProcessing(i); }}
foo()
- 接口不能继承类
在ArkTS中,接口不能继承类,只能继承其他接口。
TypeScript
class Control { state: number = 0}
interface SelectableControl extends Control { select(): void}
ArkTS
interface Control { state: number}
interface SelectableControl extends Control { select(): void}
- 只能使用类型相同的编译时表达式初始化枚举成员
ArkTS不支持使用运行期间计算的表达式初始化枚举成员。枚举中所有显式初始化的成员必须具有相同类型。
TypeScript
enum E1 { A = 0xa, B = 0xb, C = Math.random(), D = 0xd, E // 推断出0xe}
enum E2 { A = 0xa, B = '0xb', C = 0xc, D = '0xd'}
ArkTS
enum E1 { A = 0xa, B = 0xb, C = 0xc, D = 0xd, E // 推断出0xe}
enum E2 { A = '0xa', B = '0xb', C = '0xc', D = '0xd'}
参考文档: