ArkTs 和 Ts 的区别

214 阅读11分钟

一、强制严格的类型检查

动态类型语言如JavaScript(简称JS)虽能提升开发效率,但也容易在运行时引发非预期错误。例如未检查的undefined值可能导致程序崩溃,这类问题若能在开发阶段发现将显著提升稳定性。TypeScript(TS)通过类型标注机制,使编译器能在编译时检测出多数类型错误,但其非强制类型系统仍存在局限。例如未标注类型的变量会阻碍完整编译检查。ArkTS通过强制静态类型系统克服这一缺陷,实施更严格的类型验证机制,从而最大限度减少运行时错误的发生。

  1. 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开发。

应用环境限制

  1. 强制使用严格模式(use strict)
  2. 禁止使用eval()
  3. 禁止使用with() {}
  4. 禁止以字符串为代码创建函数
  5. 禁止循环依赖

与标准TS/JS的差异

在标准的TS/JS中,JSON的数字格式要求小数点后必须跟随数字,例如 2.e3 这类科学计数法不被允许,会导致 SyntaxError。方舟运行时则支持这类科学计数法。

差异细节

  1. 使用let而非var

  2. 强制使用静态类型,ArkTS中禁止使用any类型, 使用具体的类型而非any或unknown,显式指定具体类型

  3. 禁止在运行时变更对象布局。为实现最佳性能,ArkTS要求在程序执行期间不能更改对象的布局。换句话说,ArkTS禁止以下行为:

    • 向对象中添加新的属性或方法。
    • 从对象中删除已有的属性或方法。
    • 将任意类型的值赋值给对象属性。
  4. 限制运算符的语义

    // 一元运算符`+`只能作用于数值类型:
    let t = +42; // 合法运算
    let s = +'42'; // 编译时错误
    
  5. 对象的属性名必须是合法的标识符

  6. 不支持以#开头的私有字段

TypeScript

class C {  #foo: number = 42}

ArkTS

class C {  private foo: number = 42}
  1. 类型、命名空间的命名必须唯一

TypeScript

let X: stringtype X = number[] // 类型的别名与变量同名

ArkTS

let X: stringtype T = number[] // 为避免名称冲突,此处不允许使用X
  1. 不支持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) {    // ...  }}
  1. 不支持在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;  }}
  1. 不支持索引访问类型

    ArkTS不支持动态声明字段,不支持动态访问字段。只能访问已在类中声明或者继承可见的字段,访问其他字段将会造成编译时错误。

使用点操作符访问字段,例如(obj.field),不支持索引访问(obj['field'])。

ArkTS支持通过索引访问TypedArray(例如Int32Array)中的元素。

  1. 需要显式标注对象字面量的类型

在 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[]
  1. 使用箭头函数而非函数表达式

TypeScript

let f = function (s: string) {  console.info(s);}

ArkTS

let f = (s: string) => {  console.info(s);}
  1. 不支持使用类表达式,必须显式声明一个类

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);
  1. 只有接口可以被implements,类不允许被implements。

TypeScript

class C {  foo() {}}
class C1 implements C {  foo() {}}

ArkTS

interface C {  foo(): void}
class C1 implements C {  foo() {}}
  1. 不支持修改对象的方法

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
  1. 仅允许在表达式中使用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
  1. 不支持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一定存在
  1. 不支持在catch语句标注类型

TypeScript的catch语句中,只能标注any或unknown类型。ArkTS不支持这些类型,应省略类型标注。

TypeScript

try {  // ...} catch (a: unknown) {  // 处理异常}

ArkTS

try {  // ...} catch (a) {  // 处理异常}
  1. 不支持for .. in
  2. 限制throw语句中表达式的类型

ArkTS只支持抛出Error类或其派生类的实例。禁止抛出其他类型的数据,例如number或string。

TypeScript

throw 4;throw '';throw new Error();

ArkTS

throw new Error();
  1. 限制省略函数返回类型标注

ArkTS在部分场景中支持对函数返回类型进行推断。当return语句中的表达式是对某个函数或方法进行调用,且该函数或方法的返回类型没有被显著标注时,会出现编译时错误。在这种情况下,请标注函数返回类型。

  1. 不支持在函数内声明函数

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);}
  1. 不支持在函数和类的静态方法中使用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}
  1. 不支持生成器函数

目前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()
  1. 接口不能继承类

在ArkTS中,接口不能继承类,只能继承其他接口。

TypeScript

class Control {  state: number = 0}
interface SelectableControl extends Control {  select(): void}

ArkTS

interface Control {  state: number}
interface SelectableControl extends Control {  select(): void}
  1. 只能使用类型相同的编译时表达式初始化枚举成员

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'}

参考文档:

developer.huawei.com/consumer/cn…

developer.huawei.com/consumer/cn…