从零到一学会TypeScript

吐槽君 分类:javascript

从零到一学会TypeScript

背景


TypeScript是JavaScript的超集,可以理解为JavaScript的升级版。因为JavaScript是动态类型语言,这让我们在编写代码时有了更高的编码效率,写起来也更加简便,但于此同时也造成了维护的困难性,在编写时也不利于我们及时发现错误。而TypeScript,顾名思义就是一个静态类型语言,它很好的解决了动态类型造成的语言缺陷。

TypeScript无法直接运行在浏览器中,它需要通过编译器编译成JavaScript,用编译后JavaScript运行在浏览器中。TypeScript是用node编写的,所以TypeScript需要运行在node环境中,npm i typescript -g 安装的是TypeScript转换到JavaScript编译器。将一个ts文件转换为js文件只需执行:tsc xxx.ts

注:TypeScript写起来确实会比JavaScript更加麻烦,但是用久了之后必定会体会到它的好处,并且越用越喜欢。

基本类型


  • 类型声明

    • 类型声明是ts非常重要的一个特点

    • 通过类型声明可以指定ts变量(参数、形参)的类型

    • 指定类型后,当变量赋值时,ts编译器会自动检查是否符合类型声明,符合则赋值,不符合则报错

    • 语法:

      • let 变量: 类型;
        
        let 变量: 类型 = 值;
        
        let 变量 = 值; // 等同于  let 变量: 初始值的类型 = 值;
        
        function fn(参数: 类型): 类型{
          ...
        }
         
  • 自动类型判断

    • ts拥有自动的类型判断机制
    • 当对变量的声明和赋值是同时进行的,ts编译器会自动判断变量的类型
    • 如果你的变量的声明和赋值是同时进行的,可以省略掉类型声明
  • 类型:

    类型 例子 描述
    number 1、2.2、-21 任意数字
    string 'yzy' 任意字符串
    boolean true、false true或者false
    字面量 其本身 限制变量的值就是该字面量的值
    any * 任意类型
    unknown * 类型安全的any
    void 没有值(undefined) 没有值
    never 没有值 不能是任何值
    object {name: 'yzy'} 任意js对象
    array [1, 2, 3] 任意js数组
    tuple [4, 5] 元素,ts新增类型,固定长度数组
    enum Enum{A, B} 枚举,ts新增类型
  • number

    • let num: number = 6
      let num2: number
      num2 = 222
       
  • string

    • let name: string = 'yzy'
      let last_name: string
      last_name = 'tmac'
       
  • boolean

    • let ishow: boolean = false
      let loading: boolean
      loading = true
       
  • 字面量

    • let yzy: 'yzy'
      yzy = 'yzy' // 变量yzy的值只能是'yzy'
      
      let sex: 'male' | 'female'
      sex = 'female' // 变量sex的值可以是'male'或者'female'
       
  • any

    • let field: any
      field = 10
      field = 'x'
      field = false
      
      let name: string
      
      name = field // 不会报错
       
  • unknown

    • let field: unknown
      field = 10
      field = 'x'
      field = false
      
      let name: string
      
      name = field // 会报错
      
      // 可是使用断言方式解决,即告诉程序我这个值的实际类型就是string,你竟管用【下面两种写法等价】
      // 注意:field的真实值即使不是string也不会报错,但是这样就失去了类型检查的意义,所以在实际代码中field还是要赋值成string类型的值
      name = field as string
      name = <string>field
      
      // 或者通过类型判断来解决
      if (typeof field === 'string') {
        name = field
      }
       
  • void

    • function fn (): viod {
        return;
      }
       
  • never

    • // 永远不会返回结果,连空都没有,这个函数执行不完,一定报错
      function fn (): never {
        throw new Error('报错了!')
      }
       
  • object

    • let obj: object
      
      obj = {}
      obj = function () {}
      // 这里我们会发现都没问题,所以object类型限制的不是变量是否为一个对象,而是去限制对象的具体属性类型
      // 属性名后加“?”表示可选属性
      let _obj: {name: string, age?: number}
      _obj = {name: 'yzy'}
      // 当我们只需要设置一个属性名的类型,其他属性名类型不做设置时,可以这么写:
      // 注意:这里的propName可以写任何值,不做要求,只是格式占位
      let _obj_: {name: string, [propName: string]: any}
      _obj_ = {name: 'jerry', age: 18, gender: 'male'}
                 
      // 同样的,设置函数时
      let fn: (a: number, b: number) => number
      fn = function (num1, num2): number {
        return num1 + num2
      }
      
       
  • Array

    • // string[] 表示的是字符串数组
      let arr: string[]
      arr = ['yzy', 'jerry', 'tom']
      
      // 语法:类型+[]
      // 写法2: Array<类型>
       
  • tuple

    • // 元组,元组就是固定长度的数组
      let arr: [string, string]
      arr = ['yzy', 'jerry']
      
       
  • enum

    • // 可枚举的好处是,这样在书写的时候可以一眼知道0和1分别代表了什么意思
      enum Gender {
        male = 0,
        famale = 1
      }
      
      let obj = {name: string, gender: Gender}
      obj = {
        name: 'yzy',
        gender: Gender.male
      }
       
  • 类型的别名

    • // 方便重复使用
      type genderType =  'male' | 'famale'
      let gender: genderType
      gender = 'male'
       
  • 类型断言

    • 有些情况下,变量的类型对于我们来说是很明确,但是TS编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型,断言有两种形式:

      • 第一种

        • let someValue: unknown = "this is a string";
          let strLength: number = (someValue as string).length;
           
      • 第二种

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

编译选项


  • 自动编译文件

    • 编译文件时,使用-w指令后,ts编译器会自动监视文件的变化,并且在文件变化时重新对文件进行编译

    • 示例:

      • tsc xxx.ts -w
         
  • 自动编译整个项目

    • 如果直接使用tsc指令,则可以自动将当前项目下的所有ts文件编译成js文件

    • 但是能直接使用tsc指令的前提是,要先在项目的根目录下创建一个ts的配置文件tsconfing.json

    • tsconfing.json是一个json文件,添加配置文件后,只需tsc指令即可完成对整个项目的编译

    • 配置选项:

      • include

        • 定义希望被编译文件所在的目录

        • 默认值:["**/*"]

        • 示例

          • "include": ["src/**/*", "test/**/*"]
             
          • 示例中,所有src目录和test目录下的文件都会被编译

      • exclude

        • 定义需要排除在外的目录

        • 默认值:["node_modules", "bower_components", "jspm_packages"]

        • 示例

          • "exclude": ["src/hello/**/*"]
             
          • 示例中,hello目录下的文件都不会被编译

      • extends

        • 定义被继承的配置文件

        • 示例

          • "extends": "./configs/base"
             
          • 示例中,当前配置文件中会自动包含configs目录下base.json中的所有配置信息

      • files

        • 指定被编译文件的列表,只有需要编译的文件少时才会用到(同include功能相同)

        • 示例

          • "files": [
              "core.ts",
              "sys.ts",
              "types.ts",
              "scanner.ts",
              "parser.ts",
              "utilities.ts",
              "binder.ts",
              "checker.ts",
              "tsc.ts",
            ]
             
          • 列表中的文件都会被ts编译器所编译

      • complierOptions

        • 编译选项是配置文件中非常重要也比较复杂的配置选项

        • 在complierOptions中包含多个子选项,用来完成对编译的配置

          • 项目选项

            • target

              • 设置ts代码编译的目标版本

              • 可选值:

                • ES3 (默认)、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext
              • 示例:

                • "complierOptions": {
                    "target": "ES6"
                  }
                   
                • 示例中,我们所编写的ts代码会被编译成ES6版本的js代码

            • lib

              • 指定代码运行时所包含的库,若项目为浏览器项目时可以不进行设置

              • 可选值:

                • es5、 es6、 es2015、es7、es2016、es2017、es2018、es2019、es2020、esnext、dom、webworker等。默认值就是能保证在浏览器运行的配置,若在node环境中可根据具体需要进行设置。
              • 示例:

                • "lib": ["es6", "dom"]
                   
                • 示例中,表示运行环境为es6 + dom

            • module

              • 表示以哪种模块化方式进行编译

              • 可选值:

                • none、commonjs、amd、system、umd、es6、es2015、es2020、esnext
              • 示例:

                • "module": "es6"
                   
                • 示例中,以es6模块化方式进行编译,即编译后的js文件是遵循es6的模块化方式

            • outDir

              • 用来指定编译后文件所在的目录

              • 示例:

                • "outDir": "./src/dist"
                   
                • 示例中,会将目标文件编译到src/dist目录下

            • outFile

              • 将代码合并成一个文件

              • 示例:

                • "outFile": "./src/dist/app.js"
                   
                • 示例中,会将include中的ts文件编译到src/dist目录下的app.js文件中

            • allowJs

              • 是否将js文件进行编译

              • 默认值:false

              • 示例:

                • "allowJs": true
                   
                • 示例中,会将include中的js文件也会被编译

            • checkJs

              • 是否检查js代码符合语法规范

              • 默认值:false

              • 示例:

                • "checkJs": true
                   
                • 示例中,被编译的代码也会被检测变量的类型

            • removeComments

              • 是否移除编译后文件中的注释

              • 默认值:false

              • 示例:

                • "removeComments": false
                   
                • 示例中,移除了编译后文件中的注释

            • rootDir

              • 指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录
            • sourceMap

              • 是否生成sourceMap
              • 默认值:false
            • noEmit

              • 不生成编译后的文件,注:执行了编译过程,但不生成编译文件
              • 默认值:false
            • noEmitOnError

              • 若编译文件有错误,则不生成有错误的编译文件
              • 默认值:false
            • 严格模式

              • alwaysStrict

                • 用来设置编译后的文件是否使用严格模式
                • 默认值:false
                • 知识点:当代码中使用了es6模块化语法时,则无需设置严格模式,因为一旦使用了es6模块化语法,js就自动执行了严格模式
              • noImplicitAny

                • 是否允许隐式的any类型

                • 默认值:false(表示允许隐式)

                • 示例:

                  • "noImplicitAny": true
                     
                  • // 如果我们不给形参a和b设置类型的话,他俩默认的类型是隐式的any
                    // 如果我们在配置中设置了"noImplicitAny": true时,则必须要手动给a和b添加类型
                    function fn (a, b) {
                      return a + b
                    }
                     
                  • 示例中,不允许隐式的any类型,所以我们必须手动给a和b添加类型,这样才能保证文件不报错

              • noImplicitThis

                • 是否允许不明确类型的this
                • 默认值:false(表示允许)
              • strictNullChecks

                • 是否严格的检查空值

                • 默认值:false(不检查)

                • 示例:

                  • "strictNullChecks": true
                     
                  • // 因为dom有可能为空,所以需要加上&&运算符保证dom存在再向下执行
                    let dom = document.getElementById('y')
                    dom && dom.addEventListener('click', function () {
                      ...
                    })
                     
                  • 示例中,检查了变量是否有为空的可能性。解决方案:做一次不为空的判断

              • strictBindCallApply

                • 严格检查bind、call和apply的参数列表
              • strictFunctionTypes

                • 严格检查函数的类型
              • strictPropertyInitialization

                • 严格检查属性是否初始化
              • strict

                • 严格模式的总开关,即alwaysStrict、noImplicitAny、noImplicitThis、strictNullChecks四个属性的总开关。若设置为true时,其他全部为true;设置为false时,其他全部为false;若总开关和上述7个属性同时设置时,7个属性以他们自己设置的值为准。
            • 额外检查

              • noFallthroughCasesInSwitch
                • 检查switch语句包含正确的break
              • noImplicitReturns
                • 检查函数没有隐式的返回值
              • noUnusedLocals
                • 检查未使用的局部变量
              • noUnusedParameters
                • 检查未使用的参数
    • 更多配置及解释

      • {
          "compilerOptions": {
        
            /* 基本选项 */
            "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
            "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
            "lib": [],                             // 指定要包含在编译中的库文件
            "allowJs": true,                       // 允许编译 javascript 文件
            "checkJs": true,                       // 报告 javascript 文件中的错误
            "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
            "declaration": true,                   // 生成相应的 '.d.ts' 文件
            "sourceMap": true,                     // 生成相应的 '.map' 文件
            "outFile": "./",                       // 将输出文件合并为一个文件
            "outDir": "./",                        // 指定输出目录
            "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
            "removeComments": true,                // 删除编译后的所有的注释
            "noEmit": true,                        // 不生成输出文件
            "importHelpers": true,                 // 从 tslib 导入辅助工具函数
            "isolatedModules": true,               // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).
        
            /* 严格的类型检查选项 */
            "strict": true,                        // 启用所有严格类型检查选项
            "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
            "strictNullChecks": true,              // 启用严格的 null 检查
            "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
            "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
        
            /* 额外的检查 */
            "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
            "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
            "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
            "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
        
            /* 模块解析选项 */
            "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
            "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
            "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
            "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
            "typeRoots": [],                       // 包含类型声明的文件列表
            "types": [],                           // 需要包含的类型声明文件名列表
            "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。
        
            /* Source Map Options */
            "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
            "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
            "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
            "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
        
            /* 其他选项 */
            "experimentalDecorators": true,        // 启用装饰器
            "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
          }
        }
         

webpack打包TypeScript


webpack整合

通常情况下,实际开发中我们都需要使用构建工具对代码进行打包

TS同样也可以结合构建工具一起使用,下边以webpack为例介绍一下如何结合构建工具使用TS

步骤如下:

初始化项目

进入项目根目录,执行命令 npm init -y,创建package.json文件

下载构建工具

命令如下:

npm i -D webpack webpack-cli webpack-dev-server typescript ts-loader clean-webpack-plugin html-webpack-plugin
 

共安装了7个包:

  • webpack:构建工具webpack
  • webpack-cli:webpack的命令行工具
  • webpack-dev-server:webpack的开发服务器
  • typescript:ts编译器
  • ts-loader:ts加载器,用于在webpack中编译ts文件
  • html-webpack-plugin:webpack中html插件,用来自动创建html文件
  • clean-webpack-plugin:webpack中的清除插件,每次构建都会先清除目录

配置webpack

根目录下创建webpack的配置文件webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
   optimization:{
     minimize: false // 关闭代码压缩,可选
   },

   entry: "./src/index.ts",

   devtool: "inline-source-map",

   devServer: {
     contentBase: './dist'
   },

   output: {
     path: path.resolve(__dirname, "dist"),
     filename: "bundle.js",
     environment: {
       arrowFunction: false // 关闭webpack的箭头函数,可选
     }
   },

   resolve: {
     extensions: [".ts", ".js"]
   },

   module: {
     rules: [
       {
         test: /\.ts$/,
         use: {
           loader: "ts-loader"     
         },
         exclude: /node_modules/
       }
     ]
   },

   plugins: [
     new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({
       title:'TS测试'
     }),
   ]
}
 

配置TS编译选项

根目录下创建tsconfig.json,配置可以根据自己需要

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "ES2015",
    "strict": true
  }
}
 

修改package.json配置

修改package.json添加如下配置

{
  ...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack serve"
  },
  ...
}
 

项目使用

在src下创建ts文件,并在并命令行执行npm run build对代码进行编译

或者执行npm start来启动开发服务器

Babel

除了webpack,开发中还经常需要结合babel来对代码进行转换;

以使其可以兼容到更多的浏览器,在上述步骤的基础上,通过以下步骤再将babel引入到项目中;

虽然TS在编译时也支持代码转换,但是只支持简单的代码转换;

对于例如:Promise等ES6特性,TS无法直接转换,这时还要用到babel来做转换;

安装依赖包:

npm i -D @babel/core @babel/preset-env babel-loader core-js
 

共安装了4个包,分别是:

  • @babel/core:babel的核心工具
  • @babel/preset-env:babel的预定义环境
  • @babel-loader:babel在webpack中的加载器
  • core-js:core-js用来使老版本的浏览器支持新版ES语法

修改webpack.config.js配置文件

...
module: {
  rules: [
    {
      test: /\.ts$/,
      use: [
        // loader有执行顺序,从右向左加载
        {
          loader: "babel-loader",
          options:{
            presets: [
              [
                "@babel/preset-env",
                {
                  // 目标支持版本
                  "targets":{
                    "chrome": "58",
                    "ie": "11"
                  },
                  // corejs的版本,3表示3.x
                  "corejs":"3",
                  // 按需加载
                  "useBuiltIns": "usage"
                }
              ]
            ]
          }
        },
        {
          loader: "ts-loader"
        }
      ],
      exclude: /node_modules/
    }
  ]
}
...
 

如此一来,使用ts编译后的文件将会再次被babel处理

使得代码可以在大部分浏览器中直接使用

同时可以在配置选项的targets中指定要兼容的浏览器版本

类的使用


ES6为我们提供了更符合面向对象编程的语法 --- class

class本质就是function的一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。

这一章结合了ts的使用方法,并简单的讲解了类的使用

定义类

class 类名 {
  属性名: 类型
    
  constructor(参数: 类型){
    this.属性名 = 参数
  }

  方法名(){
    ....
  }

}
 

示例:

class Person{
  name: string
	age: number

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

  sayHello () {
    console.log(`大家好,我是${this.name}`);
  }
}
 

使用类:

const someone = new Person('yzy', 20)
someone.sayHello()
 

隐藏知识点

方式1:

class Person{

  constructor(name, age){
    this.name = name
    this.age = age
    this.fn = () => { console.log('call fn') }
  }

  sayHello = () => {
    console.log(`大家好,我是${this.name}`);
  }
}
 

方式2:

class Person{

  constructor(name, age){
    this.name = name
    this.age = age
		this.fn = function () { console.log('call fn') }
  }

  sayHello () {
    console.log(`大家好,我是${this.name}`);
  }
}
 

我们需要思考一下,这两种方式有什么不同吗。方式1中的sayHello写成了箭头函数,而方法2中的sayHello则是普通函数。他们两最大的区别在于实例中包含sayHello这个方法的区别

打印:

// 方法1:
new Person('xxx') 

Person {name: "xxx", age: undefined, sayHello: ƒ, fn: ƒ}
  age: undefined
  fn: () => { console.log('call fn') }
  name: "xxx"
  sayHello: () => { console.log(`大家好,我是${this.name}`); }
  __proto__:
    constructor: class Person
    __proto__: Object

// 方法2
new Person('xxx') 

Person {name: "xxx", age: undefined, fn: ƒ}
  age: undefined
  fn: ƒ ()
  name: "xxx"
  __proto__:
    constructor: class Person
    sayHello: ƒ sayHello()
    __proto__: Object
 

很明显,方法2的sayHello在实例的原型对象上,而方法1的sayHello则是实例的属性。至于这是什么原因造成的呢,我也不知道。但这让我想到了在react中通过标签的click或者onchange使用方法时,要么写成箭头函数,要么需要在constructor中做一次bind绑定this(不推荐在render中写bind,因为这样每次render都会bind,对内存损耗太大,而写在constructor中只执行一次即可达到效果)。当然了,经过思考react需要绑定的原因是因为this.fn赋值给了click,所以函数内部的this指向就改变了,所以需要进行bind操作。

关于react中的绑定简单来说:

  1. 如果你不绑定this.handleClick方法,那么在事件发生并且精确调用这个方法时,方法内部的this会丢失指向。
  2. 这不是React的原因,这是JavaScript中本来就有的。如果你传递一个函数名给一个变量,然后通过在变量后加括号()来调用这个方法,此时方法内部的this的指向就会丢失

react的需要bind的代码解释,无论被调用的方法是都实例属性还是实例圆形对象上的方法都不会影响

  • 可以拿到this.name的情况
// 情况1
const obj = {
  name: 'yzy',
  getName: function () {
    console.log('name is', this.name)
  }
}
obj.getName()

// 情况2
const obj = {
  name: 'yzy'
}
obj.__proto__.getName = function () {
  console.log('name is', this.name)
}
obj.getName()
 
  • 无法拿到this.name的情况
// 情况1
const obj = {
  name: 'yzy',
  getName: function () {
    console.log('===', this.name)
  }
}
const _get = obj.getName
_get()

// 情况2
const obj = {
  name: 'yzy'
}
obj.__proto__.getName = function () {
  console.log('name is', this.name)
}
const _get = obj.getName
_get()
 

上面两段代码很好的解释了react中bind的原因,看懂你就掌握明白了

总结:这里提出react并不是想讲方法1和方法2跟react中事件处理需bind有什么关系,而是要说明他们之间的关系不在于方法是实例属性还是方法在实例的原型对象上

构造函数

可以使用constructor定义一个构造器方法;

注:在TS中只能有一个构造器方法!

例如:

class C {
  name: string
  age: number

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

同时也可以直接将属性定义在构造函数中:

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

 

上面两种定义方法是完全相同的!

子类继承父类时,必须调用父类的构造方法(如果子类中也定义了构造方法)!

例如:

class A {
  protected num: number
  constructor(num: number) {
    this.num = num
  }
}

class X extends A {
  protected name: string
  constructor(num: number, name: string) {
    super(num)
    this.name = name
  }	
}
 

如果在X类中不调用super将会报错!

封装

对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装

默认情况下,对象的属性是可以任意修改的,为了确保数据的安全性,在TS中可以对属性的权限进行设置

  • 静态属性(static):
    • 声明为static的属性或方法不再属于实例,而是属于类的属性;
  • 只读属性(readonly):
    • 如果在声明属性时添加一个readonly,则属性便成了只读属性无法修改
  • TS中属性具有三种修饰符:
    • public(默认值),可以在类、子类和对象中修改
    • protected ,可以在类、子类中修改
    • private ,可以在类中修改

示例:

public:

class Person{
  public name: string // 写或什么都不写都是public
  public age: number

  constructor(name: string, age: number){
    this.name = name // 可以在类中修改
    this.age = age
  }

  sayHello () {
    console.log(`大家好,我是${this.name}`);
  }
}

class Rapper extends Person{
  constructor(name: string, age: number){
    super(name, age)
    this.name = name //子类中可以修改
  }
}

const p = new Person('yzy', 18)
p.name = 'jerry'// 可以通过对象修改
 

protected:

class Person{
  protected name: string
  protected age: number

  constructor(name: string, age: number){
    this.name = name // 可以修改
    this.age = age
  }

  sayHello(){
    console.log(`大家好,我是${this.name}`);
  }
}

class Rapper extends Person{
  constructor(name: string, age: number){
    super(name, age)
    this.name = name //子类中可以修改
  }
}

const p = new Person('yzy', 18)
p.name = 'jerry'// 不能修改
 

private:

class Person{
  private name: string
  private age: number

  constructor(name: string, age: number){
    this.name = name // 可以修改
    this.age = age
  }

  sayHello(){
    console.log(`大家好,我是${this.name}`);
  }
}

class Rapper extends Person{
  constructor(name: string, age: number){
    super(name, age)
    this.name = name //子类中不能修改
  }
}

const p = new Person('yzy', 18);
p.name = 'jerry'// 不能修改
 

属性存取器

对于一些不希望被任意修改的属性,可以将其设置为private

直接将其设置为private将导致无法再通过对象修改其中的属性

我们可以在类中定义一组读取、设置属性的方法,这种对属性读取或设置的属性被称为属性的存取器

读取属性的方法叫做setter方法,设置属性的方法叫做getter方法

示例:

class Person{
  private _name: string

  constructor(name: string){
    this._name = name
  }

  get name(){
    return this._name
  }

  set name(name: string){
    this._name = name
  }

}

const p1 = new Person('yzy')
// 实际通过调用getter方法读取name属性
console.log(p1.name)
// 实际通过调用setter方法修改name属性 
p1.name = 'jerry'
 

静态属性

静态属性(方法),也称为类属性。使用静态属性无需创建实例,通过类即可直接使用

静态属性(方法)使用static开头

示例:

class Tools{
  static PI = 3.1415926

  static sum(num1: number, num2: number){
    return num1 + num2
  }
}

console.log(Tools.PI)
console.log(Tools.sum(123, 456))
 

this

在类中,使用this表示当前对象

继承

继承时面向对象中的又一个特性

通过继承可以将其他类中的属性和方法引入到当前类中

示例:

class Animal{
  name: string
  age: number

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

class Dog extends Animal{
	// 不写constructor则会自动写上constructor并调用super()
  bark(){
    console.log(`${this.name}在汪汪叫!`)
  }
}

const dog = new Dog('旺财', 4)
dog.bark()
 

通过继承可以在不修改类的情况下完成对类的扩展

重写

发生继承时,如果子类中的方法会替换掉父类中的同名方法,这就称为方法的重写

示例:

class Animal{
  name: string
  age: number

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

  run(){
    console.log(`父类中的run方法!`);
  }
}

class Dog extends Animal{

  bark(){
    console.log(`${this.name}在汪汪叫!`)
  }

  run(){
    console.log(`子类中的run方法,会重写父类中的run方法!`)
  }
}

const dog = new Dog('旺财', 4)
dog.run()
 

在子类中可以使用super来完成对父类的引用

抽象类(abstract class)【仅属于TypeScript】

抽象类是专门用来被其他类所继承的类,它只能被其他类所继承不能用来创建实例;简单来说抽象类出生就是给别人当爸爸的

abstract class Animal{
  abstract run(): void
  bark(){
      console.log('动物在叫~')
  }
}

class Dog extends Animals{
  run(){
      console.log('狗在跑~')
  }
}
 

使用abstract开头的方法叫做抽象方法,抽象方法没有方法体(不是一个完整的方法函数)并且只能定义在抽象类(abstract class)中,继承抽象类的子类必须对抽象方法进行重写,否则报错

接口(Interface)


接口的作用类似于抽象类,不同点在于:接口中的所有方法和属性都是没有实值的,换句话说接口中的所有方法都是抽象方法

接口主要负责定义一个类的结构

接口可以去限制一个对象的接口:对象只有匹配接口中定义的所有属性和方法时才行(不能多也不能少)

可以让一个类去实现接口,实现接口时类中要包括接口中的所有属性(可以多,但不能少)

注:可以同时有n个相同名称的接口,这个接口的实际限制内容是这些相同名称接口的集合

示例:

interface Person{
  name: string;
  sayHello(): void;
}
interface Person{
  age: string;
  gender: number;
}

// 相当于
interface Person{
  age: string;
  gender: number;
  name: string;
  sayHello(): void;
}
 

示例(检查对象类型):

interface Person{
  name: string;
  sayHello():void;
}

function fn(per: Person){
    per.sayHello();
}

fn({name:'yzy', sayHello() {console.log(`Hello, 我是 ${this.name}`)}})
 

示例(实现):

interface Person{
  name: string;
  sayHello():void;
}

class Student implements Person{
  constructor(public name: string) {
  }

  sayHello() {
    console.log('大家好,我是' + this.name);
  }
}
 

泛型(Generic)


定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定);

此时泛型便能够发挥作用;

举个例子:

function test(arg: any): any{
  return arg;
}
 

上例中,test函数有一个参数类型不确定,但是能确定的时其返回值的类型和参数的类型是相同的;

由于类型不确定所以参数和返回值均使用了any,但是很明显这样做是不合适的:

首先使用any会关闭TS的类型检查,其次这样设置也不能体现出参数和返回值是相同的类型;

泛型函数

创建泛型函数

function test<T>(arg: T): T{
    return arg;
}
 

这里的<T>就是泛型;

T是我们给这个类型起的名字(不一定非叫T),设置泛型后即可在函数中使用T来表示该类型;

所以泛型其实很好理解,就表示某个类型;

那么如何使用上边的函数呢?

使用泛型函数

方式一(直接使用):
test(10)
 

使用时可以直接传递参数使用,类型会由TS自动推断出来,但有时编译器无法自动推断时还需要使用下面的方式

方式二(指定类型):
test<number>(10)
 

也可以在函数后手动指定泛型;

函数中声明多个泛型

可以同时指定多个泛型,泛型间使用逗号隔开:

function test<T, K>(a: T, b: K): K{
  return b;
}

test<number, string>(10, "hello");
 

使用泛型时,完全可以将泛型当成是一个普通的类去使用;

泛型类

类中同样可以使用泛型:

class MyClass<T>{
  prop: T;

  constructor(prop: T){
      this.prop = prop;
  }
}
 

泛型继承

除此之外,也可以对泛型的范围进行约束

interface MyInter{
  name: string;
  length: number;
}

type MyType = {
  name: string;
  length: number;
}

abstract class Myclass{
  abstract length: number
}

// 这里写<T extends MyType>、<T extends Myclass>和下面代码是一样的效果
function test<T extends MyInter>(arg: T): number{
  return arg.length;
}
 

使用T extends MyInter表示泛型T必须是MyInter的子类,不一定非要使用接口类,类型集合和抽象类同样做泛型继承;

结束

回复

我来回复
  • 暂无回复内容