前言
TypeScript 對於前端人員甚至後臺人員都不算是特別陌生的東西了(身邊很多java朋友看ts都覺得還不錯),今天來聊聊這玩意,基礎用法,以及專案中大家都是怎麼用的。 順便一說,ts這東西實在是太大了,因為他的型別可以很靈活的去組合,不過放心,本文不會涉及太多概念性的東西(也說不完),因為其實一個大專案,往往就是這些基本型別用得最多,像type和interface這些東西,不過也分職位,如果你是專案組長或者是你們公司前端負責人要求的特別嚴格或者你是寫工具的,封裝一些公用元件的,用這些特別的東西機會會比較多
在ts裡,型別最好是規定的越死越好,因為這東西本身就是來規範自己規範團隊的,如果要是全用any的話,不如直接用js,如果專案只有自己,那就更沒必要上這玩意了,給自己找麻煩
基礎型別
ts裡大家多少應該都聽過,一些number型別string型別包括函式的引數型別什麼nerver、void本文就不再多贅述了,因為這東西實在太簡單了。。。這裡就簡單的列一下
基本型別: number/string/boolean/Array/object
any null、undefined void never
工具
因為做實驗的時候每次都需要tsc編譯一下,然後node 檔案 太麻煩了,我這裡簡單寫了個gulp(沒用webpack因為gulp比較簡單方便並且快),大家願意用可以直接用
評論區有人指出來只編譯的話可以直接
tsc -w
其實是一樣的,不過我主要是為了自己方便watch的時候清屏,以及一切其他的檔案操作,所以gulp方便一點,這裡就留個架子,有興趣可以在評論區或者gulp官網找更多方便的命令
用法:
cnpm i -g gulp-cli
- 安裝gulp本身cnpm i
- 安裝本地依賴庫gulp watch
- 執行gulp的watch任務
package.json
{
"name": "test",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"gulp": "^4.0.2",
"gulp-clean": "^0.4.0",
"gulp-run": "^1.7.1",
"gulp-typescript": "^6.0.0-alpha.1",
"gulp-watch": "^5.0.1",
"typescript": "^3.7.4"
}
}
複製程式碼
gulpfile.js
const gulp = require('gulp'),
watch = require('gulp-watch'),
ts = require('gulp-typescript'),
run = require('gulp-run'),
clean = require('gulp-clean');
gulp.task('watch', () => {
return watch('./1.ts', () => {
gulp
.src('./1.ts')
.pipe(
ts({
target: 'ES6',
outFile: '1.js',
experimentalDecorators: true,
}),
)
.on('error', err => {
// console.error(err);
})
.pipe(gulp.dest('build'))
.pipe(run('node build/1.js'));
});
});
複製程式碼
專案結構如下
執行結果
這個build不用自己手動建立,直接執行gulp,會自動建立並且執行的
陣列
陣列的花樣其實還相對來說挺多的,這裡先介紹基礎用法,下文會講到和其他的一些東西配合
let arr = Array<number>; // 規定只能是裝數字的陣列
// 可以簡寫成 let arr = number[];
複製程式碼
Type - Interface
經常有人問我 ts type 和 interface 有啥區別,首先肯定一點,這倆功能肯定是有相同點,也肯定有區別,要不然作者也就不有病搞兩個出來了,這裡我們先說分別的用法,再說區別
我們們暫時理解成這兩個都是自定義型別的
首先ts約定型別是可以約定json內部的,這個大家都知道就像這樣 這樣是報錯的
嚴格遵守才可以
type
但是如果有一個型別特別常用,比如說是使用者型別,假設也是肯定有名字和年齡,每次定義變數的時候寫一堆那肯定是不行
這樣就可以在多個地方用了
interface
上面的例子直接改成interface也是一樣的
區別
其實這麼一看例子,大家可能會想,這不一樣麼,有啥區別
其實不是,這個interface嚴格翻譯來說叫 介面 其實這個東西我們們前面那種用法根本是不對的,也不能說不對,可以說是不是真正適合他的地方,這東西不是當型別用的,真正的用法是 需要把他用於實現 可能這時候有人會覺得,說了跟沒說一樣,怎麼就實現,實現什麼啊,說人話等等想象現在有個需求,我有一個類,這個累的是對http請求的封裝,這裡面可以直接把資料變成字串發到伺服器然後還可以拿回來的時候再解析成json,在寫這個例子前,我們先來介紹另一個東西
implements
implements 這個東西跟extends有點像,extends是從一個類繼承出來,這個implements不是繼承一個類,而是實現一個介面
那麼結合interface我們們來寫個例子
interface serializeable {
tostring(): string;
fromString(str: string): void;
}
class SendData implements serializeable {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public tostring() {
return JSON.stringify({
name: this.name,
age: this.age,
});
}
public fromString(str: string) {
let data = JSON.parse(str);
this.name = data.name;
this.age = data.age;
}
}
複製程式碼
順便一說這個,implements是可以同時實現多個介面的,就直接跟名字就可以了,像這樣
interface serializeable {
tostring(): string;
fromString(str: string): void;
}
interface serializeable2 {
}
class SendData implements serializeable, serializeable2 {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public tostring() {
return JSON.stringify({
name: this.name,
age: this.age,
});
}
public fromString(str: string) {
let data = JSON.parse(str);
this.name = data.name;
this.age = data.age;
}
}
複製程式碼
看到這。。。我相信有人還有疑問,那。。。。這東西到底怎麼用呢
參考我們們上面提到的 http的那個需求,嘗試用一下這玩意,假設,現在要傳送給伺服器的資料必須得實現我這個介面
interface serializeable {
tostring(): string;
fromString(str: string): void;
}
class SendData implements serializeable {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public tostring() {
return JSON.stringify({
name: this.name,
age: this.age,
});
}
public fromString(str: string) {
let data = JSON.parse(str);
this.name = data.name;
this.age = data.age;
}
}
function sendToServer(obj: serializeable) {}
sendToServer(new SendData('name', 18));
複製程式碼
這時候看控制檯是沒有任何錯誤的
但是我們們隨便換一個類
我這個SendData2沒有實現我的介面,這個時候他是不是就直接爆了呀,可能還是會有人覺得,那。。。。這有什麼用呢?
注意⚠️這個其實對於挑錯很有用,正常情況下,用js去寫的話,是不是需要在 sendToServer
這個函式裡執行 obj.tostring
或者這個介面的其他方法才會儲存,那這個就變成執行時的報錯了,如果專案大起來,找錯是特別麻煩的事,所以這個東西能直接避免一部分錯誤,真不錯對吧?
泛型
泛型稍微有點特殊,我們們先來看個例子,更能讓大家明白這東西的作用 這裡先寫一個函式,先不考慮實用性,就是有一個函式,可以傳進去一個數字,和迴圈的次數,然後返回一個數字陣列
function repeat(item: number, count: number): number[] {
let result: number[] = [];
for (let i = 0; i < count; i++) {
result.push(item);
}
return result;
}
let arr: number[] = repeat(13, 4);
console.log(arr);
複製程式碼
首先東西肯定是能出來,但是。。。。 repeat是吧,現在實現的只是迴圈數字,有沒有可能將來需要迴圈字串,布林值,那這個一個一個寫得寫多少 所以我們們現在得想辦法把型別傳過來,當然了 any當然也可以,不過。。。。參考我寫的前言那裡 如果用any的話乾脆用js算了 當然了,我們們這主要說明泛型,其實在這用any也是可以的
function repeat<T>(item: T, count: number): T[] {
let result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(item);
}
return result;
}
let arr: number[] = repeat<number>(13, 4);
console.log(arr);
let arr2: string[] = repeat<string>('aaa', 4);
console.log(arr2);
複製程式碼
是不是很簡單呢?
其實,大家看這東西有沒有感覺眼熟,是的沒錯,在ts裡陣列就是一個泛型,比如 Array<string>
並且,再引入一個概念就是
型別推測
就這剛才我們們的例子來說 其實直接不傳型別,也是可以出來的
ts是很聰明的,他可以自己根據你傳過來的型別,來推測你是什麼型別,當然了,該簡的簡,稍微複雜點的,比如說一個泛型類,內部宣告個陣列,然後有個add方法,第一次是數字,第二次是字串那他就推測不出來了,過份了肯定不行。 其實這個泛型說起來,非常的龐大,這個大家如果感興趣可以留言或者評論單開一章專門講它,因為這個泛型有一些變種,比方說有多兩個泛型,三個的,分別用在什麼地方,而且還可以有可選的型別,替換的型別,聯合的型別,交叉的型別,爛七八糟稀奇古怪的東西多了去了,所以。。。有興趣的話大家留言哈~
裝飾器
這個裝飾器其實我個人是很喜歡用的,他可以直接給class附加一些功能 其實這是有有人可能會有疑問,為啥我要用這玩意加,我直接加上不就完了麼,還省事,其實可以想象一下,現在需要用的使用者資料附加到我這個class身上,首先肯定一點,挨個加肯定是可以的,但是就是麻煩麼,俗話說得好,懶是推進人類進步最大的動力麼不是。 其實如果瞭解vue 2.x ts版的應該知道,他就是充滿了裝飾器的寫法(順便一說,目前vue3放出來的訊息是拋棄了這個裝飾器的寫法了,可能是因為這東西暫時是實驗性特性,具體還需要等後面通知)
看了vue的用法,我們們先來看看簡單的裝飾器該怎麼寫
類裝飾器
其實裝飾器就是一個函式,然後直接加一個@符放到class上就可以了,注意需要注意引數,要麼ts會給你報錯
注意⚠️這個fn只有在類裝飾器的時候才會只有一個引數,在屬性和方法的時候不一樣,這個下文會說
function fn(target){
}
@fn
class User {
}
複製程式碼
順便一提,如果你用的是vscode或者是什麼其他的編輯器的話,可能會給你報錯
這個也就是說剛才提到的實驗版功能的原因 看著礙眼的話可以直接新建一個 tsconfig.json{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
複製程式碼
寫上這個,就不會報錯了
其實這個 target
就是我們們的類的建構函式
function fn(target) {
console.log(target);
target.a = 12;
}
@fn
class User {}
console.log(User.a);
複製程式碼
這時候看結果會發現報了一個錯,並且結果還出來了,這個就很奇怪
事實上來說ts是很嚴格的,他必須在初始化的時候就得用,執行時是沒問題,結果也正常,但是人家就是檢測不到 那。。。怎麼辦呢? 很簡單,我們們其實直接在類上定義這個屬性就可以了,像這樣
function fn(target) {
console.log(target);
target.a = 12;
}
@fn
class User {
static a: number;
}
console.log(User.a);
複製程式碼
這時候再執行,就不會報錯了
裝飾器傳參
其實這個裝飾器傳參還稍微有點特殊,這個target(也就是 constructor
)會傳到函式return出來的函式內部,而最外層才是傳進來的引數,來看下程式碼
function fn(num:number) {
return function(constructor: Function){
constructor.prototype.a = num
}
}
@fn(12)
class User {
a: number;
}
let obj = new User();
console.log(obj.a);
複製程式碼
執行時會看到
成功了~ 很開心 而現在,我們們直接寫兩個類,就可以通過傳引數來區分了
function fn(num:number) {
return function(constructor: Function){
constructor.prototype.a = num
}
}
@fn(12)
class User {
a: number;
}
let obj = new User();
console.log(obj.a);
@fn(5)
class User2 {
a: number;
}
let obj2 = new User2();
console.log(obj2.a);
複製程式碼
結果:
真棒,對不對
進階
其實裝飾器到上一步已經能滿足大部分人的工作需求了,因為這個東西
- 是個實驗性的東西
- 工作中其實很少能用到
但是,還是有點小東西挺有意思,順便來分享一下
前言:我們們這個類,肯定是不知一個例項對吧,那麼接下來,我們們這麼寫,直接一個屬性一點毛病沒有,但是。。。萬一是個json呢,我們們來看個例子
改成json後,到這步還沒什麼錯,接下來
GG了,是不是改其中一個屬性另一個也跟著一塊兒改了呀,這也就是prototype這種方式的不完整,所以千萬別用剛才的那種裝飾器傳餐來寫真是專案,會出人命的。 但是怎麼辦呢。。。 給誰加都不對,直接說正確做法了,可以直接把之前的那個類,給它重寫了,但是有一個問題,又不能直接複製程式碼再來一套,廢話不多,直接上程式碼
function fn(num: number) {
return function<T extends {new(...arg:any[]):{}}>(constructor: T) {
return class extends constructor {
json: object = { a: num };
};
};
}
@fn(12)
class User {
json: {
a: number;
};
}
let obj = new User();
obj.json.a = 80;
console.log(obj.json);
let obj2 = new User();
console.log(obj2.json);
複製程式碼
這個時候,就沒問題了~是不是很簡單
當然。。。放下你們手中的刀,可能有人會說,等會等會,這玩意怎麼就突然變成這一大坨東西了,什麼 <T extends {new(...arg:any[]):{}}>
這都什麼玩意啊,對不對?
其實是這樣的,我們們可以先拋棄這個來看一眼
會看到一個錯誤,說這個constructor不是一個一個function, 因為它可能是一個 User,可能是任何一個類,那怎麼辦呢? 所以,我們們需要一個泛型函式但是直接寫T也不行,直接說了,我們們這個T要繼承一個介面,並且這個介面還不能是一個普通的介面,還需要是一個動態的介面,所以需要動態的建立一個函式 也就是上面程式碼的 new ()
並且呢,這裡面肯定是什麼引數都有,所以全拿出來 new(...args:any[])
什麼型別都有這種時候給一個 any
就可以,並且還需要返回一個{},結合起來就是 <T extends {new(...arg:any[]):{}}>
是不是很簡單呢~~~~~
好的,本節ts教程就寫到這了,大家有什麼問題歡迎在評論區評論噢 或者可以加我的qq和微信,我們們一起溝通 qq:
916829411
複製程式碼
微信:
Dyy916829411
複製程式碼