TS: 类

作者: 写代码的海怪 | 来源:发表于2019-03-11 12:30 被阅读35次

    TS 的类其实和 ES6 里的类差不多,只不过 TS 加多了一些功能。这篇文章会介绍 TS 类的常用功能与接口的对比,以及抽象类。

    入门

    还是先从基础(ES6 自带)的语法讲起吧,假设现在我们要定义 Github 的 GithubRepository 类。

    class GithubRepository {
        name: string
        commits: number
    
        constructor(name: string) {
            this.name = name
            this.commits = 0
        }
    
        remove():void {
            console.log('Delete this repo')
        }
    
        rename(name: string):void {
            this.name = name
        }
    }
    
    let repo = new GithubRepository('My Repo')
    

    GithubRepository 有仓库名字 (name),commit 的次数 (commits),还有删除仓库 (delete) 和重命名 (rename) 两个方法。这就是一个完整类的定义。

    其中要注意的是一定要有 constructor 构造器,这是在 let repo = new GithubRepository('My Repo') 的时候用来创建临时对象的,在创建之后再将对象的内存地址赋值给 repo 的,所以无论要不要初始化类里的变量都要写 constructor。

    继承

    在 ES6 之前 JS 已经可以通过原型链实现类的继承功能了,ES6 其实加了个语法糖,而 TS 里的类继承和 ES6 是一样的。

    现在微软收购了 Github 了嘛,那就假设他们家的仓库继承了 Github 的 GithubRepository 类吧。

    class GithubRepository {
        public name: string
        public commits: number
    
        constructor(name: string) {
            this.name = name
            this.commits = 0
        }
    
        remove():void {
            console.log('Delete this repo')
        }
    
        rename(name: string):void {
            this.name = name
        }
    }
    
    class MicrosoftRepository extends Repository {
        public logo: string = 'Microsoft'
        constructor(name: string) {
            super(name)
        }
    }
    
    let repo = new MicrosoftRepository('My Repo')
    

    现在假设微软的生成的 MicrosoftRepository 都要加上微软的 logo,所以 logo 放在 MicrosoftRepository 里。而在 constructor 里要调用 super 方法,还要将 name 传过去,作用相当于调用了 GithubRepository 类的 constructor。

    现在变量 repo 就可以使用 GithubRepository 类里的方法,同时也具有 logo 属性了。

    console.log(repo.name) // "My Repo"
    
    repo.remove() // "Delete this repo"
    
    repo.rename('Your Repo')
    console.log(repo.name) // "Your Repo"
    
    console.log(repo.logo) // "Microsoft"
    

    作用域

    public

    现在我们定义 GithubRepository 里 namecommits 都是可以被外界访问的,所以这两个默认的作用域是 public,也就说可以写成这样

    class GithubRepository {
        public name: string
        public commits: number
        ...
    }
    
    let repo = new GithubRepository('My Repo')
    
    console.log(repo.name) // 可以访问,"My Repo"
    console.log(repo.commits) // 可以访问,0
    

    private

    假设现在有个变量 githubLogo 只能只属于 GithubRepoistory,而外界不能访问,当然微软的类也是不能访问的,这就要设置成 private 了。

    class GithubRepository {
        ...
        private githubLogo: string 
        ...
    }
    
    class MicrosoftRepository extends GithubRepository {
        public logo: string = 'Microsoft'
        constructor(name: string) {
            super(name)
            console.log(this.commits)    // 0
            console.log(this.githubLogo) // 不能访问,报错
        }
    }
    
    let repo = new GithubRepository('My Repo')
    
    console.log(repo.githubLogo) // 出错,不能访问 githubLogo
    

    protected

    protected 的作用域就是只能在“本家族”里才能访问,别人都不能访问,有点像“家传秘方”的意思。

    假设 Github 的 GithubRepository 有家传的推荐算法 recommend,微软不想自己实现推荐算法,所以只好继承 GithubRepository 祖传的推荐算法喽。

    class GithubRepository {
        ...
        protected recommend(): void {
            console.log('Recommend...')
        }
    }
    
    class MicrosoftRepository extends GithubRepository {
        public logo: string = 'Microsoft'
        constructor(name: string) {
            super(name)
            this.recommend() // "Recommend..."
        }
    }
    
    let repo = new MicrosoftRepository('My Repo')
    repo.recommend() // 不能访问 recommend,报错
    

    静态属性

    静态属性(方法)用关键字 static 来表示。静态属性可以不用创建对象就可以访问该属性/调用该方法。

    假设现在 GithubRepository 有一个方法是获取该仓库下截量的方法,用静态方法可以写成这样

    class GithubRepository {
        ...
        static getDownload(name: string): void {
            console.log(`Repo ${name} download/month is ....`)
        }
    }
    

    如果我们要查某个仓库的每月下载量就可以直接调用 getDownload 方法即可

    GithubRepository.getDownload('MyRepo')
    

    而不是创建一个 GithubRepository 去调用

    (new GithubRepository()).getDownload('MyRepo')
    

    静态属性类似,我们可以给 GithubRepository 加一个官网链接,这个链接变量设置为静态属性。

    class GithubRepository {
        ...
        public static url: string = 'https://github.com'
        ...
    }
    
    console.log(GithubRepository.url) // "https://github.com"
    

    setter 与 getter

    刚刚说过可以用 private 关键字使得某些属性不对外公开,这样我们就可以隐藏一些功能的实现了。比如说对不同浏览器的兼容等。

    回到我们的例子,这个 GithubRepository 要对 Firefox,Chrome,IE 进行兼容,不同浏览器要去计算对应 Logo 的横坐标 X,这就可以使用 setter 与 getter 来完成了。

    class GithubRepository {
        ...
        private _logoPositionX: number = 0
    
        set logoPositionX(rawX: number) {
            browser = getBrowserName()
            if (browser === 'IE') {
                this._logoPositionX = rawX + 1
            }
            else if (browser = 'Chrome') {
                this._logoPositionX = rawX + 2
            }
            else if (browser = 'Firefox') {
                this._logoPositionX = rawX + 3
            }
        }
        get logoPositionX(): number {
            return this._logoPositionX
        }
        ...
    }
    
    let repo = new MicrosoftRepository('My Repo')
    repo.logoPositionX = 2
    console.log(repo.logoPositionX) // "4"
    

    上面的代码就将 _logoPositionX 隐藏了,每次设置新的位置时都会根据当前的浏览器进行再将计算,这就完成了浏览器的兼容,而这个兼容的操作外面是不知道的。外界只需要设置位置,和获取位置就可以了。

    类与接口

    其实这两个东西都是对创建对象的一种约束,不同的是类像是一个工厂,里面有很多功能,如设置属性的作用域,初始化对象等。接口更像说明书,只是说明这个对象应该有什么属性/方法,就没了。

    使用代码可以看出他们有很大的不同。

    interface Human {
        name: string
        gender: string
    }
    
    let jack:Human = {
        name: 'Jack',
        age: 18
    }
    

    下面是类的声明

    class Human {
        name: string
        gender: string
        constructor(name, gender) {
            this.name = name
            this.gender = gender
        }
    }
    
    let jack = new Human('Jack', 18)
    

    从上面可以看到接口的写法完全可以用类来替代,但是写类麻烦。简单来说两者的区别就是:

    • 接口是类的低配版
    • 类是接口的调配版

    抽象类

    说完接口与类的区别,我们来看看抽象类。抽象类的用法是在类的基础上可以不实现一些方法,而让子类去实现。

    回到我们的例子,假设 Github 本来一直想实现一套仓库排名的算法,直到微软收购了还没有实现,所以这个重任就交给微软做了。

    abstract class GithubRepository {
        ...
        // 声明抽象方法
        abstract sort(): void
    }
    
    class MicrosoftRepository extends GithubRepository {
        ...
        // 实现抽象方法
        sort(): void {
            console.log('Sorting...')
        }
    }
    
    let repo = new MicrosoftRepository('My Repo')
    repo.sort() // "Sorting"
    

    这里 GithubRepository 变成了抽象类,前面加 abstract,里面就有一个还没实现的 sort 方法,所以前面也要加 abstract。到了 MicrosoftRepository,他就一定要去实现 sort 方法了。

    这里要注意的点是抽象类不能用来创建实例,想想看,如果可以创建实例,那未实现的方法调用怎么办呢?所以一定要有一个子类去实现那些未实现的方法,再用这个子类去创建实例。所以抽象类一般都作为“父亲类”,术语叫基类。他的功能是比一般的类要多的(可以声明未完成的方法)。

    就像以前总有科学家提出 XXX 猜想,但就是自己不去实现或者自己不能实现,反而让那些苦逼大学生去实现。

    抽象类与接口

    这个抽象类怎么看起来和接口差不多呀。是差不多,但是又不能完全一样。

    就像刚刚说的接口只是一份说明书,而抽象类就像工厂里的科学家,他提出很多猜想,同时也完成了很多实现,别的工厂(子类)就用继承他的思想去做自己的产品(创建实例)。

    interface Human {
        name: string
        age: string
    }
    
    let jack = {
        name: 'Jack',
        age: 18
    }
    

    下面再看看抽象类的实现。

    abstract class God {
        constructor() { }
        abstract createHuman(): void
    }
    
    abstract class Woman extends God {
        createHuman():void {
            console.log('XXOO') // :)
        }
    }
    

    当然还能这么写

    abstract class X {
        name: string
        gender: string
    }
    
    class Human extends X {
        constructor(name: string, gender: string) {
            super()
            this.name = name
            this.gender = gender
        }
    }
    
    let jack = new Human('Jack', 'Male')
    
    console.log(jack.name, jack.gender)
    

    相关文章

      网友评论

        本文标题:TS: 类

        本文链接:https://www.haomeiwen.com/subject/xtsvpqtx.html