了解什么是 TypeScript

皮卡皮卡皮
• 阅读 1563

内容纲要

  • 了解什么是 TypeScript
  • TypeScript 基本语法

了解什么是 TypeScript

TypeScript 介绍

TypeScript 是什么

TypeScript 是 JavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。

TypeScript 是 JavaScript 的超集,这意味着他支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。

和 JavaScript 若类型不同,TypeScript 这种强类型语言最大的优势在于静态类型检查,可以在代码开发阶段就预知一些低级错误的发生。

  • 一种类似于 JavaScript 的语言,在 JavaScript 的基础之上增加了类型,同时增强了 JavaScript 部分语法功能
  • 遵循 EcmaScript 6 标准规范
  • 由微软开发
  • Angular 8 框架采用 TypeScript 编写
  • 背后有微软和谷歌两大公司的支持
  • TypeScript 可以编译成 JavaScript 从而在支持 JavaScript 的环境中运行
  • TypeScript 和 JavaScript 的关系就好比 less 和 css 的关系

Why TypeScript

  • 从 Angular 2 之后,官方推荐使用 TypeScript 作为开发 Angular 应用的首选语言

  • 遵循 EcmaScript 6

  • 强大的 IDE 支持

    • 类型检查
    • 严谨的语法提示
  • 代码重构

  • 可读性良好

TypeScript 使用场景

  • 大型团队开发
  • Angular 官推语言
  • 其它…

这里引用知乎上一位开发者对使用推广 TypeScript 的看法:

typescript绝对是好东西,不过推广是有难度的:
1、TS是微软制造,最好的开发工具是VS,想想有些人就激动了(什么vi流,sublime流,macbook流,虽然也能写ts,但你无法跟他们说用vs写有多么好);
2、即使你告诉他们TS有多好,但是几十人的团队里总有一半以上的人不想学新的东西(当然我没有权利说不学新东西的人应该全部滚动,因为互联网打工的是大爷,想跳槽随便找工作);
3、JSer很多没有学习OOP开发经验(特别是从设计/页面重构转过来的);
4、很多人接触TS前根本没学过JS,经常有人问“使用TS如何写元素拖拽”这样的问题(那是DOM API好伐,不过你跟初学者很难解释明白);

TypeScript 不仅仅用于开发 Angular 应用

https://www.typescriptlang.org/samples/index.html

  • React
  • Angular
  • Node.js
  • Vue.js
  • WeChat

凡是可以写 JavaScript 的都可以使用 TypeScript。

前置知识

  • EcmaScript 6
  • TypeScript 概念及关系
  • 具有一定的 JavaScript 开发经验
  • 有 Java、C#、C++、C 等静态类型语言使用经验更佳

如何学习 TypeScript

  • 官方文档为准
  • 阅读别人的代码
  • 由于 TypeScript 是兼容 EcmaScript 6 的,所以在开发的时候不需要完全学会 TypeScript 再使用
  • 一个建议是有空就学,会了就用
  • 虽然兼容 EcmaScript 6,但建议既然使用了 TypeScript 就让你的 TypeScript 代码更加 TypeScript,这样才能发挥出 TypeScript 的威力。

相关链接

起步

搭建 TypeScript 开发环境

  • 什么是 compiler?
  • less 编译器:less
  • EcmaScript 6 编译器:babel
  • TypeScript 编译器:typescript
  • 一句话:把 TypeScript 转换为 JavaScript ,浏览器就具有运行了
  • 在线测试编译环境 compiler
  • 本地开发编译环境
    npm i -g typescript
    # 查看版本号
    tsc --version
    # 查看使用帮助
    tsc --help

编辑器的选择

  • Visual Studio Code
  • Sublime
  • Webstorm

Hello World

新建 greeter.ts 并写入以下内容:

    function greeter(person) {
       return "Hello, " + person;
    }
    let user = "Jane User";
    document.body.innerHTML = greeter(user);

安装编译器:

npm i -g typescript    

编译:

tsc greeter.ts

修改 greeter.ts 文件中的代码,为 greeter 函数的参数 person 加上类型声明 :string

    function greeter(person: string) {
        return "Hello, " + person;
    }
    let user = "Jane User";
    document.body.innerHTML = greeter(user);

重新编译执行。

让我们继续修改:

    function greeter(person: string) {
        return "Hello, " + person;
    }
    let user = [0, 1, 2];
    document.body.innerHTML = greeter(user);

重新编译,你将看到如下错误:

      error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

接口(Interface)


      interface Person {
          firstName: string;
          lastName: string;
      }
      function greeter(person: Person) {
          return "Hello, " + person.firstName + " " + person.lastName;
      }
      let user = { firstName: "Jane", lastName: "User" };
      document.body.innerHTML = greeter(user);

类(Class)


      class Student {

          constructor(public firstName: string, public lastName: string) {
               this.firstName = firstName;
               this.lastName = lastName;
          }
      }
     interface Person {
          firstName: string;
          lastName: string;
      }
     function greeter(person : Person) {
          return "Hello, " + person.firstName + " " + person.lastName;
      }
      let user = new Student("zeng", "mumu");
      document.body.innerHTML = greeter(user);

变量声明

var

  • 作用域
  • 重复声明

let

  • 块级作用域
  • 在同一个块中不能重复声明

const

  • 声明同时必须赋值
  • 一定声明不可改变
    • 对象可以修改
  • 块级作用域

let vs const

使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const也可以让我们更容易的推测数据的流动。

基本数据类型

布尔值


 let isDone: boolean = false;

数字

 let amount: number = 6;

字符串

  • 类型
  • 模板字符串
    • 支持换行
    • 支持内嵌表达式
  • 和 JavaScript 一样,可以使用双引号,也可以使用单引号,推荐单引号
  let nickname: string = '张三';

还可以使用模板字符串(换行 + 嵌入表达式):

let nickname: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my nickname is ${ nickname }.
I'll be ${ age + 1 } years old next month.`;

数组

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组:

 let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>

let list: Array<number> = [1, 2, 3];

元组

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为stringnumber类型的元组。

      // Declare a tuple type
      let x: [string, number];
      // Initialize it
      x = ['hello', 10]; // OK
      // Initialize it incorrectly
      x = [10, 'hello']; // Error

Object

  • 允许赋任意值
  • 但是不能调用任意方法,即便它真的有
  let foo: Object = {
    name: 'Jack',
    age: 18
  }

知道即可,用的很少,没有类型校验和语法提示

Any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:

    let notSure: any = 4;
    notSure = "maybe a string instead";
    notSure = false; // okay, definitely a boolean

Void

void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

      function warnUser(): void {
        alert("This is my warning message");
      }

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

     let unusable: void = undefined;

Null 和 Undefined

void相似,它们的本身的类型用处不是很大:

      // Not much else we can assign to these variables!
      let u: undefined = undefined;
      let n: null = null;

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks 标记,nullundefined 只能赋值给 void 和它们各自。 这能避免 很多 常见的问题。许在某处你想传入一个 stringnullundefined,你可以使用联合类型string | null | undefined

注意:我们推荐尽可能地使用--strictNullChecks ,因为它使你的代码更严谨,可以极大的减少出错的几率。

类型推断

有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过 类型断言 这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

      let someValue: any = "this is a string";
      let strLength: number = (<string>someValue).length;

另一个为as语法:

      let someValue: any = "this is a string";
      let strLength: number = (someValue as string).length;

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。

其它

  • ReadonlyArray<T> 去除了数组的所有可变方法,确保数组创建后再也不能被修改

接口

TypeScript的核心原则之一是对值所具有的 结构 进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

基本示例

      function printLabel(labelledObj: { label: string }) {
        console.log(labelledObj.label);
      }
      let myObj = { size: 10, label: "Size 10 Object" };
      printLabel(myObj);

类型检查器会查看printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。

下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string

      interface LabelledValue {
        label: string;
      }
      function printLabel(labelledObj: LabelledValue) {
        console.log(labelledObj.label);
      }
      let myObj = {size: 10, label: "Size 10 Object"};
      printLabel(myObj);

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

      interface SquareConfig {
        color?: string;
        width?: number;
      }
    function createSquare(config: SquareConfig): {color: string; area: number} {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
      newSquare.color = config.color;
    }
    if (config.width) {
      newSquare.area = config.width * config.width;
    }
    return newSquare;
  }
    let mySquare = createSquare({color: "black"});

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:

      interface Point {
          readonly x: number;
          readonly y: number;
      }

你可以通过赋值一个对象字面量来构造一个Point。 赋值后, xy再也不能被改变了。

  let p1: Point = { x: 10, y: 20 };
  p1.x = 5; // error!

readonly vs const

  • 常量使用 const
  • 对象属性使用 readonly

解构赋值

数组解构

      let input = [1, 2];
      let [first, second] = input;
      console.log(first); // outputs 1
      console.log(second); // outputs 2

上面的写法等价于:

      first = input[0];
      second = input[1];

利用解构赋值交换变量:


    [first, second] = [second, first];

函数参数解构:


      function f ([first, second]: [number, number]) [
        console.log(first)
        console.log(second)
      ]
      f(1, 2)

解构剩余参数:

      let [first, ...rest] = [1, 2, 3, 4]
      console.log(first) // 1
      console.log(rest) // [2, 3, 4]

也可以忽略其它参数:

      let [first] = [1, 2, 3, 4];
      console.log(first); // outputs 1

或者跳过解构:

let [, second, , fourth] = [1, 2, 3, 4]

对象解构

示例一:

      let o = {
          a: "foo",
          b: 12,
          c: "bar"
      };
      let { a, b } = o;

就像数组解构,你可以用没有声明的赋值:

      let a: number,
      b: number;
      ({a, b} = {a: 123, b: 456})
      console.log(a, b) // 123 456

你可以在对象里使用 ... 语法创建剩余变量:

      let { a, ...passthrough } = o;
      let total = passthrough.b + passthrough.c.length;

属性解构重命名

你也可以给属性以不同的名字:

      let { a: newName1, b: newName2 } = o;

注意,这里的冒号 不是 指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。

      let {a, b}: {a: string, b: number} = o;

默认值

      function keepWholeObject(wholeObject: { a: string, b?: number }) {
          let { a, b = 1001 } = wholeObject;
      }

展开操作符

  • 展开数组
  • 展开对象
    • 不会展开方法

解构赋值用于函数声明

  type C = {a: string, b?: number}
    function f ({a, b}: C): void {
    // ...
  }

解构赋值用于加载指定模块成员

基本示例

      class Person {
          name: string;
          age: number;
            constructor(name: string, age: number) {
              this.name = name;
              this.age = age;
          }
            sayHello() {
              console.log(this.name);
          }
      }
        let zs: Person = new Person('张三', 18);

构造函数

继承

      class Animal {
          move(distanceInMeters: number = 0) {
              console.log(`Animal moved ${distanceInMeters}m.`);
          }
      }
        class Dog extends Animal {
          bark() {
              console.log('Woof! Woof!');
          }
      }
        const dog = new Dog();
      dog.bark();
      dog.move(10);
      dog.bark();

这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里, Dog是一个 派生类 ,它派生自 Animal 基类 ,通过 extends关键字。 派生类通常被称作 子类 ,基类通常被称作 超类

因为 Dog继承了 Animal的功能,因此我们可以创建一个 Dog的实例,它能够 bark()move()

下面是一个更复杂的例子:

      class Animal {
          name: string;
          constructor(theName: string) { this.name = theName; }
          move(distanceInMeters: number = 0) {
              console.log(`${this.name} moved ${distanceInMeters}m.`);
          }
      }
        class Snake extends Animal {
          constructor(name: string) { super(name); }
          move(distanceInMeters = 5) {
              console.log("Slithering...");
              super.move(distanceInMeters);
          }
      }
        class Horse extends Animal {
          constructor(name: string) { super(name); }
          move(distanceInMeters = 45) {
              console.log("Galloping...");
              super.move(distanceInMeters);
          }
      }
        let sam = new Snake("Sammy the Python");
      let tom: Animal = new Horse("Tommy the Palomino");
        sam.move();
      tom.move(34);

与前一个例子的不同点是,派生类包含了一个构造函数,它 必须 调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定 要调用 super()。 这个是TypeScript强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。 Snake类和 Horse类都创建了 move方法,它们重写了从Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能。 注意,即使 tom被声明为Animal类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse里重写的方法:

      Slithering...
      Sammy the Python moved 5m.
      Galloping...
      Tommy the Palomino moved 34m.

实例成员访问修饰符

public 开放的

  • 默认为 public

      class Animal {
          public name: string;
          public constructor(theName: string) { this.name = theName; }
          public move(distanceInMeters: number) {
              console.log(`${this.name} moved ${distanceInMeters}m.`);
          }
      }

private 私有的

  • 不能被外部访问,只能在类的内部访问使用

  • 私有成员不会被继承

      class Person {
        public name: string;
        public age: number = 18;
        private type: string = 'human'
        public constructor (name, age) {
          this.name = name
          this.age = age
        }
      }

protected 受保护的

  • private 类似,但是可以被继承

      class Person {
          protected name: string;
          constructor(name: string) { this.name = name; }
      }
        class Employee extends Person {
          private department: string;
            constructor(name: string, department: string) {
              super(name)
              this.department = department;
          }
            public getElevatorPitch() {
              return `Hello, my name is ${this.name} and I work in ${this.department}.`;
          }
      }
        let howard = new Employee("Howard", "Sales");
      console.log(howard.getElevatorPitch());
      console.log(howard.name); // 错误

注意,我们不能在 Person类外使用 name,但是我们仍然可以通过 Employee类的实例方法访问,因为Employee是由 Person派生而来的。

readonly 只读的

在参数中使用修饰符

在上面的例子中,我们不得不定义一个受保护的成员 name和一个构造函数参数 theNamePerson类里,并且立刻给 nametheName赋值。 这种情况经常会遇到。 参数属性 可以方便地让我们在一个地方定义并初始化一个成员。

      class Person {
            name: string;
            age: number;
          constructor(name: string, age: number) {
              this.name = name;
              this.age = age;
          }
      }

可以简写为:

      class Person {
          constructor(public name: string, public age: number) {
          }
      }

属性的存(get)取(set)器

      let passcode = "secret passcode";
        class Employee {
            // 私有成员,外部无法访问
          private _fullName: string;
              // 当访问 实例.fullName 的时候会调用 get 方法
          get fullName(): string {
              return this._fullName;
          }
              // 当对 实例.fullName = xxx 赋值的时候会调用 set 方法
          set fullName(newName: string) {
              if (passcode && passcode == "secret passcode") {
                  this._fullName = newName;
              }
              else {
                  console.log("Error: Unauthorized update of employee!");
              }
          }
      }
        let employee = new Employee();
      employee.fullName = "Bob Smith";
      if (employee.fullName) {
          alert(employee.fullName);
      }

静态成员

  • 不需要实例化访问的成员称之为静态成员,即只能被类访问的成员
  • static 关键字
      class Grid {
          static origin = {x: 0, y: 0};
          calculateDistanceFromOrigin(point: {x: number; y: number;}) {
              let xDist = (point.x - Grid.origin.x);
              let yDist = (point.y - Grid.origin.y);
              return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
          }
          constructor (public scale: number) { }
      }
      let grid1 = new Grid(1.0);  // 1x scale
      let grid2 = new Grid(5.0);  // 5x scale
      console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
      console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

函数

函数参数

  • 参数及返回值类型

      function add(x: number, y: number): number {
          return x + y
      }
    
  • 可选参数

      function add(x: number, y?: number): number {
          return x + 10
      }
  • 默认参数

      function add(x: number, y: number = 20): number {
          return x + y
      }
  • 剩余参数

      function sum(...args: number[]): number {
          let ret: number = 0
          args.forEach((item: number): void => {
              ret += item
          })
          return ret
      }
        sum(1, 2, 3)

箭头函数

  • 基本示例

      let add = (x: number, y: number): number => x + y

for-of 循环

  • for 循环
  • forEach
    • 不支持 break
  • for in
    • 会把数组当作对象来遍历
  • for of
    • 支持 break

类型推断(Type Inference)

类型兼容性

模块

概念

模块通信:导出

      export default xxx
        export const foo: string = 'bar';
      export const bar: string = 'foo';

模块通信:导入

      // 加载默认成员
      import xxx from '模块标识'
        // 按需加载模块成员
      import {foo, bar} from '模块'

TypeScript 总结

  • TypeScript 是什么
  • 变量声明
    • var
    • let
    • const
  • 基本数据类型
    • 布尔值 boolean
    • 数字 number
    • 字符串 string
    • 数组 number[] 或者 Array<number>
    • 元祖 [number, string]
    • 对象 object ,了解即可
    • 任意类型 any
    • 函数空返回值 void
    • nullundefined
  • 接口
    • interface
  • 解构赋值
    • 数组解构
    • 对象解构
  • 展开操作符
    • 展开数组
    • 展开对象
    • 基本语法
    • 构造函数
    • 继承
    • 属性修饰符
    • 属性的 get 和 set
  • 函数
    • 参数
    • 箭头函数
  • for-of 循环
  • 模块
    • 导出
    • 导入
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
2年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
2个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
皮卡皮卡皮
皮卡皮卡皮
Lv1
十年旧梦无寻处,几度新春不在家。
文章
8
粉丝
1
获赞
6