TypeScript 基礎(chǔ)語法入門

Vue3 基本全部使用 TypeScript 來進(jìn)行重寫,盡管你可能覺得要學(xué)的東西越來越多了,但是作為程序員,如果沒有保持對新鮮事物的好奇和接納,很可能兩年后你就必須要加入外賣大軍了!話不多說,一起走進(jìn) TypeScript 的世界吧!

學(xué)習(xí)資料


TypeScript 中文文檔地址 | TypeScript 線上學(xué)習(xí)視頻

什么是 TypeScript


進(jìn)入官網(wǎng),映入眼簾的第一句就給出了解釋:TypeScriptJavaScript 類型的超集,它可以編譯成純 JavaScript。所謂超集意思就是:js 有的東西,我 ts 有,js 沒有的,我 ts 還有。這樣說可能有點(diǎn)糊,我們先來看一個(gè)栗子。

熟悉 js 的小伙伴都知道當(dāng)我們聲明了一個(gè)變量后,比如賦值了一個(gè)字符串,但是后續(xù)仍然可以將其再賦值成其它類型的數(shù)據(jù)。例如:

let a = '123' // a 是 string 類型
a = 123 // 將 a 改變成 number 類型
console.log(typeof a) // number

我們嘗試用 ts 寫上面這段代碼

let b = 123
b = '123' // 編輯器提示錯(cuò)誤:不能將類型 string 分配給類型 number 

其實(shí)如果用 ts 的規(guī)范來寫上面這段代碼應(yīng)該下面這樣的

let b: number = 123 // 規(guī)定了 b 是 number 類型,如果賦值給 string 就會直接提示報(bào)錯(cuò)
b = 456

但是如果我們把這段代碼直接拿到瀏覽器中,就會報(bào)錯(cuò),因?yàn)闉g覽器不認(rèn)識 TypeScript,我們寫的所有 ts 代碼都需要編譯成純 js 代碼然后才能被瀏覽器識別,我們可以在 TypeScript 官網(wǎng)練習(xí) 將上面代碼復(fù)制到左邊,右邊就會自動生成與其對應(yīng)的 js 代碼。

繼續(xù)學(xué)習(xí)前,我們來看看 TypeScript 相對于 JavaScript 帶來了什么優(yōu)勢,為什么未來我們一定要學(xué) TypeScript 呢?我們先用 JavaScript 來寫一段代碼:

function demo (data) {
  return data.x * 2 + data.y * 2
}
demo()

上面這段 js 代碼咋一看好像沒錯(cuò),但是如果我們放到瀏覽器中運(yùn)行就會發(fā)現(xiàn)報(bào)錯(cuò)信息 Cannot read property 'x' of undefined(無法讀取未定義的屬性 x) 。我們在調(diào)用 demo() 的時(shí)候沒有傳參,導(dǎo)致代碼真正運(yùn)行過程中報(bào)錯(cuò),但是它不會再編輯器中給我們報(bào)錯(cuò)提示, 只有當(dāng)代碼運(yùn)行了才會報(bào)錯(cuò)。

我們使用 ts 改造一下上述代碼:

function tsDemo(data: { x: number, y: number }) {
  return data.x * 2 + data.y * 2
}
tsDemo() // 編輯器直接報(bào)錯(cuò)提醒:應(yīng)有一個(gè)參數(shù),但獲得 0 個(gè)

假如我們隨便傳一個(gè)不符合規(guī)定的參數(shù),編輯器也會直接給我們錯(cuò)誤提醒:

tsDemo(10) // 類型 number 的參數(shù)不能賦值給類型 {x: number, y: number} 的參數(shù)

只有我們正確傳入了對應(yīng)的類型參數(shù),編輯器才會讓我們通過,并且在函數(shù)中調(diào)用 data 編輯器就會直接將其下面的 x、y 給我們提示出來:

tsDemo({ x: 10, y: 10 }) // ok
TypeScript 相對于 JavaScript 優(yōu)勢總結(jié):

1、ts 編寫代碼就會提示你的代碼是否編寫正確,而不需要像 js 等到代碼運(yùn)行時(shí)才發(fā)現(xiàn)錯(cuò)誤。
2、ts 有更好的編輯器語法提示,寫起代碼來更舒服。
3、類型的聲明可以讓我們直觀看到我們代碼里潛在的語義,代碼可讀性更好。

TypeScript 基礎(chǔ)環(huán)境搭建

一般使用 npm 來安裝,當(dāng)然前提肯定需要你裝了 node ,然后使用如下命令:

npm install -g typescript

這樣我們就全局安裝了 typescript ,此時(shí)我們怎么應(yīng)該運(yùn)行 ts 文件的代碼呢?我們知道在 node 環(huán)境下運(yùn)行 js 文件只需要使用 node demo.js 就行了,但是 ts 文件不能直接在瀏覽器和 node 環(huán)境下運(yùn)行,所以我們需要先將 ts 轉(zhuǎn)成 js 文件:

tsc demo.ts

然后我們就會發(fā)現(xiàn)文件夾下多了一個(gè) demo.js 文件,這個(gè)文件就是由 demo.ts 編譯過來的。 接下來我們使用 node demo.js 就可以運(yùn)行了。但是如果我們每次都需要先轉(zhuǎn)一道 js 然后在運(yùn)行會顯的十分繁瑣,所以我們可以安裝 ts-node ,命令行輸入如下指令:

npm install -g ts-node

此時(shí)我們就可以直接使用如下指令直接輸出 ts 文件的結(jié)果:

ts-node demo.ts
靜態(tài)類型的深度理解

TypeScript 中如果我們做了如下定義

const num: number = 123

我們不僅僅是將常量 num 永久定義成了 number 類型,而且我們在使用常量 num 的時(shí)候還可以直接使用 number 類型下面的所有方法,編輯器都會給我們最友好的提示。又比如我們定義了一個(gè)對象類型:

interface Point {
  x: number;
  y: number;
}
const point: Point = {
  x: 3,
  y: 4
}
point // 編輯器直接就會有 x 和 y 的提示
point.z // 類型 Point 上不存在屬性 z

如果我們看到一個(gè)變量是靜態(tài)類型,不僅僅意味著這個(gè)變量類型不能修改,還意味著這個(gè)變量的屬性和方法基本也就確定了,編輯器在使用靜態(tài)類型時(shí)就會給我們很好的語法提示。

TypeScript 的使用


為了讓程序有價(jià)值,我們需要能夠處理最簡單的數(shù)據(jù)單元:數(shù)字,字符串,結(jié)構(gòu)體,布爾值等。 TypeScript 支持與 JavaScript 幾乎相同的數(shù)據(jù)類型,此外還提供了實(shí)用的枚舉類型方便我們使用。

基礎(chǔ)類型

日常開發(fā)中常見的基礎(chǔ)類型一般有:

  • boolean
  • number
  • string
  • void
  • undefined
  • symbol
  • null
// boolean
let isDone: boolean = false

// number 注意 es6 還支持2進(jìn)制和8進(jìn)制
let age: number = 10
let binaryNumber: number = 0b1111

// string,注意es6新增的模版字符串也是沒有問題的
let firstName: string = 'viking'
let message: string = `hello, ${firstName}`

// 奇葩兄弟二人組,undefined 和 null
let u: undefined = undefined
let n: null = null
// 注意 undefined 和 null 是所有類型的子類型。
// 也就是說 undefined 類型的變量,可以賦值給 number 類型的變量:
let num: number = undefined

在實(shí)際使用中,其實(shí)單獨(dú)的 tsnull、undefined 不是很常用,因?yàn)槁暶饕粋€(gè) null 或者 undefined 的變量沒有意義,這種數(shù)據(jù)類型的聲明也只是在 js 中使用,比如程序初始時(shí),先聲明一個(gè) null 的變量,后續(xù)的代碼邏輯中再改成其它數(shù)據(jù)類型。

如果我們在編程階段還不清楚類型的變量到底會指定為哪一個(gè)類型,那應(yīng)該怎么辦呢?在這種情況下,我們不希望類型檢查器對這些值進(jìn)行檢查而是直接讓它們通過編譯階段的檢查。 那么我們可以使用 any 類型來標(biāo)記這些變量:

let notSure: any = 4
notSure = 'maybe a string'
notSure = true
notSure.myName
notSure.getName()

當(dāng)然,如果我們知道一個(gè)變量可能只是 number 和 string 類型的其中一種,我們也可以不使用 any 而是用下面這種語法:

// num 可以是 number 類型也可以是 string 類型
let num: number | string = 123
num = '456'
類型注解類型推斷

類型注解指的是我們在定義變量的時(shí)候就會去告訴 ts 這個(gè)變量是什么類型:

let count: number = 123 // 指定 count 是 number 類型

類型推斷是指當(dāng)我們未使用類型注解時(shí),ts 會自動去嘗試分析變量的類型:

const firstName = 1
const lastName = 2
// 自動推斷出 total 為 number 類型,因?yàn)樗?2 個(gè)數(shù)字的和
const total = firstNumber + lastNumber

所以如果 ts 能夠自動分析變量類型,我們就什么也不需要做,但是如果 ts 無法分析變量類型的話,我們就要使用類型注解了。栗子如下:

function getTotal(firstNumber, lastNumber) {
  return firstNumber + lastNumber
}
const result = getTotal(1, 2)

此時(shí) ts 就無法推斷出 result 是什么類型,只為給它分配一個(gè) any 類型,所以像這種時(shí)候,我們就應(yīng)該使用類型注解:

function getTotal(firstNumber: number, lastNumber: number) {
  return firstNumber + lastNumber
}
const result = getTotal(1, 2)

此時(shí) result 就會被推斷為 number 類型,符合我們預(yù)期的結(jié)果。

所以在未來使用 ts 的過程中我們就是希望變量和屬性能夠類型固定,能夠推斷的就讓它推斷,不能推斷的我們告訴它就行了。

函數(shù)類型

JavaScript 一樣,TypeScript 函數(shù)可以創(chuàng)建有名字的函數(shù)和匿名函數(shù)。 你可以隨意選擇適合應(yīng)用程序的方式,不論是定義一系列 API 函數(shù)還是只使用一次的函數(shù)。

通過下面的例子可以迅速回想起這兩種 JavaScript 中的函數(shù):

// 命名函數(shù)
function add(x, y) {
    return x + y;
}

// 匿名函數(shù)
let myAdd = function(x, y) { return x + y; };

我們首先來嘗試為一個(gè)命名函數(shù)添加類型:

// 約定函數(shù)傳入兩個(gè)值都是 number 類型,且函數(shù)的返回值也為 number 類型
function add(x: number, y: number): number {
  return x + y
}
add(1, 2) // OK
add(1, 2, 3) // Error 應(yīng)有 2 個(gè)參數(shù),但獲得 3 個(gè)
add(1, '2') // Error 類型 'string' 的參數(shù)不能賦值給類型 'number' 的參數(shù)

我們約定了 add() 的形參只能傳入兩個(gè)數(shù)字類型,并且它的返回值也是 nunmber 類型。你也許會奇怪,根據(jù)類型推斷來說,我們不需要給函數(shù)的返回值加上類型注解,可以通過類型推斷默認(rèn)推斷出兩個(gè)數(shù)字的和肯定也是 number 類型。但是如果我們不小心在上述函數(shù)中寫入了如下代碼:

function add(x: number, y: number) {
  return x + y + ''
}
const result = add(1, 2) // result 變成了 string 類型,編輯器未報(bào)錯(cuò)提醒

此時(shí)我們不小心在函數(shù)結(jié)尾加了一個(gè)空字符串,結(jié)果導(dǎo)致不能推斷出正確的類型,所以為了保證代碼的嚴(yán)謹(jǐn)性,對于函數(shù)類型來說,我們一般在結(jié)尾處給其進(jìn)行類型注解。

在函數(shù)的形參中,如果我們希望傳遞的第三個(gè)參數(shù)為可選參數(shù),如果有就用,沒有就不用,那么我們可以這樣來寫:

// ?: number 代表這是一個(gè)可選參數(shù),有就用,沒有就不用
function add(x: number, y: number, z?: number): number {
    if (typeof z === 'number') {
        return x + y + z
    } else {
        return x + y
    }
}
add(1, 2) // ok
add(1, 2, 3) // ok
add(1, 2, 3, 4) // error 應(yīng)該有 2-3 個(gè)參數(shù),但獲得 4 個(gè)

如果我們希望這個(gè)命名函數(shù)沒有返回值,那么我們就可以為這個(gè)函數(shù)給定 void 類型:

function add(x: number, y: number): void {
  return x + y // error 不能將類型 number 分配給類型 void
}
const result = add(1, 2)

日常開發(fā)中,我們經(jīng)常會使用對象解構(gòu)的方式進(jìn)行傳參,如果我們使用解構(gòu)去傳參,那么我們應(yīng)該怎么樣來定義對應(yīng)的類型呢?你可能會這樣寫:

function add({ x: number, y: number }) { // error
  return x + y
}
const result = add({ x: 1, y: 2 })

編輯器直接給我們報(bào)錯(cuò)了,正確的寫法應(yīng)該如下:

function add({ x, y }: { x: number, y: number }): number {
  return x + y
}
const result = add({ x: 1, y: 2 })

命名函數(shù)其實(shí)在 ES6 之后我們基本很少寫了,現(xiàn)在的開發(fā)中,你可能進(jìn)場這樣寫一個(gè)函數(shù):

const add = (x: number, y: number): number => {
    return x + y
}

此時(shí)聰明的你會發(fā)現(xiàn),add 好像就直接是函數(shù)類型,我們試著在編輯器中把鼠標(biāo)一如到 add 上面會發(fā)現(xiàn)它的類型為:const add:(x: number, y: number) => number,所以函數(shù)不僅輸入輸出有類型,它自己本身也是有類型的。

此時(shí)如果我們把 add 賦值給一個(gè) string 類型就會報(bào)錯(cuò)

let add2: string = add // error 不能將函數(shù)類型分配給 string 類型

此時(shí)我們應(yīng)該將 add2 定義為函數(shù)類型才能接收 add,那么問題來了,怎么給常量定義一個(gè)函數(shù)類型呢,我們先跟著編輯器的提示寫:

let add2: (x: number, y: number) => number = add // ok 

感覺寫著寫著你會不會蒙圈,感覺定義類型就像在寫一個(gè)箭頭函數(shù),這個(gè)時(shí)候我們要始終記得兩點(diǎn):

1、在 ts 中凡是在 : 后面都是在聲明類型,和實(shí)際的代碼邏輯沒有任何關(guān)系,而 = 后面跟的是函數(shù)的具體實(shí)現(xiàn),也就是函數(shù)體。
2、定義函數(shù)類型時(shí),后面的 => 不是箭頭函數(shù),而是 ts 中聲明函數(shù)類型返回值的方法,在上述栗子中它只是代表這個(gè)函數(shù)類型的返回值是一個(gè) number 類型。

數(shù)組

TypeScriptJavaScript 一樣可以操作數(shù)組元素。 有兩種方式可以定義數(shù)組。 第一種,可以在元素類型后面接上 [],表示由此類型元素組成的一個(gè)數(shù)組:

//最簡單的方法是使用「類型 + 方括號」來表示數(shù)組
let numberArr: number[] = [1, 2, 4]
let stringArr: string[] = ['1', '2', '3']

如果數(shù)組中是一個(gè)對象類型,那么我們應(yīng)該如下定義:

let objArr: { name: string, age: number }[] = [{
  name: 'cc',
  age: 18
}]

當(dāng)然,因?yàn)槲覀兗s束了類型中只能有 nameage 兩個(gè)字段,所以此時(shí) objArr 就被限定了只能有 nameage 兩個(gè)屬性,如果我們隨意添加一個(gè)新的屬性,編輯器就會給出報(bào)錯(cuò)提醒。如果我們想在類型約束中給出更多的字段,我們肯定不能用上面那種寫法,此時(shí)我們可以使用類型別名 type,具體怎么用呢?

// 使用 type 定義一個(gè) User 類型
type User = {
  name: string;
  age: number;
  sex: string
}
// 使用 User 類型
let objArr: User[] = [{
  name: 'cc',
  age: 18,
  sex: '男'
}]

當(dāng)然前面說了定義數(shù)組有兩種方式,其實(shí)第二種方式就是是使用數(shù)組泛型,Array<元素類型>。當(dāng)然泛型算是 ts 中比較難懂的一個(gè)概念,我們這里先羅列出來用法,后面說到泛型在來具體講解:

// 使用數(shù)組泛型定義變量的類型
let list: Array<number> = [1, 2, 3]
元組

元組和數(shù)組很像,但是元組更具象,它表示一個(gè)已知元素?cái)?shù)量和類型的數(shù)組,各元素的類型不必相同。我們先寫個(gè)栗子:

// 這表示一個(gè) [] 里面只能是 string 類型或者 number 類型
const arr: (string | number)[] = ['aa', 'bb', 18]

但是上述栗子中如果我們希望這個(gè)數(shù)組只有 3 個(gè)值,并且這 3 個(gè)值的類型就是 string string number,并且這三個(gè)值的類型順序也不能顛倒哦。那么我們就可以使用元組:

const arr: [string, string, number] = ['aa', 'bb', 18] // ok
const arr: [string, string, number] = ['aa', 18, 'bb'] // error

元素的應(yīng)用場景,一般向 csv 格式中的數(shù)據(jù)都是知道表頭是姓名、性別、年齡等信息就可以使用元組:

const teacherList: [string, string, number][] = [
  ['cc', 'male', 18],
  ['wc', 'female', 28]
]
interface 接口

這應(yīng)該是最重要的一個(gè)基礎(chǔ)語法了,我們項(xiàng)目中用的最多的就是 interface,我們先通過一個(gè)小栗子來看看它到底怎么用:

const getPersonName = (person: { name: string }): void => {
  console.log(person.name)
}
const setPersonName = (person: { name: string }, name: string): void => {
  person.name = name
}
const person: { name: string } = {
  name: 'cc'
}
getPersonName(person)
setPersonName(person, 'wc')

上述代碼中我們定義了兩個(gè)函數(shù)和一個(gè)常量,我們發(fā)現(xiàn)有一個(gè)對象類型 {name: string} 我們寫了 3 遍,那么我們能不能把這個(gè)對象類型抽離出來公用呢,聰明的你肯定想到了使用 類型別名 type,那么我們使用 type 改造一下上面的代碼:

// 使用類型別名 type
type Person = {
  name: string
}
const getPersonName = (person: Person): void => {
  console.log(person.name)
}
const setPersonName = (person: Person, name: string): void => {
  person.name = name

const person: Person = {
  name: 'cc'
}

除了 type 我們是否還有其它方法改造這段代碼呢?接下來就要介紹 interface 了,那我們就使用 interface 繼續(xù)改造:

interface Person {
  name: string
}
const getPersonName = (person: Person): void => {
  console.log(person.name)
}
const setPersonName = (person: Person, name: string): void => {
  person.name = name
}
const person: Person = {
  name: 'cc'
}

聰明的你應(yīng)該發(fā)現(xiàn)貌似 interfacetype 好像用法都差不多,但是在 ts 中通用性的規(guī)范是如果能用接口去表述一些類型的話就要優(yōu)先去使用接口,實(shí)在不行我們才使用類型別名 type

接下來我們在 interface 中使用 ? 可選參數(shù)和 readonly 只讀參數(shù):

interface Person {
  readonly name: string, // name 只能讀不能修改
  age?: number // age 可有可無
}

const setPersonName = (person: Person, name: string): void => {
  person.name = name // error 無法分配到 name,因?yàn)樗侵蛔x屬性
}
const person: Person = {
  name: 'cc'
}

但是我們可能會遇到這種情況,就是我們定義的常量 person 雖然規(guī)定了 Person 類型,但是如果這個(gè)常量還有性別、身高等屬性,而我們的接口類型定義中又不知道未來還會有哪些屬性,怎么樣防止 ts 效驗(yàn)錯(cuò)誤呢?還是看栗子:

interface Person {
  name: string,
  age?: number
}
const person: Person = { // error Person 類型中未指定 sex 和 height
  name: 'cc',
  sex: 'male',
  height: 180
}

此時(shí)我們可以在 Person 類型中使用如下定義:

interface Person {
  name: string,
  age?: number,
  [propName: string]: any
}

此時(shí)錯(cuò)誤提示就被關(guān)閉了,它代表 Person 這個(gè)類型除了 nameage 以外還可以有其它的屬性,屬性的名字是一個(gè)字符串類型就行,屬性的值可以是任意類型。當(dāng)然一個(gè)接口里面除了有屬性還可以有方法:

interface Person {
  name: string,
  age?: number,
  [propName: string]: any,
  say(): string // 必須有 say 方法,返回一個(gè) string 類型
}
const person: Person = {
  name: 'cc',
  sex: 'male',
  height: 180,
  say() {
    return '111'
  }
}
implements

在面向?qū)ο笾?,一個(gè)類只能繼承另外一個(gè)類,有時(shí)候不同類之間有一些共同的特性,而一個(gè)類想使用一個(gè)接口去做類的一些屬性約束時(shí)就要使用 implements 關(guān)鍵字,這時(shí)候我們就可以把這些特性提取成接口然后用 implements 關(guān)鍵詞來實(shí)現(xiàn),這樣就可以提高靈活性。舉個(gè)栗子:

// car 和 cellphone 兩個(gè)類有相同的特性,但是沒有公共父類,這時(shí)候可以把相同特性抽離成接口
interface Radio {
  switchRadio(trigger: boolean): void
}
class Car implements Radio {
  switchRadio(trigger: boolean) {}
}
class Cellphone implements Radio {
  switchRadio(trigger: boolean) {}  
}

interface Battery {
  checkBatteryStatus(): void
}
// 要實(shí)現(xiàn)多個(gè)接口,我們只需要中間用逗號隔開即可
class Cellphone implements Radio, Battery {
  switchRadio() {
  }
  checkBatteryStatus() {}
}
接口繼承

接口之間也可以實(shí)現(xiàn)相互繼承,這讓我們能夠從一個(gè)接口里復(fù)制成員到另一個(gè)接口里,可以更靈活地將接口分割到可重用的模塊里。

interface Shape {
  color: string;
}
interface PenStroke {
  penWidth: number;
}
// Square 繼承了 Shape 和 PenStroke
interface Square extends Shape, PenStroke {
  sideLength: number;
}
// 既然 demo 使用了 Square 那么就必須有 color、penWidth、sideLength 屬性
const demo: Square = {
  color: '#ccc',
  penWidth: 20,
  sideLength: 10
}

當(dāng)然接口不僅可以定義成一個(gè)對象,還可以定義成一個(gè)函數(shù):

interface Test {
  // 定義一個(gè)函數(shù)類型接收兩個(gè) number 類型的形參同時(shí)返回 number 類型
  (firstNumber: number, lastNumber: number): number;
}
const total: Test = (firstNumber: number, lastNumber: number): number => {
  return firstNumber + lastNumber
}
const result1 = total(1, 2)
class 類 -- TypeScript

首先我們來定義一個(gè)類:

class Person {
  name: string;
  constructor(message: string) {
    this.name = message
  }
  getName() {
    return this.name;
  }
}
const person = new Person('cc')
console.log(person.getName()) // cc

我們再次定義一個(gè) Teacher 類來繼承父類 Person

// 使用 extends 構(gòu)建繼承關(guān)系
class Teacher extends Person {
  getTeacherName() {
    return 'Teacher'
  }
}
const teacher = new Teacher('dd')
console.log(teacher.getName()) // dd
console.log(teacher.getTeacherName()) // Teacher

使用繼承之后,子類的實(shí)例可以直接訪問父類上的方法。如果子類上面也有 getName 方法,那么就會優(yōu)先執(zhí)行子類上的 getName(),如下栗子:

class Teacher extends Person {
  getTeacherName() {
    return 'Teacher'
  }
  getName() {
    return 'cc'
  }
}
const teacher = new Teacher('dd')
console.log(teacher.getName()) // cc

假如我們希望在子類重寫的方法中再次調(diào)用父類的這個(gè)方法,就可以使用 super 關(guān)鍵字,如下栗子:

class Teacher extends Person {
  getTeacherName() {
    return 'Teacher'
  }
  getName() {
    return super.getName() + 'ee'
  }
}

const teacher = new Teacher('dd')
console.log(teacher.getName()) // ddee

當(dāng)子類的方法將父類的方法覆蓋之后,如果我們?nèi)孕枵{(diào)用父類的這個(gè)方法,就可以通過 super 進(jìn)行調(diào)用。

公共,私有與受保護(hù)的修飾符

了解了類的基本用法之后,我們來認(rèn)識類中的訪問類型,類中的訪問類型基本分為以下三種:

  • Public:修飾的屬性或方法是共有的,不僅可以自己訪問,且實(shí)例對象和子類都能訪問,如不定義訪問類型默認(rèn)類型就是 Publice
  • Private:修飾的屬性或方法是私有的,只有自己可以訪問,實(shí)例對象和子類都不能訪問
  • Protected:修飾的屬性或方法是受保護(hù)的,只有自己和自己的子類中可以訪問,實(shí)例對象無法訪問

通過栗子我們來瞅瞅 Private 的用法:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name
  }
  private run() {
    return `${this.name} is running`
  }
  eat() {
    return this.run()
  }
}
const snake = new Animal('snake')
console.log(snake.run()) // error run 為私有屬性,只能在 Animal 類中使用
console.log(snake.eat()) // 所以我們又在其中定義 eat 方法,通過 eat 調(diào)用 run

我們再來瞅瞅 Protected 的用法,其實(shí)也很簡單,跟著栗子看看就行:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name
  }
  protected eat() {
    return this.name + ' eat'
  }
}
const snake = new Animal('snake')
console.log(snake.eat()) // error 只能在類和子類中訪問,實(shí)例對象無法訪問

class Dog extends Animal {
  myEat() {
    return super.eat() // 子類內(nèi)部可以調(diào)用 eat 方法
  }
}
const dog = new Dog('dog')
console.log(dog.myEat())
構(gòu)造器 constructor

接下來認(rèn)識一下構(gòu)造器 constructor ,其實(shí) ts 中的類基本和 js es6 中的類用法相同,constructor 方法是類的默認(rèn)方法,通過 new 命令生成對象實(shí)例時(shí),自動調(diào)用該方法。一個(gè)類必須有 constructor 方法,如果沒有顯式定義,一個(gè)空的 constructor 方法會被默認(rèn)添加。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name
  }
}
const person = new Person('cc')
console.log(person.name)

constructor 接收的形參就是實(shí)例化傳過來的值,然后我們通過 this.name 將其賦值給類中的 name 屬性,那么我們能不能將 constructor 接收的形參直接初始化為類中的全局變量呢,我們只需要這樣寫就行了:

class Person {
  constructor(public name: string) {
  }
}
const person = new Person('cc')
console.log(person.name)

如果父類有構(gòu)造器,而子類也有自己的構(gòu)造器時(shí)編輯器就會報(bào)錯(cuò),代碼如下:

class Teacher extends Person {
  constructor() {} // 報(bào)錯(cuò)
  sayHi() {
    console.log(this.name)
  }
}

子類中聲明構(gòu)造器的時(shí)候,必須要調(diào)用 super() 去把父類的構(gòu)造器也調(diào)用一下,當(dāng)然這里我們只通過 super() 調(diào)用父類的構(gòu)造器也不行,因?yàn)楦割惖臉?gòu)造器中要求我們傳入一個(gè) name,所以我們子類的 super() 中也要加入傳參才行。栗子如下:

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

class Teacher extends Person {
  constructor(public age: number) {
    super('zz');
  }
  sayHi() {
    console.log(this.name)
  }
}
const person = new Person('cc')
const teacher = new Teacher(18)
console.log(teacher.name) // zz
console.log(teacher.age) // 18

當(dāng)然,如果我們父類的 constructor 沒有接收任何參數(shù),子類的構(gòu)造器也必須調(diào)用 super() 只是此時(shí)可以不用傳遞對應(yīng)的參數(shù):

class Person {
  constructor() {
  }
}
class Teacher extends Person {
  constructor(public age: number) {
    super()
  }
}
存儲器 (getters / setters)

此時(shí)你有沒有一個(gè)疑問,privateprotected 這些訪問類型存在的意義是什么呢?或者說我們在什么地方會用到呢?其實(shí) privateprotected 一般結(jié)合著存儲器來用,好吧?前面那個(gè)我都沒弄明白,怎么又突然來一個(gè)存儲器的概念?TypeScript 支持通過 getters/setters 來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問。我們還是用栗子來說話吧:

class Person {
  constructor(private _name: string) { }
  get name() {
    return this._name + ' wc'
  }
}
const person = new Person('cc')
console.log(person.name) // 'cc wc'

Person 類中我們有一個(gè)私有屬性 _name (一般私有屬性的定義我們都會在屬性前面加上 _ 表示這是一個(gè)私有屬性),我們不希望外界能直接訪問到這個(gè)私有屬性,所以此時(shí)我們可以通過 getters 像外曝光這個(gè)數(shù)據(jù),然后實(shí)例對象可以直接訪問到,使用 getters 之后此時(shí)實(shí)例就不是函數(shù)調(diào)用的方式訪問,而是直接通過屬性調(diào)用方式訪問。在 get 的過程中,我們可以對私密數(shù)據(jù)進(jìn)行加密處理,不讓外界猜到基礎(chǔ)數(shù)據(jù)的值。

我們想重新給這個(gè)私有屬性賦值,那么我們就可以使用 setters ,在 setters 中我們可以解析新設(shè)置的值,讓私有屬性的值等于解析過后的值:

class Person {
  constructor(private _name: string) { }
  get name() {
    return this._name
  }
  set name(name: string) {
    const realName = name.split(' ')[0]
    this._name = realName
  }
}
const person = new Person('cc')
person.name = 'cc wc'
console.log(person.name) // cc
靜態(tài)屬性

到目前為止,我們只討論了類的實(shí)例成員,那些僅當(dāng)類被實(shí)例化的時(shí)候才會被初始化的屬性。 我們也可以創(chuàng)建類的靜態(tài)成員,這些屬性存在于類本身上面而不是類的實(shí)例上。

我們可以給類的 constructor 上設(shè)置一個(gè) private,因?yàn)槊總€(gè)類實(shí)例之后都會自動執(zhí)行 constructor() ,而現(xiàn)在它上面設(shè)置了私有屬性,那么這個(gè)類就不能進(jìn)行實(shí)例化了,但是我們卻可以通過 static 關(guān)鍵字直接訪問這個(gè)類上的方法,而不是實(shí)例上的方法,舉個(gè)栗子:

class Person {
  private constructor() { }
  static eat() { // 使用 static 關(guān)鍵字將方法直接定義在類上
    console.log('eat...')
  }
}
const person = new Person('cc') // 無法實(shí)例化
console.log(Person.eat()) // 直接訪問類上的方法

延伸思考:是否可以在類上實(shí)現(xiàn)單例模式呢?

class Person {
  private constructor(public name: string) { }
  // 定義一個(gè)私有屬性 instance 將其類型設(shè)置為 Person
  private static instance: Person
  static getInstance() {
    if (!this.instance) {
      this.instance = new Person('cc')
    }
    return this.instance
  }
}
const person1 = Person.getInstance()
const person2 = Person.getInstance()
console.log(person1.name) // cc
console.log(person2.name) // cc
抽象類

抽象類做為其它派生類的基類使用。 它們一般不會直接被實(shí)例化。 不同于接口,抽象類可以包含成員的實(shí)現(xiàn)細(xì)節(jié)。 abstract 關(guān)鍵字是用于定義抽象類和在抽象類內(nèi)部定義抽象方法。這是官網(wǎng)給出的解釋,看這句話我們大概能知道抽象類前面都要加上 abstract 關(guān)鍵字,然后就是它不能直接實(shí)例化。

這里我們用栗子來說明,假如有多個(gè)圖像類,它們都有計(jì)算面積的方法:

// 3 個(gè)圖形類應(yīng)該都有一個(gè)公有的方法,比如說計(jì)算面積的方法
class Circle {
  getArea() { }
}
class Square {
  getArea() { }
}
class Triangle {
  getArea() { }
}

但是這三個(gè)圖形計(jì)算面積的方法確實(shí)不一樣的,他們并不能完全抽離成一個(gè)方法,此時(shí)我們就可以使用抽象類。抽象類中建立一個(gè)公用的抽象方法,抽象方法不負(fù)責(zé)具體的實(shí)現(xiàn),只是做一個(gè)定義。如下栗子:

// 建一個(gè)抽象圖形類
abstract class Geom {
  width: number;
  // 建一個(gè)抽象的圖形面積實(shí)現(xiàn)方法,但是具體實(shí)現(xiàn)不能在抽象方法里寫
  abstract getArea(): number
}
new Geom() // 報(bào)錯(cuò),不能直接實(shí)例化

然后我們在圖形類出繼承這個(gè)抽象類:

class Circle extends Geom {
  // 此時(shí)類中必須要有 getArea() 并且給其一個(gè)返回值,不然就會報(bào)錯(cuò)
  getArea() {
    return 123
  }
}
const circle = new Circle()
circle.width = 100
circle.getArea()

如果我們實(shí)例化這個(gè) Circle 類,就會發(fā)現(xiàn)這個(gè)實(shí)例上有 width 屬性和 getArea() 可以直接調(diào)用,有沒有感覺它和 extends 還有接口中的 implements 傻傻分不清,感覺功能都好相似,當(dāng)然具體的功能差異我們在后面繼續(xù)來講。

突然發(fā)現(xiàn)這篇筆記居然已經(jīng)寫了這么長了,泛型、枚舉等一些進(jìn)階語法還沒開始寫。不過基礎(chǔ)語法就先寫到這里吧,后面在寫進(jìn)階篇,文章整理僅供學(xué)習(xí)參考,如有錯(cuò)誤,歡迎指正!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容