TypeScript
環境TypeScript
代碼的編譯與運行TypeScript
編寫的程序並不能直接經過瀏覽器運行,咱們須要先經過 TypeScript
編譯器把 TypeScript
代碼編譯成 JavaScript
代碼javascript
TypeScript
的編譯器是基於 Node.js
的,因此咱們須要先安裝 Node.js
css
Node.js
nodejs.orghtml
安裝完成之後,能夠經過 終端
或者 cmd
等命令行工具來調用 node
前端
# 查看當前 node 版本
node -v
複製代碼
TypeScript
編譯器經過 NPM
包管理工具安裝 TypeScript
編譯器java
npm i -g typescript
複製代碼
安裝完成之後,咱們能夠經過命令 tsc
來調用編譯器node
# 查看當前 tsc 編譯器版本
tsc -v
複製代碼
代碼編輯器 - vscodees6
vsCode
和 TypeScript
都是微軟的產品,vsCode
自己就是基於 TypeScript
進行開發的,vsCode
對 TypeScript
有着自然友好的支持web
TypeScript
文件算法
默認狀況下,TypeScript
的文件的後綴爲 .ts
TypeScript
代碼
// ./src/hello.ts
let str: string = 'Typescript';
複製代碼
使用咱們安裝的 TypeScript
編譯器 tsc
對 .ts
文件進行編譯
tsc ./src/hello.ts
複製代碼
默認狀況下會在當前文件所在目錄下生成同名的 js
文件
編譯命令 tsc
還支持許多編譯選項,這裏我先來了解幾個比較經常使用的
指定編譯文件輸出目錄
tsc --outDir ./dist ./src/hello.ts
複製代碼
指定編譯的代碼版本目標,默認爲 ES3
tsc --outDir ./dist --target ES6 ./src/hello.ts
複製代碼
在監聽模式下運行,當文件發生改變的時候自動編譯
tsc --outDir ./dist --target ES6 --watch ./src/hello.ts
複製代碼
經過上面幾個例子,咱們基本能夠了解 tsc 的使用了,可是你們應該也發現了,若是每次編譯都輸入這麼一大堆的選項實際上是很繁瑣的,好在TypeScript
編譯爲咱們提供了一個更增強大且方便的方式,編譯配置文件:tsconfig.json
,咱們能夠把上面的編譯選項保存到這個配置文件中
咱們能夠把編譯的一些選項保存在一個指定的 json
文件中,默認狀況下 tsc
命令運行的時候會自動去加載運行命令所在的目錄下的 tsconfig.json
文件,配置文件格式以下
{
"compilerOptions": {
"outDir": "./dist",
"target": "ES2015",
"watch": true,
},
// ** : 全部目錄(包括子目錄)
// * : 全部文件,也能夠指定類型 *.ts
"include": ["./src/**/*"]
}
複製代碼
有了單獨的配置文件,咱們就能夠直接運行
tsc
複製代碼
使用 --project
或 -p
指定配置文件目錄,會默認加載該目錄下的 tsconfig.json
文件
tsc -p ./configs
複製代碼
也能夠指定某個具體的配置文件
tsc -p ./configs/ts.json
複製代碼
程序 = 數據結構 + 算法 = 各類格式的數據 + 處理數據的邏輯
不一樣類型的數據有不一樣的操做方式或方法,如:字符串類型的數據就不該該直接參與數學運算
動態類型語言
程序運行期間才作數據類型檢查的語言,如:JavaScript
靜態類型語言
程序編譯期間作數據類型檢查的語言,如:Java
優勢
缺點
優勢
缺點
靜態類型語言的核心 : 類型系統
類型系統包含兩個重要組成部分
類型標註就是在代碼中給數據(變量、函數(參數、返回值))添加類型說明,當一個變量或者函數(參數)等被標註之後就不能存儲或傳入與標註類型不符合的類型
有了標註,TypeScript
編譯器就能按照標註對這些數據進行類型合法檢測。
有了標註,各類編輯器、IDE等就能進行智能提示
顧名思義,就是對數據的類型進行檢測。注意這裏,重點是類型兩字。
類型系統檢測的是類型,不是具體值(雖然,某些時候也能夠檢測值),好比某個參數的取值範圍(1-100之間),咱們不能依靠類型系統來完成這個檢測,它應該是咱們的業務層具體邏輯,類型系統檢測的是它的值類型是否爲數字!
在 TypeScript
中,類型標註的基本語法格式爲:
數據載體:類型
複製代碼
TypeScript
的類型標註,咱們能夠分爲
基礎類型包含:string,number,boolean
標註語法
let title: string = '吧';
let n: number = 100;
let isOk: boolean = true;
複製代碼
由於在 Null
和 Undefined
這兩種類型有且只有一個值,在標註一個變量爲 Null
和 Undefined
類型,那就表示該變量不能修改了
let a: null;
// ok
a = null;
// error
a = 1;
複製代碼
默認狀況下 null
和 undefined
是全部類型的子類型。 就是說你能夠把 null
和 undefined
其它類型的變量
let a: number;
// ok
a = null;
複製代碼
若是一個變量聲明瞭,可是未賦值,那麼該變量的值爲 undefined
,可是若是它同時也沒有標註類型的話,默認類型爲 any
,any
類型後面有詳細說明
// 類型爲 `number`,值爲 `undefined`
let a: number;
// 類型爲 `any`,值爲 `undefined`
複製代碼
小技巧
由於 null
和 undefined
都是其它類型的子類型,因此默認狀況下會有一些隱藏的問題
let a:number;
a = null;
// ok(實際運行是有問題的)
a.toFixed(1);
複製代碼
小技巧:指定
strictNullChecks
配置爲true
,能夠有效的檢測null
或者undefined
,避免不少常見問題
let a:number;
a = null;
// error
a.toFixed(1);
複製代碼
也可使咱們程序編寫更加嚴謹
let ele = document.querySelector('div');
// 獲取元素的方法返回的類型可能會包含 null,因此最好是先進行必要的判斷,再進行操做
if (ele) {
ele.style.display = 'none';
}
複製代碼
內置對象類型
在 JavaScript
中,有許多的內置對象,好比:Object、Array、Date……,咱們能夠經過對象的 構造函數 或者 類 來進行標註
let a: object = {};
// 數組這裏標註格式有點不太同樣,後面咱們在數組標註中進行詳細講解
let arr: Array<number> = [1,2,3];
let d1: Date = new Date();
複製代碼
自定義對象類型
另一種狀況,許多時候,咱們可能須要自定義結構的對象。這個時候,咱們能夠:
字面量標註:
let a: {username: string; age: number} = {
username: 'zMouse',
age: 35
};
// ok
a.username;
a.age;
// error
a.gender;
複製代碼
優勢
: 方便、直接
缺點
: 不利於複用和維護
接口:
// 這裏使用了 interface 關鍵字,在後面的接口章節中會詳細講解
interface Person {
username: string;
age: number;
};
let a: Person = {
username: 'zMouse',
age: 35
};
// ok
a.username;
a.age;
// error
a.gender;
複製代碼
優勢
: 複用性高
缺點
: 接口只能做爲類型標註使用,不能做爲具體值,它只是一種抽象的結構定義,並非實體,沒有具體功能實現
類與構造函數:
// 類的具體使用,也會在後面的章節中講解
class Person {
constructor(public username: string, public age: number) {
}
}
// ok
a.username;
a.age;
// error
a.gender;
複製代碼
優勢
: 功能相對強大,定義實體的同時也定義了對應的類型
缺點
: 複雜,好比只想約束某個函數接收的參數結構,沒有必要去定一個類,使用接口會更加簡單
interface AjaxOptions {
url: string;
method: string;
}
function ajax(options: AjaxOptions) {}
ajax({
url: '',
method: 'get'
});
複製代碼
擴展
包裝對象:
這裏說的包裝對象其實就是 JavaScript
中的 String
、Number
、Boolean
,咱們知道 string
類型 和 String
類型並不同,在 TypeScript
中也是同樣
let a: string;
a = '1';
// error String有的,string不必定有(對象有的,基礎類型不必定有)
a = new String('1');
let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';
複製代碼
TypeScript
中數組存儲的類型必須一致,因此在標註數組類型的時候,同時要標註數組中存儲的數據類型
使用泛型標註
// <number> 表示數組中存儲的數據類型,泛型具體概念後續會講
let arr1: Array<number> = [];
// ok
arr1.push(100);
// error
arr1.push('吧');
複製代碼
簡單標註
let arr2: string[] = [];
// ok
arr2.push('吧');
// error
arr2.push(1);
複製代碼
元組相似數組,可是存儲的元素類型沒必要相同,可是須要注意:
let data1: [string, number] = ['吧', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);
複製代碼
枚舉的做用組織收集一組關聯數據的方式,經過枚舉咱們能夠給一組有關聯意義的數據賦予一些友好的名字
enum HTTP_CODE {
OK = 200,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;
複製代碼
注意事項:
字符串類型枚舉
枚舉類型的值,也能夠是字符串類型
enum URLS {
USER_REGISETER = '/user/register',
USER_LOGIN = '/user/login',
// 若是前一個枚舉值類型爲字符串,則後續枚舉項必須手動賦值
INDEX = 0
}
複製代碼
注意:若是前一個枚舉值類型爲字符串,則後續枚舉項必須手動賦值
小技巧:枚舉名稱能夠是大寫,也能夠是小寫,推薦使用全大寫(一般使用全大寫的命名方式來標註值爲常量)
表示沒有任何數據的類型,一般用於標註無返回值函數的返回值類型,函數默認標註類型爲:void
function fn():void {
// 沒有 return 或者 return undefined
}
複製代碼
在
strictNullChecks
爲false
的狀況下,undefined
和null
均可以賦值給void
,可是當strictNullChecks
爲true
的狀況下,只有undefined
才能夠賦值給void
當一個函數永遠不可能執行 return
的時候,返回的就是 never
,與 void 不一樣,void
是執行了 return
, 只是沒有值,never
是不會執行 return
,好比拋出錯誤,致使函數終止執行
function fn(): never {
throw new Error('error');
}
複製代碼
有的時候,咱們並不肯定這個值究竟是什麼類型或者不須要對該值進行類型檢測,就能夠標註爲 any
類型
let a: any;
複製代碼
any
類型any
類型any
類型也能夠賦值給任意類型any
類型有任意屬性和方法注意:標註爲 any
類型,也意味着放棄對該值的類型檢測,同時放棄 IDE 的智能提示
小技巧:當指定
noImplicitAny
配置爲true
,當函數參數出現隱含的any
類型時報錯
unknow,3.0 版本中新增,屬於安全版的 any,可是與 any 不一樣的是:
在 JavaScript 函數是很是重要的,在 TypeScript 也是如此。一樣的,函數也有本身的類型標註格式
函數名稱( 參數1: 類型, 參數2: 類型... ): 返回值類型;
複製代碼
function add(x: number, y: number): number {
return x + y;
}
複製代碼
函數更多的細節內容,在後期有專門的章節來進行深刻的探討
聯合類型也能夠稱爲多選類型,當咱們但願標註一個變量爲多個類型之一時能夠選擇聯合類型標註,或 的關係
function css(ele: Element, attr: string, value: string|number) {
// ...
}
let box = document.querySelector('.box');
// document.querySelector 方法返回值就是一個聯合類型
if (box) {
// ts 會提示有 null 的可能性,加上判斷更嚴謹
css(box, 'width', '100px');
css(box, 'opacity', 1);
css(box, 'opacity', [1,2]); // 錯誤
}
複製代碼
交叉類型也能夠稱爲合併類型,能夠把多種類型合併到一塊兒成爲一種新的類型,而且 的關係
對一個對象進行擴展:
interface o1 {x: number, y: string};
interface o2 {z: number};
let o: o1 & o2 = Object.assign({}, {x:1,y:'2'}, {z: 100});
複製代碼
小技巧
TypeScript
在編譯過程當中只會轉換語法(好比擴展運算符,箭頭函數等語法進行轉換,對於API
是不會進行轉換的(也不必轉換,而是引入一些擴展庫進行處理的),若是咱們的代碼中使用了target
中沒有的API
,則須要手動進行引入,默認狀況下TypeScript
會根據target
載入核心的類型庫
target
爲es5
時:["dom", "es5", "scripthost"]
target
爲es6
時:["dom", "es6", "dom.iterable", "scripthost"]
若是代碼中使用了這些默認載入庫之外的代碼,則能夠經過
lib
選項來進行設置
有的時候,咱們但願標註的不是某個類型,而是一個固定值,就可使用字面量類型,配合聯合類型會更有用
function setPosition(ele: Element, direction: 'left' | 'top' | 'right' | 'bottom') {
// ...
}
// ok
box && setDirection(box, 'bottom');
// error
box && setDirection(box, 'hehe');
複製代碼
有的時候類型標註比較複雜,這個時候咱們能夠類型標註起一個相對簡單的名字
type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
// ...
}
複製代碼
這裏須要注意一下,若是使用 type
來定義函數類型,和接口有點不太相同
type callback = (a: string) => string;
let fn: callback = function(a) {};
// 或者直接
let fn: (a: string) => string = function(a) {}
複製代碼
interface
object
/class
/function
的類型interface
自動合併,利於擴展type
每次都顯式標註類型會比較麻煩,TypeScript 提供了一種更加方便的特性:類型推導。TypeScript 編譯器會根據當前上下文自動的推導出對應的類型標註,這個過程發生在:
// 自動推斷 x 爲 number
let x = 1;
// 不能將類型「"a"」分配給類型「number」
x = 'a';
// 函數參數類型、函數返回值會根據對應的默認值和返回值進行自動推斷
function fn(a = 1) {return a * a}
複製代碼
有的時候,咱們可能標註一個更加精確的類型(縮小類型標註範圍),好比:
let img = document.querySelector('#img');
複製代碼
咱們能夠看到 img 的類型爲 Element,而 Element 類型其實只是元素類型的通用類型,若是咱們去訪問 src 這個屬性是有問題的,咱們須要把它的類型標註得更爲精確:HTMLImageElement 類型,這個時候,咱們就可使用類型斷言,它相似於一種 類型轉換:
let img = <HTMLImageElement>document.querySelector('#img');
複製代碼
或者
let img = document.querySelector('#img') as HTMLImageElement;
複製代碼
注意:斷言只是一種預判,並不會數據自己產生實際的做用,即:相似轉換,但並不是真的轉換了
前面咱們說到,TypeScript 的核心之一就是對值(數據)所具備的結構進行類型檢查,除了一些前面說到基本類型標註,針對對象類型的數據,除了前面提到的一些方式意外,咱們還能夠經過: Interface (接口),來進行標註。
接口:對複雜的對象類型進行標註的一種方式,或者給其它代碼定義一種契約(好比:類)
接口的基礎語法定義結構特別簡單
interface Point {
x: number;
y: number;
}
複製代碼
上面的代碼定義了一個類型,該類型包含兩個屬性,一個 number 類型的 x 和一個 number 類型的 y,接口中多個屬性之間可使用 逗號 或者 分號 進行分隔
咱們能夠經過這個接口來給一個數據進行類型標註
let p1: Point = {
x: 100,
y: 100
};
複製代碼
注意:接口是一種 類型 ,不能做爲 值 使用
interface Point {
x: number;
y: number;
}
let p1 = Point; //錯誤
複製代碼
固然,接口的定義規則遠遠不止這些
接口也能夠定義可選的屬性,經過 ? 來進行標註
interface Point {
x: number;
y: number;
color?: string;
}
複製代碼
其中的 color? 表示該屬性是可選的
咱們還能夠經過 readonly 來標註屬性爲只讀
interface Point {
readonly x: number;
readonly y: number;
}
複製代碼
當咱們標註了一個屬性爲只讀,那麼該屬性除了初始化之外,是不能被再次賦值的
有的時候,咱們但願給一個接口添加任意屬性,能夠經過索引類型來實現
數字類型索引
interface Point {
x: number;
y: number;
[prop: number]: number;
}
複製代碼
字符串類型索引
interface Point {
x: number;
y: number;
[prop: string]: number;
}
複製代碼
數字索引是字符串索引的子類型
注意:索引簽名參數類型必須爲 string 或 number 之一,但二者可同時出現
interface Point {
[prop1: string]: string;
[prop2: number]: string;
}
複製代碼
注意:當同時存在數字類型索引和字符串類型索引的時候,數字類型的值類型必須是字符串類型的值類型或子類型
interface Point1 {
[prop1: string]: string;
[prop2: number]: number; // 錯誤
}
interface Point2 {
[prop1: string]: Object;
[prop2: number]: Date; // 正確
}
複製代碼
咱們還可使用接口來描述一個函數
interface IFunc {
(a: string): string;
}
let fn: IFunc = function(a) {}
複製代碼
注意,若是使用接口來單獨描述一個函數,是沒
key
的
多個同名的接口合併成一個接口
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10}
複製代碼
this
一個函數的標註包含
function fn(a: string): string {};
let fn: (a: string) => string = function(a) {};
type callback = (a: string): string;
interface ICallBack {
(a: string): string;
}
let fn: callback = function(a) {};
let fn: ICallBack = function(a) {};
複製代碼
經過參數名後面添加 ?
來標註該參數是可選的
let div = document.querySelector('div');
function css(el: HTMLElement, attr: string, val?: any) {
}
// 設置
div && css( div, 'width', '100px' );
// 獲取
div && css( div, 'width' );
複製代碼
咱們還能夠給參數設置默認值
function sort(items: Array<number>, order = 'desc') {}
sort([1,2,3]);
// 也能夠經過聯合類型來限制取值
function sort(items: Array<number>, order:'desc'|'asc' = 'desc') {}
// ok
sort([1,2,3]);
// ok
sort([1,2,3], 'asc');
// error
sort([1,2,3], 'abc');
複製代碼
剩餘參數是一個數組,因此標註的時候必定要注意
interface IObj {
[key:string]: any;
}
function merge(target: IObj, ...others: Array<IObj>) {
return others.reduce( (prev, currnet) => {
prev = Object.assign(prev, currnet);
return prev;
}, target );
}
let newObj = merge({x: 1}, {y: 2}, {z: 3});
複製代碼
不管是 JavaScript
仍是 TypeScript
,函數中的 this
都是咱們須要關心的,那函數中 this
的類型該如何進行標註呢?
對於普通函數而言,this
是會隨着調用環境的變化而變化的,因此默認狀況下,普通函數中的 this
被標註爲 any
,但咱們能夠在函數的第一個參數位(它不佔據實際參數位置)上顯式的標註 this
的類型
interface T {
a: number;
fn: (x: number) => void;
}
let obj1:T = {
a: 1,
fn(x: number) {
//any類型
console.log(this);
}
}
let obj2:T = {
a: 1,
fn(this: T, x: number) {
//經過第一個參數位標註 this 的類型,它對實際參數不會有影響
console.log(this);
}
}
obj2.fn(1);
複製代碼
箭頭函數的 this
不能像普通函數那樣進行標註,它的 this
標註類型取決於它所在的做用域 this
的標註類型
interface T {
a: number;
fn: (x: number) => void;
}
let obj2: T = {
a: 2,
fn(this: T) {
return () => {
// T
console.log(this);
}
}
}
複製代碼
有的時候,同一個函數會接收不一樣類型的參數返回不一樣類型的返回值,咱們可使用函數重載來實現,經過下面的例子來體會一下函數重載
function showOrHide(ele: HTMLElement, attr: string, value: 'block'|'none'|number) {
//
}
let div = document.querySelector('div');
if (div) {
showOrHide( div, 'display', 'none' );
showOrHide( div, 'opacity', 1 );
// error,這裏是有問題的,雖然經過聯合類型可以處理同時接收不一樣類型的參數,可是多個參數之間是一種組合的模式,咱們須要的應該是一種對應的關係
showOrHide( div, 'display', 1 );
}
複製代碼
咱們來看一下函數重載
function showOrHide(ele: HTMLElement, attr: 'display', value: 'block'|'none');
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number);
function showOrHide(ele: HTMLElement, attr: string, value: any) {
ele.style[attr] = value;
}
let div = document.querySelector('div');
if (div) {
showOrHide( div, 'display', 'none' );
showOrHide( div, 'opacity', 1 );
// 經過函數重載能夠設置不一樣的參數對應關係
showOrHide( div, 'display', 1 );
}
複製代碼
interface PlainObject {
[key: string]: string|number;
}
function css(ele: HTMLElement, attr: PlainObject);
function css(ele: HTMLElement, attr: string, value: string|number);
function css(ele: HTMLElement, attr: any, value?: any) {
if (typeof attr === 'string' && value) {
ele.style[attr] = value;
}
if (typeof attr === 'object') {
for (let key in attr) {
ele.style[attr] = attr[key];
}
}
}
let div = document.querySelector('div');
if (div) {
css(div, 'width', '100px');
css(div, {
width: '100px'
});
// error,若是不使用重載,這裏就會有問題了
css(div, 'width');
}
複製代碼
面向對象編程中一個重要的核心就是:類
,當咱們使用面向對象的方式進行編程的時候,一般會首先去分析具體要實現的功能,把特性類似的抽象成一個一個的類,而後經過這些類實例化出來的具體對象來完成具體業務需求。
在類的基礎中,包含下面幾個核心的知識點,也是 TypeScript
與 EMCAScript2015+
在類方面共有的一些特性
class
關鍵字constructor
除了以上的共同特性之外,在 TypeScript
中還有許多 ECMAScript
沒有的,或當前還不支持的一些特性,如:抽象
經過 class
就能夠描述和組織一個類的結構,語法:
// 一般類的名稱咱們會使用 大坨峯命名 規則,也就是 (單詞)首字母大寫
class User {
// 類的特徵都定義在 {} 內部
}
複製代碼
經過 class
定義了一個類之後,咱們能夠經過 new
關鍵字來調用該類從而獲得該類型的一個具體對象:也就是實例化。
爲何類能夠像函數同樣去調用呢,其實咱們執行的並非這個類,而是類中包含的一個特殊函數:構造函數 - constructor
class User {
constructor() {
console.log('實例化...')
}
}
let user1 = new User;
複製代碼
默認狀況下,構造函數是一個空函數
構造函數會在類被實例化的時候調用
咱們定義的構造函數會覆蓋默認構造函數
若是在實例化(new)一個類的時候無需傳入參數,則能夠省略 ()
構造函數 constructor
不容許有return
和返回值類型標註的(由於要返回實例對象)
一般狀況下,咱們會把一個類實例化的時候的初始化相關代碼寫在構造函數中,好比對類成員屬性的初始化賦值
class User {
id: number;
username: string;
constructor(id: number, username: string) {
this.id = id;
this.username = username;
}
postArticle(title: string, content: string): void {
console.log(`發表了一篇文章: ${title}`)
}
}
let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');
複製代碼
在類內部,咱們能夠經過 this
關鍵字來訪問類的成員屬性和方法
class User {
id: number;
username: string;
postArticle(title: string, content: string): void {
// 在類的內部能夠經過 `this` 來訪問成員屬性和方法
console.log(`${this.username} 發表了一篇文章: ${title}`)
}
}
複製代碼
由於在構造函數中對類成員屬性進行傳參賦值初始化是一個比較常見的場景,因此 ts
提供了一個簡化操做:給構造函數參數添加修飾符來直接生成成員屬性
public
就是類的默認修飾符,表示該成員能夠在任何地方進行讀寫操做class User {
constructor( public id: number, public username: string ) {
// 能夠省略初始化賦值
}
postArticle(title: string, content: string): void {
console.log(`${this.username} 發表了一篇文章: ${title}`)
}
}
let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');
複製代碼
在 ts
中,也是經過 extends
關鍵字來實現類的繼承
class VIP extends User {
}
複製代碼
在子類中,咱們能夠經過 super
來引用父類
若是子類沒有重寫構造函數,則會在默認的 constructor
中調用 super()
若是子類有本身的構造函數,則須要在子類構造函數中顯示的調用父類構造函數 : super(//參數)
,不然會報錯
在子類構造函數中只有在 super(//參數)
以後才能訪問 this
在子類中,能夠經過 super
來訪問父類的成員屬性和方法
經過 super
訪問父類的的同時,會自動綁定上下文對象爲當前子類 this
class VIP extends User {
constructor( id: number, username: string, public score = 0 ) {
super(id, username);
}
postAttachment(file: string): void {
console.log(`${this.username} 上傳了一個附件: ${file}`)
}
}
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('標題', '內容');
vip1.postAttachment('1.png');
複製代碼
方法的重寫與重載
默認狀況下,子類成員方法集成自父類,可是子類也能夠對它們進行重寫和重載
class VIP extends User {
constructor( id: number, username: string, public score = 0 ) {
super(id, username);
}
// postArticle 方法重寫,覆蓋
postArticle(title: string, content: string): void {
this.score++;
console.log(`${this.username} 發表了一篇文章: ${title},積分:${this.score}`);
}
postAttachment(file: string): void {
console.log(`${this.username} 上傳了一個附件: ${file}`)
}
}
// 具體使用場景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('標題', '內容');
複製代碼
class VIP extends User {
constructor( id: number, username: string, public score = 0 ) {
super(id, username);
}
// 參數個數,參數類型不一樣:重載
postArticle(title: string, content: string): void;
postArticle(title: string, content: string, file: string): void;
postArticle(title: string, content: string, file?: string) {
super.postArticle(title, content);
if (file) {
this.postAttachment(file);
}
}
postAttachment(file: string): void {
console.log(`${this.username} 上傳了一個附件: ${file}`)
}
}
// 具體使用場景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('標題', '內容');
vip1.postArticle('標題', '內容', '1.png');
複製代碼
有的時候,咱們但願對類成員(屬性、方法)進行必定的訪問控制,來保證數據的安全,經過 類修飾符
能夠作到這一點,目前 TypeScript 提供了四種修飾符:
這個是類成員的默認修飾符,它的訪問級別爲:
它的訪問級別爲:
它的訪問級別爲:
只讀修飾符只能針對成員屬性使用,且必須在聲明時或構造函數裏被初始化,它的訪問級別爲:
class User {
constructor( // 能夠訪問,可是一旦肯定不能修改 readonly id: number, // 能夠訪問,可是不能外部修改 protected username: string, // 外部包括子類不能訪問,也不可修改 private password: string ) {
// ...
}
// ...
}
let user1 = new User(1, 'zMouse', '123456');
複製代碼
有的時候,咱們須要對類成員 屬性
進行更加細膩的控制,就可使用 寄存器
來完成這個需求,經過 寄存器
,咱們能夠對類成員屬性的訪問進行攔截並加以控制,更好的控制成員屬性的設置和訪問邊界,寄存器分爲兩種:
訪問控制器,當訪問指定成員屬性時調用
- 函數式組件
- 類式組件
- props 與 state
- 組件通訊
- 表單與受控組件
設置控制器,當設置指定成員屬性時調用
class User {
constructor( readonly _id: number, readonly _username: string, private _password: string ) {
}
public set password(password: string) {
if (password.length >= 6) {
this._password = password;
}
}
public get password() {
return '******';
}
// ...
}
複製代碼
前面咱們說到的是成員屬性和方法都是實例對象的,可是有的時候,咱們須要給類自己添加成員,區分某成員是靜態仍是實例的:
this
,那麼該方法就是靜態的type IAllowFileTypeList = 'png'|'gif'|'jpg'|'jpeg'|'webp';
class VIP extends User {
// static 必須在 readonly 以前
static readonly ALLOW_FILE_TYPE_LIST: Array<IAllowFileTypeList> = ['png','gif','jpg','jpeg','webp'];
constructor( id: number, username: string, private _allowFileTypes: Array<IAllowFileTypeList> ) {
super(id, username);
}
info(): void {
// 類的靜態成員都是使用 類名.靜態成員 來訪問
// VIP 這種類型的用戶容許上傳的全部類型有哪一些
console.log(VIP.ALLOW_FILE_TYPE_LIST);
// 當前這個 vip 用戶容許上傳類型有哪一些
console.log(this._allowFileTypes);
}
}
let vip1 = new VIP(1, 'zMouse', ['jpg','jpeg']);
// 類的靜態成員都是使用 類名.靜態成員 來訪問
console.log(VIP.ALLOW_FILE_TYPE_LIST);
this.info();
複製代碼
有的時候,一個基類(父類)的一些方法沒法肯定具體的行爲,而是由繼承的子類去實現,看下面的例子:
如今前端比較流行組件化設計,好比 React
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
//...
}
}
複製代碼
根據上面代碼,咱們能夠大體設計以下類結構
props
屬性,能夠經過構造函數進行初始化,由父級定義state
屬性,由父級定義render
的方法class Component<T1, T2> {
public state: T2;
constructor( public props: T1 ) {
// ...
}
render(): string {
// ...不知道作點啥纔好,可是爲了不子類沒有 render 方法而致使組件解析錯誤,父類就用一個默認的 render 去處理可能會出現的錯誤
}
}
interface IMyComponentProps {
title: string;
}
interface IMyComponentState {
val: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
val: 1
}
}
render() {
this.props.title;
this.state.val;
return `<div>組件</div>`;
}
}
複製代碼
上面的代碼雖然從功能上講沒什麼太大問題,可是咱們能夠看到,父類的 render
有點尷尬,其實咱們更應該從代碼層面上去約束子類必須得有 render
方法,不然編碼就不能經過
若是一個方法沒有具體的實現方法,則能夠經過 abstract 關鍵字進行修飾
abstract class Component<T1, T2> {
public state: T2;
constructor(
public props: T1
) {
}
public abstract render(): string;
}
複製代碼
使用抽象類有一個好處:
約定了全部繼承子類的所必須實現的方法,使類的設計更加的規範
使用注意事項:
- abstract 修飾的方法不能有方法體
- 若是一個類有抽象方法,那麼該類也必須爲抽象的
- 若是一個類是抽象的,那麼就不能使用 new 進行實例化(由於抽象類表名該類有未實現的方法,因此不容許實例化)
- 若是一個子類繼承了一個抽象類,那麼該子類就必須實現抽象類中的全部抽象方法,不然該類還得聲明爲抽象的
在前面咱們已經學習了接口的使用,經過接口,咱們能夠爲對象定義一種結構和契約。咱們還能夠把接口與類進行結合,經過接口,讓類去強制符合某種契約,從某個方面來講,當一個抽象類中只有抽象的時候,它就與接口沒有太大區別了,這個時候,咱們更推薦經過接口的方式來定義契約
TypeScript
只支持單繼承,即一個子類只能有一個父類,可是一個類能夠實現過個接口在一個類中使用接口並非使用 extends
關鍵字,而是 implements
implements
了一個接口,那麼就必須實現該接口中定義的契約,
分隔implements
與 extends
可同時存在interface ILog {
getInfo(): string;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
val: 1
}
}
render() {
this.props.title;
this.state.val;
return `<div>組件</div>`;
}
getInfo() {
return `組件:MyComponent,props:${this.props},state:${this.state}`;
}
}
複製代碼
實現多個接口
interface ILog {
getInfo(): string;
}
interface IStorage {
save(data: string): void;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog, IStorage {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
val: 1
}
}
render() {
this.props.title;
this.state.val;
return `<div>組件</div>`;
}
getInfo(): string {
return `組件:MyComponent,props:${this.props},state:${this.state}`;
}
save(data: string) {
// ... 存儲
}
}
複製代碼
接口也能夠繼承
interface ILog {
getInfo(): string;
}
interface IStorage extends ILog {
save(data: string): void;
}
複製代碼
當咱們在 TypeScript 定義一個類的時候,其實同時定義了兩個不一樣的類型
首先,對象類型好理解,就是咱們的 new 出來的實例類型
那類類型是什麼,咱們知道 JavaScript 中的類,或者說是 TypeScript 中的類其實本質上仍是一個函數,固然咱們也稱爲構造函數,那麼這個類或者構造函數自己也是有類型的,那麼這個類型就是類的類型
class Person {
// 屬於類的
static type = '人';
// 屬於實例的
name: string;
age: number;
gender: string;
// 類的構造函數也是屬於類的
constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
this.name = name;
this.age = age;
this.gender = gender;
}
public eat(): void {
// ...
}
}
let p1 = new Person('zMouse', 35, '男');
p1.eat();
Person.type;
複製代碼
上面例子中,有兩個不一樣的數據
Person
類(構造函數)Person
實例化出來的對象 p1
對應的也有兩種不一樣的類型
Person
)typeof Person
)用接口的方式描述以下
interface Person {
name: string;
age: number;
gender: string;
eat(): void;
}
interface PersonConstructor {
// new 表示它是一個構造函數
new (name: string, age: number, gender: '男'|'女'): PersonInstance;
type: string;
}
複製代碼
在使用的時候要格外注意
function fn1(arg: Person /*若是但願這裏傳入的Person 的實例對象*/) {
arg.eat();
}
fn1( new Person('', 1, '男') );
function fn2(arg: typeof Person /*若是但願傳入的Person構造函數*/) {
new arg('', 1, '男');
}
fn2(Person);
複製代碼
雖然早期的時候,TypeScript
有一套本身的模塊系統實現,可是隨着更新,以及 JavaScript
模塊化的日趨成熟,TypeScript
對 ESM
模塊系統的支持也是愈來愈完善
不管是 JavaScript
仍是 TypeScript
都是以一個文件做爲模塊最小單元
import
或者 export
的文件都被當成一個模塊import
或者 export
,那麼它的內容就是全局可見的若是一個文件中沒有頂級 import
或者 export
,那麼它的內容就是全局的,整個項目可見的
// a.ts
let a1 = 100;
let a2 = 200;
複製代碼
// b.ts
// ok, 100
console.log(a1);
// error
let a2 = 300;
複製代碼
不推薦使用全局模塊,由於它會容易形成代碼命名衝突(全局變量污染)
任何一個包含了頂級 import
或者 export
的文件都會當作一個模塊,在 TypeScript
中也稱爲外部模塊。
TypeScript
與 ESM
語法相似
使用 export
導出模塊內部數據(變量、函數、類、類型別名、接口……)
使用 import
導入外部模塊數據
TypeScript
編譯器也可以根據相應的編譯參數,把代碼編譯成指定的模塊系統使用的代碼
module
選項在 TypeScript
編譯選項中,module
選項是用來指定生成哪一個模塊系統的代碼,可設置的值有:"none"
、"commonjs"
、"amd"
、"udm"
、"es6"
/"es2015/esnext"
、"System"
target=="es3" or "es5"
:默認使用 commonjs
es6
若是一個模塊沒有默認導出
// m1.ts
export let obj = {
x: 1
}
複製代碼
則在引入該模塊的時候,須要使用下列一些方式來導入
// main.ts
// error: 提示 m1 模塊沒有默認導出
import v from './m1'
// 能夠簡單的使用以下方式
import {obj} from './m1'
console.log(obj.x)
// or
import * as m1 from './m1'
console.log(m1.obj.x)
複製代碼
TS
文件有的時候,咱們須要引入一些 js
的模塊,好比導入一些第三方的使用 js
而非 ts
編寫的模塊,默認狀況下 tsc
是不對非 ts
模塊文件進行處理的
咱們能夠經過 allowJs
選項開啓該特性
// m1.js
export default 100;
// main.ts
import m1 from './m1.js'
複製代碼
ESM
模塊中的默認值問題在 ESM
中模塊能夠設置默認導出值
export default 'hello';
複製代碼
可是在 CommonJS
、AMD
中是沒有默認值設置的,它們導出的是一個對象(exports
)
module.exports.obj = {
x: 100
}
複製代碼
在 TypeScript
中導入這種模塊的時候會出現 模塊沒有默認導出的錯誤提示
。
簡單一些的作法:
import * as m from './m1.js'
複製代碼
經過配置選項解決:
allowSyntheticDefaultImports
設置爲:true
,容許從沒有設置默認導出的模塊中默認導入。
雖然經過上面的方式能夠解決編譯過程當中的檢測問題,可是編譯後的具體要運行代碼仍是有問題的
esModuleInterop
設置爲:true
,則在編譯的同時生成一個 __importDefault
函數,用來處理具體的 default
默認導出
注意:以上設置只能當
module
不爲es6+
的狀況下有效
TypeScript 2.9+
版本添加了一個新的編譯選項:resolveJsonModule
,它容許咱們把一個 JSON
文件做爲模塊進行加載
resolveJsonModule
設置爲:true
,能夠把 json
文件做爲一個模塊進行解析
data.json
{
"name": "zMouse",
"age": 35,
"gender": "男"
}
複製代碼
ts文件
import * as userData from './data.json';
console.log(userData.name);
複製代碼
模塊解析是指編譯器在查找導入模塊內容時所遵循的流程。
根據模塊引用是相對的仍是非相對的,模塊導入會以不一樣的方式解析。
相對導入是以 /
、./
或 ../
開頭的引用
// 導入根目錄下的 m1 模塊文件
import m1 from '/m1'
// 導入當前目錄下的 mods 目錄下的 m2 模塊文件
import m2 from './mods/m2'
// 導入上級目錄下的 m3 模塊文件
import m3 from '../m3'
複製代碼
全部其它形式的導入被看成非相對的
import m1 from 'm1'
複製代碼
爲了兼容不一樣的模塊系統(CommonJS
、ESM
),TypeScript
支持兩種不一樣的模塊解析策略:Node
、Classic
,當 --module
選項爲:AMD
、System
、ES2015
的時候,默認爲 Classic
,其它狀況爲 Node
除了根據 --module
選項自動選擇默認模塊系統類型,咱們還能夠經過 --moduleResolution
選項來手動指定解析策略
// tsconfig.json
{
...,
"moduleResolution": "node"
}
複製代碼
該策略是 TypeScript
之前的默認解析策略,它已經被新的 Node
策略所取代,如今使用該策略主要是爲了向後兼容
// /src/m1/a.ts
import b from './b.ts'
複製代碼
解析查找流程:
默認後綴補全
// /src/m1/a.ts
import b from './b'
複製代碼
解析查找流程:
/src/m1/b.ts
/src/m1/b.d.ts
// /src/m1/a.ts
import b from 'b'
複製代碼
對於非相對模塊的導入,則會從包含導入文件的目錄開始依次向上級目錄遍歷查找,直到根目錄爲止
/src/m1/b.ts
/src/m1/b.d.ts
/src/b.ts
/src/b.d.ts
/b.ts
/b.d.ts
該解析策略是參照了 Node.js
的模塊解析機制
// node.js
// /src/m1/a.js
import b from './b'
複製代碼
在 Classic
中,模塊只會按照單個的文件進行查找,可是在 Node.js
中,會首先按照單個文件進行查找,若是不存在,則會按照目錄進行查找
// node.js
// /src/m1/a.js
import b from 'b'
複製代碼
對於非相對導入模塊,解析是很特殊的,Node.js
會這一個特殊文件夾 node_modules
裏查找,而且在查找過程當中從當前目錄的 node_modules
目錄下逐級向上級文件夾進行查找
TypeScript
如今使用了與 Node.js
相似的模塊解析策略,可是 TypeScript
增長了其它幾個源文件擴展名的查找(.ts
、.tsx
、.d.ts
),同時 TypeScript
在 package.json
裏使用字段 types
來表示 main
的意義
在 TS
中,export
和 import
稱爲 外部模塊,TS
中還支持一種內部模塊 namespace
,它的主要做用只是單純的在文件內部(模塊內容)隔離做用域
namespace k1 {
let a = 10;
export var obj = {
a
}
}
namespace k2 {
let a = 20;
console.log(k1.obj);
}
複製代碼
裝飾器-Decorators
在 TypeScript
中是一種能夠在不修改類代碼的基礎上經過添加標註的方式來對類型進行擴展的一種方式
在
TypeScript
中,裝飾器只能在類中使用
裝飾器的使用極其的簡單
啓用裝飾器特性
experimentalDecorators: true
// 裝飾器函數
function log(target: Function, type: string, descriptor: PropertyDescriptor) {
let value = descriptor.value;
descriptor.value = function(a: number, b: number) {
let result = value(a, b);
console.log('日誌:', {
type,
a,
b,
result
})
return result;
}
}
// 原始類
class M {
@log
static add(a: number, b: number) {
return a + b;
}
@log
static sub(a: number, b: number) {
return a - b;
}
}
let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
複製代碼
裝飾器
是一個函數,它能夠經過 @裝飾器函數
這種特殊的語法附加在 類
、方法
、訪問符
、屬性
、參數
上,對它們進行包裝,而後返回一個包裝後的目標對象(類
、方法
、訪問符
、屬性
、參數
),裝飾器工做在類的構建階段,而不是使用階段
function 裝飾器1() {}
...
@裝飾器1
class MyClass {
@裝飾器2
a: number;
@裝飾器3
static property1: number;
@裝飾器4
get b() {
return 1;
}
@裝飾器5
static get c() {
return 2;
}
@裝飾器6
public method1(@裝飾器5 x: number) {
//
}
@裝飾器7
public static method2() {}
}
複製代碼
目標
參數
目標
參數
目標
參數
目標
參數
目標
參數
實例裝飾器
屬性 => 訪問符 => 參數 => 方法
靜態裝飾器
屬性 => 訪問符 => 參數 => 方法
類
類
若是咱們須要給裝飾器執行過程當中傳入一些參數的時候,就可使用裝飾器工廠來實現
// 裝飾器函數
function log(callback: Function) {
return function(target: Function, type: string, descriptor: PropertyDescriptor) {
let value = descriptor.value;
descriptor.value = function(a: number, b: number) {
let result = value(a, b);
callback({
type,
a,
b,
result
});
return result;
}
}
}
// 原始類
class M {
@log(function(result: any) {
console.log('日誌:', result)
})
static add(a: number, b: number) {
return a + b;
}
@log(function(result: any) {
localStorage.setItem('log', JSON.stringify(result));
})
static sub(a: number, b: number) {
return a - b;
}
}
let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
複製代碼
在 裝飾器
函數中 ,咱們能夠拿到 類
、方法
、訪問符
、屬性
、參數
的基本信息,如它們的名稱,描述符 等,可是咱們想獲取更多信息就須要經過另外的方式來進行:元數據
元數據
:用來描述數據的數據,在咱們的程序中,對象
、類
等都是數據,它們描述了某種數據,另外還有一種數據,它能夠用來描述 對象
、類
,這些用來描述數據的數據就是 元數據
好比一首歌曲自己就是一組數據,同時還有一組用來描述歌曲的歌手、格式、時長的數據,那麼這組數據就是歌曲數據的元數據
reflect-metadata
首先,須要安裝 reflect-metadata
npm install reflect-metadata
複製代碼
咱們能夠 類
、方法
等數據定義元數據
類
、方法
等數據之上,可是又不會影響 類
、方法
自己的代碼設置
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)
調用方式
經過 Reflect.defineMetadata
方法調用來添加 元數據
經過 @Reflect.metadata
裝飾器來添加 元數據
import "reflect-metadata"
@Reflect.metadata("n", 1)
class A {
@Reflect.metadata("n", 2)
public static method1() {
}
@Reflect.metadata("n", 4)
public method2() {
}
}
// or
Reflect.defineMetadata('n', 1, A);
Reflect.defineMetadata('n', 2, A, 'method1');
let obj = new A();
Reflect.defineMetadata('n', 3, obj);
Reflect.defineMetadata('n', 4, obj, 'method2');
console.log(Reflect.getMetadata('n', A));
console.log(Reflect.getMetadata('n', A, ));
複製代碼
獲取
Reflect.getMetadata(metadataKey, target, propertyKey)
參數的含義與 defineMetadata
對應
import "reflect-metadata"
function L(type = 'log') {
return function(target: any) {
Reflect.defineMetadata("type", type, target);
}
}
// 裝飾器函數
function log(callback: Function) {
return function(target: any, name: string, descriptor: PropertyDescriptor) {
let value = descriptor.value;
let type = Reflect.getMetadata("type", target);
descriptor.value = function(a: number, b: number) {
let result = value(a, b);
if (type === 'log') {
console.log('日誌:', {
name,
a,
b,
result
})
}
if (type === 'storage') {
localStorage.setItem('storageLog', JSON.stringify({
name,
a,
b,
result
}));
}
return result;
}
}
}
// 原始類
@L('log')
class M {
@log
static add(a: number, b: number) {
return a + b;
}
@log
static sub(a: number, b: number) {
return a - b;
}
}
let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
複製代碼
emitDecoratorMetadata
在 tsconfig.json
中有一個配置 emitDecoratorMetadata
,開啓該特性,typescript
會在編譯以後自動給 類
、方法
、訪問符
、屬性
、參數
添加以下幾個元數據
Function
類型import "reflect-metadata"
function n(target: any) {
}
function f(name: string) {
return function(target: any, propertyKey: string, descriptor: any) {
console.log( 'design type', Reflect.getMetadata('design:type', target, propertyKey) );
console.log( 'params type', Reflect.getMetadata('design:paramtypes', target, propertyKey) );
console.log( 'return type', Reflect.getMetadata('design:returntype', target, propertyKey) );
}
}
function m(target: any, propertyKey: string) {
}
@n
class B {
@m
name: string;
constructor(a: string) {
}
@f('')
method1(a: string, b: string) {
return 'a'
}
}
複製代碼
編譯後
__decorate([
m,
__metadata("design:type", String)
], B.prototype, "name", void 0);
__decorate([
f(''),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", void 0)
], B.prototype, "method1", null);
B = __decorate([
n,
__metadata("design:paramtypes", [String])
], B);
複製代碼