Map

Redux 中的 compose 的 source code

compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for
* the resulting composite function.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/

export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

若是沒有參數的話就回傳一個預設的函式

若是只有一個則直接回傳 function

聊到 reduce 前 可以先談談 遞回

React Native import Image Error

React Native version: 0.51.0

React version: 16.0.0

Initial React Native Project

1
2
3
$ react-native init myapp
$ cd myapp
$ jest

Step1

安裝 react-native-router-fluxstyled-components

1
$ npm install styled-components react-native-router-flux

建立資料夾 和 檔案

1
2
$ mkdir -p src ./src/components
$ cd src/components && touch Home.js Counter.js && cd -

./src/components/Home.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { Component } from "react";
import styled from "styled-components/native";
import { Text, View } from "react-native";
import { Actions } from "react-native-router-flux";

const Container = styled.View`
width: 100%;
height: 100%;
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
`;

const ContentText = styled.Text`
font-size: 20;
font-weight: 400;
`;

export default class Counter extends Component {
render() {
return (
<Container>
<ContentText>
<Text onPress={Actions.counter}>Navigator to Counter</Text>
</ContentText>
</Container>
);
}
}

./src/components/Counter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { Component } from "react";
import styled from "styled-components/native";
import { Text, View, Button } from "react-native";
import { Actions } from "react-native-router-flux";

const Container = styled.View`
width: 100%;
height: 100%;
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
`;

const Row = styled.View`
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
`;

const ContentText = styled.Text`
font-size: 20;
font-weight: 400;
`;

const CounterText = styled.Text`
font-size: 16;
font-weight: 400;
`;

export default class Counter extends Component {
state = {
count: 0
};

render() {
return (
<Container>
<Row>
<ContentText>Counter</ContentText>
<CounterText>{this.state.count}</CounterText>
<Button
onPress={() => this.setState({ count: this.state.count + 1 })}
title="+"
/>
<Button
onPress={() => this.setState({ count: this.state.count - 1 })}
title="-"
/>
<ContentText onPress={Actions.pop}>Back</ContentText>
</Row>
</Container>
);
}
}

./App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from "react";
import { Router, Scene } from "react-native-router-flux";
import Home from "./src/components/Home";
import Counter from "./src/components/Counter";

export default class App extends Component<{}> {
render() {
return (
<Router>
<Scene key="root">
<Scene path="home" key="home" component={Home} />
<Scene path="counter" key="counter" component={Counter} />
</Scene>
</Router>
);
}
}
1
2
$ npm run run-ios //ok
$ npm run test // Error

import type { ^^^^^^
SyntaxError: Unexpected token import

在package.json 中增加 transform

package.json

1
2
3
4
5
6
7
8
9
{
...
"jest": {
"preset": "react-native",
"transform": {
"^.+\\.jsx?$": "babel-jest"
}
}
}

在重新執行測試的時候會產生另一個錯誤


({"Object.":function(module,exports,require,__dirname,__filename,global,jest){�PNG ...

因為這是在 react-native-router-flux 中的 Navbar 中的 png 在編譯的時候產生的錯誤

解法

1
$ npm install --save-dev identity-obj-proxy

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
...
"jest": {
"preset": "react-native",
"transform": {
"^.+\\.jsx?$": "babel-jest"
},
"transformIgnorePatterns": [
"!node_modules/react-runtime"
],
"moduleNameMapper": {
".+\\.(png|jpg|ttf|woff|woff2)$": "identity-obj-proxy"
}
}
}

Typescript-JSX

JSX

JSX 是一種類似 XML 的標記性語言,可以被轉換為合法的 Javascript 因為 React 的框架而開始流行,但是也可以,但是也可以使用在其他程式中

基本使用方式

TypeScript 使用 JSX 必須先做兩件事情

  1. 副檔名為 tsx
  2. 開啟 jsx 的功能

TypeScript 有三種 JSX 的模式, preserve, react, react-native

preserve

會保留 JSX 提供後續轉換使用

react

會生成 React.createElement 在使用前不需要再轉換

react-native

相當於 preserve 但是輸出的檔案副檔名為 .js

可以在命令列中使用 --jsx 或是在 tsconfig.json 中指定模式

as Oprator

寫一個 class

1
const foo = <foo>bar;

因為 JSX 語法解析困難,所以在 TypeScript 禁止使用 <> 來宣告,所以在 tsx 中改為

1
const foo = bar as foo;

as.ts 或是 .tsx 中都可以使用

Type Check

為了理解 JSX 如何檢查類型必須要先了解原生的元件根基於值得元件有什麼不同

假如有一個元件 <expr /> 可能會引用 div 或是 span 這類的標籤

  1. React HTML 標籤會自動生成 React.createElement('div') 但是自定義的元件部會生成 React.createElement(MyComponent)
  2. 原本 HTML 就有的 tag 本身支援類型檢查,但是自定義的元件則需要自己定義類型檢查

TypeScript 使用和 React 相同的規範來做區別

Intrinsic elements

Intrinsic elements 預設是 JSX.IntrinsicElements 做類型檢查,預設是 any

1
2
3
4
5
6
7
8
decalre namespace JSX {
interface IntrinsicElements {
foo: any
}
}

<foo /> // Success
<bar /> // Error

上面範例中 foo 可以執行,但是 bar 會報錯誤訊息,因為 bar 並沒有在 JSX.IntrinsicElements 內指定

也可以指定為所有

1
2
3
4
5
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}

一般使用 Component 如範例

1
2
3
import MyComponent from './MyComponent';
<MyComponent /> // Success
<OtherComponent /> // Error

Stateless Component (SFC)

範例

1
2
3
4
5
6
7
8
9
10
11
interface FooProp{
name: string,
X: number,
Y: number
}

declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
return <AnotherComponent name=prop.name />;
}
const Button = (prop: {value: string}, context: { color: string }) => <button>

因為 SFC 是簡單的 Function 所以可以盡量的使用

Class Component

這裡需要先介紹兩個新的名詞 the element class typeelement instance type

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyComponent {
render() {}
}

// use a construct signature
var myComponent = new MyComponent();

// element class type => MyComponent
// element instance type => { render: () => void }

function MyFactoryFunction() {
return {
render: () => {}
};
}

// use a call signature
var myComponent = MyFactoryFunction();

// element class type => FactoryFunction
// element instance type => { render: () => void }

element instance type 很有趣,他必須要指定給 JSX.ElementClass 否則就會報錯

預設 JSX.ElementClass{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
declare namespace JSX {
interface ElementClass {
render: any;
}
}

class MyComponent {
render() {}
}
function MyFactoryFunction02() {
return { render: () => {} };
}

<MyComponent />; // ok
<MyFactoryFunction />; // ok

class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}

// <NotAValidComponent />; // error
// <NotAValidFactoryFunction />; // error

TypeScript-Generic

泛型

泛型的 Hello world

1
2
3
function identity(arg: number): number {
return arg;
}

或是我們用 any 來宣告型態

1
2
3
function identity(arg: any): any {
return arg;
}

使用 any 導致這個函式可以接受任何類型的 arg 參數,但是這樣會錯失一些訊息

我們需要一種方式讓 input 和 output 類型是一樣的

1
2
3
function identity<T>(arg: T): T{
return arg;
}

藉由前面輸入的 T 協助接到 input 也希望和 response 是相同的,是相同的

1
2
3
4
5
function identity<T>(arg: T): T{
return arg;
}

let output = identity<string>("Mystring");

使用泛型變量

若是我們希望取回 arg 的長度但是並沒有指明 arg 具有 .length 這個屬性,
因為這個變量是任意類型,所以傳入的可能是字串或數字

那就沒有 length 這個屬性,我們可以先宣告陣列

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[]{
console.log(arg.length);
return arg;
}

現在 loggingIdentity 的輸入直是包含了 T 屬性的陣列,回傳的也是包含了 T 的陣列

也可以改寫成為

1
2
3
4
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

泛型 class

在函式忠宣告泛型的方式如下

1
2
3
4
5
function identity<T>(arg: T): T{
return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

也可以使用不同的名稱宣告,只要變數的數量吻合就可以了

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

也可以利用 signature 物件的方式來做宣告

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

上述範例可以利用 interface 來做宣告

1
2
3
4
5
6
7
8
interface GenericIdentityFn{
<T>(arg: <T>): T;
}
function identity<T>(arg: T): T{
return arg;
}

let myIdentity: GenericIdentityFn = identity;

藉由上述範例使用泛型來做宣告的話,就可以把這個參數的型態作為函式的輸入值

例如: Dictionary而不是單純的 Dictionary

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn<T> {
(arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;

泛型類別

泛型類別和泛型宣告方式是一樣的,利用 <> 在 class 後面

1
2
3
4
5
6
7
8
class GenericNumber<T>{
zeroValue: T;
add: (x: T, y: T) => T;
}

et myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

GenericNumber 很片面的 class,沒什麼去限制他使用 number 你可以使用字串或其他更複雜的物件

1
2
3
4
5
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

就像 interface 一樣,泛型類別只是確認你使用的是同一個型態,而什麼型態則不是他們限制的

在之前 類別中有提到,類別有分兩個部分 靜態與實例層,泛型類別只負責實例層

靜態層不能使用 泛型類別

Generic Constraints

之前有一個簡單的範例,如果我們傳入的類別沒有 length 這個屬性就會報錯

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

我們不希望使用 any 來做檢查,因為這樣會沒有任何錯誤訊息,希望在傳入的屬性中一定要有一個 length

為了達到這個目的,我們宣告一個 interface 裡面有 length 的屬性,然後繼承他的屬性

1
2
3
4
5
6
7
8
9
10
11
interface Lengthwise{
length: number;
}

function logginIdentity<T extends Lengthwise>(arg: T): T{
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}

// loggingIdentity(3); // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3});

這時候他就不是 any 參數,而是必須擁有 length 這個屬性

Typescript-Function

Functions

Function 在 Javascript 中是很基本的型態,可以用在隱藏資訊,或攥寫模組等功用

Javascript 中基本的 Function 有兩種

1
2
3
4
5
function add(x, y){
return x+y;
}

let myAdd = function(x, y){return x+y;}

在 Javascript 中的 Function 可以使用外部的變數,這個行為叫做 capture

1
2
3
4
let z = 100;
function addToZ(x, y){
return x + y + z;
}

使用 TypeScript 寫一個最基本的範例

1
2
3
4
5
function add (x: number, y: number): number{
return x + y;
}

let myAdd = function(x: number, y: number): number{return x+y;};

完整的 Function Type 範例

1
2
3
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number{
return x+y;
}

上述範例中輸入的參數可以定義類別,同時也宣告 Function 回傳的類別,並且有兩個相同的宣告類別,

但是做事在一開始就宣告完整的 Function 類別,那之後就可以省略

範例

1
2
3
4
5
let myAdd = function(x: number, y: number): number{
return x + y;
};

let myAdd2: (x: number, y: number) => number = function(x, y){return x + y;};

Optional and Default Parameters

TypeScript 中指定每個參數不代表他們不能是 null 或是 undefined

1
2
3
4
5
6
7
function buildName(firstName: string, lastName: string){
return firstName + ' ' + lastName;
}

// let result1 = buildName("Bob"); // error, too few parameters
// let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right

在 Javascript 中每一個 Function 都是非必要的,當你沒有輸入的時候值都會是 undefinedTypeScript 有宣告的都是必要的,但是也提供一個 ? 來宣告非必要

1
2
3
4
5
6
7
8
9
10
function buildName(firstName: string, lastName?: string){
if (lastName){
return firstName + " " + lastName;
}else{
return firstName;
}
}
let result1 = buildName("Bob"); // works correctly now
// let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right

TypeScript 非必要參數必須要在 必要參數的後面, TypeScript 也可以提供預設值的設定

範例

1
2
3
4
5
6
7
8
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}

let res1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let res2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
// let res3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let res4 = buildName("Bob", "Adams"); // ah, just right

上面的範例中可以看到,你可以先預設值給予預設值之後就不是必要參數,因為當你沒有輸入該參數的時候也會有預設輸入不會影響程式執行

Rest Parameters

ES6 也有一種特性 Rest

而這個特性在 TypeScript 也可以應用在 Function 之中,當你不知道之後輸入的參數值總共有幾個,可以利用這個特性將所有後面輸入的參數值組合成一個陣列

1
2
3
4
5
6
7
function buildName4(firstName: string, ...restOfName: string[]) {
console.log(`firstName: ${firstName}`);
console.log(`restOfName: ${restOfName}`);
return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName4("Joseph", "Samuel", "Lucas", "MacKinzie");

this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);

return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

上述範例是可以編譯的,但是在執行的時候會有錯誤

因為在執行照 createCadrPicker 的時候會找不到 suits 這個 Function 因為這是 this的作用域的問題

因為對 Javascript 來說 Function 也是物件,所以在上述範例中的 createCadrPicker 中的 this 是指這個 createCadrPicker Function 本身,但是這個 Function 並沒有 suits 這個屬性,所以他會找到 undefeind 在後面 this.suits[pickedSuit] 的時候因為 undefined 在 Javascript 並不是物件,所以就會造成這個錯誤

所以我們可以把這個範例做一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);

return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

其實我們做的修改只是將 function(){} 修改為 () => {} 但是因為在 Javascript 中的 arrow function的特性 所以他找到的 this 是指 desk 這個物件,就可以指導 suits 這個屬性這個屬性

this parameters

但是在上面範例中的 this 的型別依舊是 any 如果我們希望在 Function 定義 this 的型態就要將這個宣告放在 Function 的第一個參數

1
2
3
function f(this: void){
// ....
}

加上兩個 interfaceCardDeck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Card{
suit: string;
card: number
}
interface Deck{
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}

let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function(this: Deck){
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);

return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}

interface 中的 createCardPicker 有宣告了 this 的型態是 Deck 而不是 any 所以 --noImplicitThis 不會有錯誤

this parameters in callbacks

你在 callback 中使用 this 的話依舊會產生一些錯誤,因為 this 會是 undefine 你可以宣告一個 interface 來避免這種錯誤

1
2
3
interface UIElement{
addClickListener(onClick: (this: void, e: Event => void): void)
}

this: void 代表 addClickListener 預計 onClick 是一個 Fucntion 並沒有 this 的類別

1
2
3
4
5
6
7
8
9
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!

Overloads

Javascript 是一個動態繼承的程式語言,一個函式藉由輸入值得到不同的回傳值是十分常見的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x): any {
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
} else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

let myDeck = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 }
];
let pickedCard01 = myDeck[pickCard(myDeck)];
console.log("card: " + pickedCard01.card + " of " + pickedCard01.suit);
console.log(`pickedCard01: ${JSON.stringify(pickedCard01)}`);
let pickedCard02 = pickCard(15);
console.log("card: " + pickedCard02.card + " of " + pickedCard02.suit);
console.log(`pickedCard02: ${JSON.stringify(pickedCard02)}`);

pickCard 會依據我們傳進去的參數不同,回傳不同的資訊這樣的話我們該如何去定義呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let suits02 = ["hearts", "spades", "clubs", "diamonds"];

function pickCard1(x: { suit: string; card: number }[]): number;
function pickCard1(x: number): { suit: string; card: number };
function pickCard1(x): any {
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
} else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits02[pickedSuit], card: x % 13 };
}
}

let myDeck02 = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 }
];
let pickedCard001 = myDeck02[pickCard1(myDeck02)];
console.log("card: " + pickedCard001.card + " of " + pickedCard001.suit);

let pickedCard002 = pickCard1(15);
console.log("card: " + pickedCard002.card + " of " + pickedCard002.suit);

為了要讓 TypeScript 編譯的時候能夠選擇正確的型態,會宣告兩個不同的 PickCard1 然後分別宣告不同的 parmeter 而產生的不同的 response,然後在最後真正宣告 function pickCard1():any 設定回傳值是 any
,之後再真正使用 pickCard1 的時候就會依據不同的 parmeter 檢查不同的形態和 response

Typescript-Classes

Classes

傳統的 Javascript 使用 function 加上 protyotype-based 來繼承建立元件,但是這樣的機制對程式開發者習慣 Object-oriented 的感覺很尷尬, ECMAScript 2015ECMAScript6 中允許開發者使用 object-oriented class-based approach

Basic Classes

最基本的 class-based 範例

1
2
3
4
5
6
7
8
9
10
11
class Greeter{
greeting: string;
constructor(messaage: string){
this.greeting = message;
}
greet(){
return "Hello, " + this.greeting;
}
}

let greeter = new Greeter("world");

這樣的程式對 C# 或是 Java 的開發者應該會比較親切,

宣告了一個新的 class Greeter,這個 class 中有一個屬性是 greeting constructor 和 greet

看到有一個關鍵字 this. 之後可以呼叫這個 class 的屬性

繼承

TypeScript 中可以直接使用 object-oriented patterns,當然也可以建立一個 class 做繼承的動作,

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Animnal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}`);
}
}

class Snake extends Animnal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}

class Horse extends Animnal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}

let sam = new Snake("Sammy the Python");
let tom: Animnal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

上述範例中以 extends 這個關鍵字來建立一個子類, HorseSnake 是繼承在 class Animal 之下的子類

在子類中的 constructor 必須使用 super() 這將會執行父類的 constructor

這個範例也示範了如何覆寫父類的 FunctionSnakeHorse 都有建立一個 moveFunction 來覆寫過 Animalmove 執行結果後如下

1
2
3
4
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

Public, private 和 protected modifiers

public by default

在我們的範例中可以自由地宣告屬性,但在其他語言(C#) 需要使用 public 這個關鍵字來規範屬性是不是可以被瀏覽

但是在 TypeScriptpublic 是預設值

但是你也可以使用 public 來宣告屬性

1
2
3
4
5
6
7
8
9
class Animal{
public name: string;
public constructor(theName: string){
this.name = theName;
}
public move(distanceInMeters: number){
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

了解 private

當某個屬性使用 private 來宣告,他不能來宣告,他不能被直接呼叫

1
2
3
4
5
6
class Animal{
private name: string;
constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // Error: 'name' is private

TypeScript 是一個結構型態系統,我們比較兩種不同的類別,不論他們是如何產生的

只要他們的所有屬性沒有衝突,我們就可以稱這兩個類別是相容的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
constructor() { super("Rhino"); }
}

class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}


let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // Error: 'Animal' and 'Employee' are not compatible

然而當兩個類別在比較的時候如果擁有 privateprotected 屬性, 除非他們所這個 privateprotected 繼承的是同一個父類別才會是兼容的,否則在形態上兩個都會是不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Animal{
private name: string;
constructor(theName: string){
thie.name = theName;
}
}

class Rhino extends Animal{
constructor(){
super("Rhino");
}
}

class Employee{
private name: string;
constructor(theName: string){
this.name = theName;
}
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = Employee("Bob");

animal = rhino;
animal = employee; // Error: 'Animal' and 'Employee' are not compatible

在這個範例中我們有 AnimalRhino 兩個 class, RhinoAnimal 的子類別

另外也有一個 Employee 他看起來和 Animal 十分相似,都有一個 private name: string

因為 Rhino 是繼承 Animal 所以 Animal 實體化後可以 assign 給 Rhino 的實體並不衝突

代表他們是相容的,而 Employee 即使有一樣的 private name: string 但是卻無法相容,

因為他們並不是在同一個父類的類別

了解 protected

protectedprivate 很相似,只是當你宣告為 protected

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}

class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}

public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
// console.log(howard.name); // error

我們沒辦法直接呼叫 name 但是可以透過 Employee instance method 來使用,因為 Employee 繼承自 Person

我們也可以將 constructor 宣告為 protected 這代表這個 class 只能用來繼承,而無法直接產生 instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
protected name: string;
protected constructor(theName: string) {
this.name = theName;
}
}

// Employee can extend Person
class Employee extends Person {
private department: string;

constructor(name: string, department: string) {
super(name);
this.department = department;
}

public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}

let howard = new Employee("Howard", "Sales");
//let john = new Person("John"); // Error: The 'Person' constructor is protected

Readonly modifier

你可以宣告某些參數或變數是 readonly 使用 readonly 這個關鍵字來宣告,但是必須在初始化或是在 constructor 的時候進行宣告

1
2
3
4
5
6
7
8
9
10
class Octopus{
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theName: string){
this.name = theName;
}
}

let dad = new Octopus('Man with the 8 strong legs');
//dad.name = "Man with the 3-piece suit"; // error! name is readonly.

Accessors

TypeScript 支援 getters/setters 去對 Object 中的屬性進行取值或是修改

1
2
3
4
5
6
7
8
9
class Employee{
fulllName: string;
}

let employee = new Employee();
employee.fullName = 'Bob Smith';
if(employee.fullName){
console.log(employee.fullName);
}

我們希望使用者是有足夠的安全性,所以使用 private 宣告 fullName 然後允許使用 set 來對 fullName 來做修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let passcode = 'secret passcode';
class Employee{
private _fulllName: string;

get fullName(): string{
return this._fullName;
}

set fullName(newName: string){
if(passcode && passcode === 'secret passcode'){
this._fulllName = newName;
}else{
console.log("Error: Unauthorized update of employee!");
}
}
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}

command line

1
$ tsc -t ES5 ./Accessors.ts

有兩點需要注意

  • 因為必須要指定 ECMAScript 5 以上才可以使用 Accessors
  • 如果你只有設定 get 而沒有設定 set 代表這個屬性是 readonly

Static Properties

在這個部分我們討論的是實體的屬性,也是靜態屬性,實體的屬性,也是靜態屬性,這個屬性只能在 class 中取得,而無法被繼承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Grid {
static origin = { x: 0, y: 0 };
calculateDistanceFromOrigin(point: { x: number; y: number }) {
let xDist = point.x - Grid.origin.x;
let yDist = point.y - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor(public scale: number) {}
}

let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale

console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));

抽象類別

抽象類別就像之前的類別一樣,也許不需要實體化,使用 abstract 關鍵字來宣告抽象類別與抽象函式

1
2
3
4
5
6
abstract class Animal{
abstract makeSound(): void;
move(): void{
console.log("roaming the earth..");
}
}

abstract 中的函式並不會並不會包含在實體,也一定會使用 abstract 關鍵字來做宣告定義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
abstract class Department{
constructor(public name: string){
}
printName():void{
console.log("Department name: " + this.name);
}

abstract printMeeting():void;
}

class AccountingDepartment extends Department{
constructor(){
super("Accounting and Auditing");
}

printMeeting():void{
console.log("The Accounting Department meets each Monday at 10am.");
}

generateReports():void{
console.log("Generating accounting reports...");
}
}

let department: Department; // ok to create a reference to an abstract type
// department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
// department.generateReports(); // error: method doesn't exist on declared abstract type

抽象類別無法直接使用 new 產生物件,若是在抽象類別中並沒有宣告的類別與屬性,其子類別即使寫了也無法使用

Advanced Techniques

constructor function

TypeScript 中宣告一個 class 的時候,其實你已經同時執行了多個宣告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Greeter {
greeting: string;
constructor(message: string){
this.greeting= message;
}
greet(){
return "Hello, " + this.greeting;
}
}


let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

上述範例中當 let greeter: Greeter 我們將會使用 Greeter 類別的 instance 賦予 class Greeter

當我們使用 new 這個關鍵字來實體化的時候,便會執行 constructor 轉譯之後的結果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

let Greeter 就會準備指定給 constructor,而看到接下來的 new 關鍵字並且開始執行 constructor 就會取得一個藉由 Gretter 這個函式實體化的一個結果

在修改一下上面的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
} else {
return Greeter.standardGreeting;
}
}
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
console.log(greeter1.greet());
greeterMaker.standardGreeting = "Hey there!";

console.log(greeter2.greet());
console.log(greeter1.greet());

在這個範例中我們在 Greeter 宣告了一個靜態的屬性 standardGreeting並且給予值 Hello, there

第一步驟跟之前的範例一樣,利用 Greeter 產生了一個物件是 greeter1 然後將他的類別 assign 給 greeterMaker 並且修改了他的 standardGreetingHey there! 之後再由 greeterMaker 產生一個 greeter2 當它的 greet() 執行的時候產生的字串卻是 Hey there! 而且此時我再次執行 greeter1.greet() 的時候得到的卻也是 Hey there!

也就是當我們 可以利用 這樣的方式統一管理一個靜態屬性也會互相繼承靜態屬性

Typescript-Interfaces

Interfaces

簡介

TypeScript 有一種類型宣告方式,有時候這個模式叫做 duck typing 或是 structural subtyping, 或統稱為 interface

第一個 interface

最簡單的的 interface

1
2
3
4
5
6
function printLabel(labelledObj: {label: string}){
console.log(labelledObj.label);
}

let myObj = {size: 10, label: 'Size 10 Object'};
printLabel(myObj);

呼叫 printLabel 的時候會進行 type-check,而在 printLabel 中就有參數檢查, label 必須是 string, 實際上可能有更多的屬性,不只是 label, 檢查只會檢查 label 屬性是不是字串,有些狀況 TypeScript 並不寬鬆,之後會慢慢做解釋

依據上面的範例可以使用 interface 指定 label 為必要參數

1
2
3
4
5
6
7
8
9
10
interface LabelledValue{
label: string
}

function printLabel(labelledObj: LabelledValue){
console.log(labelledObj.label);
}

let myObj = {size: 10, label: 'Size 10 Object'};
printLabel(myObj);

LabelledValue 是我們可以描述參數必要性的範例,代表輸入值必須要有一個 label 變數型態為字串,我們並不需要非常明確的指定 printLabel 這個 Function 的輸入參數,只要符合這個 interface 就會允許使用

Optional Properties

也可以定義不一定會存在的參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Squareconfig{
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): {color: string, area: number}{
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}

let mySquare = createSquare({color: "black"});

唯讀

有些 properties 應該只能被修改,無法整個被覆寫

1
2
3
4
5
6
7
interface Point {
readonly x: number;
readonly y: number;
}

let p1: Point = {x: 10, y: 20};
p1.x = 5; // Error

也可以定義一個唯讀的陣列

1
2
3
4
5
6
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; //Error
ro.push(8); //Error
ro.length = 100; // Error
a = ro; //Error

最後一行中,當你定義為普通 ReadArray 要 assign 給一個 Array 是不允許的

readonly vs const

const 只是禁止你的物件被覆寫,而 readonly 則是設定你的物件中的參數被覆寫

Excess Property Checks

在第一個範例中,雖然我們寫了一個 interface{size: number, label: string} 但是我們真正有使用的只有 {label: string}, 我們在剛剛也有提到 optional properties 或是稱為 option bags

但是這兩個一起使用的話也有可能產生一些問題

1
2
3
4
5
6
7
8
9
10
interface SquareConfig{
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): {color: string, area: number}{
//...
}

let mySquare = createSquare({colour: "red", width: 100});

上述範例中 creteSquare 中的 colour 拼錯了,正確應該是 color, 並且 TypeScript 會顯示編譯錯誤,然而你可以辯解說因為 width 是正確的, color 並不存在,但是 colour 名稱的錯誤是微不足道的

1
2
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

這時候正規的實作方式可以是

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

我們將會討論 index signatures 但是在這裡可以說 SquareConfig 可以有任意數量的 properties 不論是不是 color 或是 width 他們並不在意

另外還有一種方法,你直接宣告一個 SquareOptions 物件來放入 createSquare 中也不會有錯誤出現

1
2
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

請記得上述的範例,不應該讓這些檢查類別變得更加的複雜,你應該要持續檢查這些類型,因為大多數的錯誤都會造成 bugs。如果你允許 在 createSquare 中使用 color 或是 colour 這兩個參數,你應該修改 squareConfig 來顯示這兩種使用情境

Function Types

interfaces 可以用來描述物件的輪廓,然而為了要可以描述物件的 properties 所以 interfaces 應該也是可以描述 Function types

interfaces 描述一個 function type 的時候只需要定義 parameter 列表和回傳值,每一個 parameter 都需要明確的定義名稱和類別

1
2
3
interface SearchFunc{
(source: string, subString: string): boolean;
}

只需要定義一次之後就可以拿這個 interface 來建立變數

1
2
3
4
5
6
7
8
9
interface SearchFunc{
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string){
let result = source.search(subString);
return result > -1;
}

在宣告 Function 的時候 parameter 的名字不一定要一樣

1
2
3
4
5
6
7
8
9
interface SearchFunc{
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean{
let result = src.search(sub);
return result > -1;
}

宣告也可以只宣告一次,,之後依據同類型宣告的 Function 也會依照之前宣告的 interface 做檢查,不避在重複定義。

1
2
3
4
5
6
7
8
9
interface SearchFunc{
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}

Indexable Types

基本上我們可以用 interface 來定義 Function 也可以來定義 index

1
2
3
4
5
6
7
8
interface StringArray{
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

在上方的範例中, StringArray 中有宣告一個 index type 為 number

基本上只有 兩種類型的 index, 就是 numberstring,也可以同時支援兩種類別,但是在支援兩種類別的時候若是為 100 則必須是回傳 '100',也就是兩種類別必須要統一

1
2
3
4
5
6
7
8
9
10
11
12
class Animal{
name: string;
}

class Dog extends Animal{
breed: string;
}

interface NotOkay{
[x: number]: Animal;
[x: string]: Dog;
}

string 是非常實用的宣告 index 方式,
因為 obj.property 也可以視為 obj['property']

這一個範例因為 name 的類別並不匹配,所以在檢查類別的時候會有錯誤

1
2
3
4
5
interface NumberDictionary{
[index: string]: number;
length: number; //ok, length is a number
name: string; //error name is not a subtype of the indexer
}

最後我們試著宣告一個唯讀的 interface

1
2
3
4
5
6
7
interface ReadonlyStringArray{
readonly [index: number]: string
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];

myArray[2] = "Mallory"; // Error

Class Type

實現一個 class 的 type

1
2
3
4
5
6
7
8
interface ClockInterface{
currentTime: Date;
}

class Clock implements ClockInterface{
currentTime: Date;
constructor(h: number, m: number){}
}

也可以描述在 class 中的 method, 例如在 Clock 中描述一個 setTime 的 method

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface{
currentTime: Date;
setTime(d: Date): void;
}

class Clock implements ClockInterface{
currentTime: Date;
setTime(d: Date){
this.currentTime = d;
}
constructor(h: number, m: number){}
}

Difference between the static and instance sides of classes

當我們要使用 interface 來宣告 class 的時候,要記得 class 有兩種類型,一種是 public 一種是 static 當你要宣告一個 class 的 constructor 的時候會有錯誤

1
2
3
4
5
6
7
8
interface ClockConstructor{
new (hour: number, minute: number);
}

class Clock implements ClockConstructor{
currentTime: Date;
constructor(h: number, m: number) { }
}

這是因為當一個 class 轉為 instance 的時候,只有 instance 這邊有做 typing-check 而再 static-side 並沒有包含這個檢查

所以在下面的這個範例,需要定義兩個 interfaceClockContructor 是為了 constructor 而 ClockInterface 是為了實體化後的物件定義,而會了方便我們定義 constructor 所以又建立一個 createClock 來做這件事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
interface ClockConstructor {
new (hour: number, minute: number);
}
interface ClockInterface {
tick();
}

function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}

class AnalogClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("tick tock");
}
}

let digital = createClock(DigitalClock, 12, 18);
let analog = createClock(AnalogClock, 7, 21);

因為 createClock 的第一個參數是 ClockConstructorcreateClock(AnalogClock, 7, 21) 中檢查 Analogclock 的 constructor 是否有正確的參數類型

就像 classes 一樣 interface 可以利用繼承將他們的屬性傳給自己的 Children

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
clolor: string;
}

interface Square extends Shape {
sideLength: number;
}

let square = <Square>{};
square.color = 'blue';
square.sideLength = 10;

也允許多重繼承,建立一個集合體

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

let s = <Square>{};
s.color = "blue";
s.sideLength = 10;
s.penWidth = 5.0;

Hybird Type

Javascript 常常會有很豐富的一個 多次繼承,也可以使用 Hybird Type 來做多個繼承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Conter{
(start: number): string;
interval: number;
reset():void;
}

function getCounter():Counter{
let counter = <Counter>function(start: number){};
counter.interval = 123;
counter.reset = function(){};
;return counter;
}

let c = getCounter();
c(10);
c.reset()
c.interval = 5.0;

Interfaces Extending Classes

當一個 interface 繼承了一個 class 只是繼承了他的屬性而不是他的實體只是繼承了他的屬性而不是他的實體,這就是說當你要實踐這個 interface 的同時也必需繼承同一個 class 來實現他的所有屬性

當你有一個很大的繼承架構,但是又想要自訂一個程式碼專為某一個 subclass 中的某些屬性 又不希望她繼承所有的父輩繼承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() { }
}

class TextBox extends Control {

}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
state = 1;
select() { }
}

class Location {

}

上述範例中 SelectableControl 包含了所有的 Control 的屬性,包含 private 的 state,這意味著之後要實現 SelectableControl 的同時只能 extends Control 一個類別去承接他的 private 的 state

Control 之中允許透過 SelectableControl 來取得 private state,而 SelectableControl 就像是 Control 知道他還會有一個 function selectButtonTextBoxSelectableContorl 的子類,因為他們都是繼承魚 Control 但是 ImageLocation 則不是

TypeScript-VariableDecarations

Variable Decarations

letconst 是兩種 Javascript 新的宣告方式, letvar 比較類似

const 則是定義之後禁止之後修改(常數)

TypeScript 也有提供 letconst 的宣告方式,下個部分將會解釋為什麼會推薦使用 letconst

var 宣告

在 Javascript 中宣告一個變數常用的方式是

1
var a = 10;

在上面的例子之中,你宣告了一個變數 a10

你也可以再 Function 中宣告

1
2
3
4
function f(){
var message = 'Hello, World!';
return message
}

也可以允許相同的變數在不同的 Function scope

1
2
3
4
5
6
7
function f(){
var a = 10;
return function g(){
var b = a + 1;
return b;
}
}

在上方的範例中 g() 中可以取得變數 a 得值

1
2
3
4
5
6
7
8
9
10
11
12
function f(){
var a = 1;
a = 2;
var b = g();
b = 3;
return b;
function g(){
return a;
}
}

f(); // result 2

使用 var 宣告會有一些區域的規則問題

1
2
3
4
5
6
7
8
9
function f(shouldInitialize: boolean){
if(shouldInitialize){
var x = 10;
}
return x;
}

f(true); // 10
f(false); //undefined

因為 var 是在 if裡面,所以當 shouldInitialize 是 false 的話就部會執行 if裡面的程式碼

所以 x 並未宣告過,就會造成 undefined

這個規則可能會造成壹些不同種類型的錯誤,其中一種就是當你重複宣告同樣名稱的變數的時候彼此會互相覆蓋

1
2
3
4
5
6
7
8
9
10
11
function sumMatrix(matrix: number[][]){
var sum = 0;
for(var i = 0; i < currentRow.length; i++){
var currentRow = matrix[i];
for(var i =0;i < currentRow.length; i++>){
sum += currentRow[i];
}
}

return sum;
}

上述範例就可以發現因為 i 變數在雙迴圈中會被互相覆蓋造成程式執行上的錯誤,不會依據我們預想的去執行

奇怪的問題

1
2
3
for(var i=0;i<10;i++){
setTimeout(function(){console.log(i)}, 100*i);
}

但是結過卻是

1
2
3
4
5
6
7
8
9
10
10
10
10
10
10
10
10
10
10
10

但是我們希望的是

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

因為每次呼叫 setTimeout 會延遲一段時間後才開始執行 Function但是迴圈會不斷覆蓋掉 i 這個變數,而在延遲時間之後呼叫到的 i 則是最後覆蓋成 10 的 i

最常見要解決這件事情的方式如下

1
2
3
4
5
for(var =0; i< 10; i++){
(function(i){
setTimeout(function(i){console.log(i);}, 100*i);
})(i)
}

這個看起來有點奇怪的解決方式在 javascript 中卻是常見解決這個問題的方式

let

現在你已經知道 var 會有一些問題,所以會有一些問題,所以為什麼需要介紹 let.letvar 的使用方式依樣

1
let hello = 'hello';

Block-scoping

使用 let 宣告的時候,它的作用域市 blocking-scope。和 var 宣告的作用域不一樣,他是用大括號來做區隔

1
2
3
4
5
6
7
8
function f(input: boolean){
let a = 100;
if(input){
let b = a + 1;
return b;
}
return b; // b 並不存在
}

上述範例中有 ab 兩個變數, a 的變數範圍在整個 f() Function 之中,而 b 只會存在 if之中

而變數使用 try catch 宣告的範例如下

1
2
3
4
5
6
7
try{
throw 'oh no!';
}catch(e){
console.log('oh well');
}

console.log(e);// e not found

另外一個很重要的 blocking-scope 變數不能在宣告之前做任何動作

1
2
a++;
let a;

TypeScript 中對這樣的提前宣告較為寬鬆,你需要使用 try catch 來取得錯誤訊息

若是沒有使用 try catch TypeScrtip 並不會顯示這個訊息,若是在 ES2015 則會顯示這個錯誤訊息

1
2
3
4
5
6
7
function foo(){
return a;
}

foo(); //會丟出一個錯誤訊息

let a;

重複宣告和 shadowing

若是使用 var 的方式來宣告的話,他不會在意你宣告過幾次

1
2
3
4
5
6
7
function f(x){
var x;
var x;
if(true){
var x;
}
}

使用 let 宣告在同一個 scope 中只能宣告一次

1
2
let x = 10;
let x = 20; // Error
1
2
3
4
5
6
7
8
function f(x){
let x = 100;// Error
}

function g(){
let x = 100;
let x = 120;// Error
}

只要是在不同的 blocking-scope 就可以做同名的宣告

1
2
3
4
5
6
7
8
9
10
11
function (condition, x){
if(condition){
let x = 100;
return x;
}
return x;
}


f(false, 0); // returns '0'
f(true, 0); // returns '100'

宣告一個新的名稱在另外一個內嵌的 block-scoping 這個行為叫做 shadowing,但是這樣的行為會造成一些 bugs

例如:

1
2
3
4
5
6
7
8
9
10
11
function sumMatrix(matrix: number[][]){
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}

return sum;
}

shadowing 在攥寫程式碼的時候應該要避免的狀況之一

Block-scoped variable capturing

當我們在一個作用域中宣告一個變數與 Function ,而 Function 也是其中一個作用域,在這個 Function 使用已宣告的變數的時候,即使脫離了那個作用域,也是依舊可以使用該變數

1
2
3
4
5
6
7
8
9
10
11
function theCityThatAlwaysSleeps(){
let getCity;
if(true){
let city = 'Seattle';
getCity = function (){
return city;
}
}

return getCity(); // Seattle
}

因為 city 雖然是在 if 的作用域宣告的,但是可以透過 Function 記住他的指標即使脫離作用域之後也可以透過該 Function 做呼叫使用

回憶之前 setTimeout 的範例, let 有相當大程度的不同

1
2
3
for(let i=0; i < 10; i++>){
setTimeout(function(){console.log(i)}, i * 100);
}

結果為

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

const

const 是另外一種不同的宣告

1
const numLivesForCat = 9;

雖然看起來跟 let 宣告一樣,雖然他們有相同的 block-scoping 規則,但是還是有些不同

const 宣告的變數是 immutable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat
};

kitty = {
name: 'Danielle',
numLives: numLivesForCat
}; // Error

// All OK
kitty.name = 'Rory';
kitty.name = 'Kitty';
kitty.name = 'Cat';
kitty.numLives--;

除非你要整個複寫整個物件,否則還是可以修改參數值得,

也就是此物件性質為 唯讀 的,詳情參閱

let vs. const

為什麼需要兩個不同的語意卻擁有相同的 block-scoping 的宣告方式呢?

基於 最小權限原則
若之後變數都不需要修改或是物件僅僅提供修改參數的權限時,則使用 const,換句話說若是變數之後有可能會被覆寫則使用 let 來宣告

Destructuring

ES2015 的特性在 Typescript 中依舊可以使用

Array destructuring

1
2
3
4
5
let input = [1, 2];
let [first, secode] = input;

console.log(first); // 1
console.log(secode); // 2

這個解構也可以在 Function 中使用

1
2
3
4
5
6
function f([first, second]: [number, number]){
console.log(first);
console.log(second;
}

f([1, 2]);

也可以將大量的變數指定給某一個變數

1
2
3
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [2, 3, 4]

當然你也可抵應某些參數

1
2
let [first] = [1, 2, 3, 4];
console.log(first); //1

也可以單存指定某些特定的參數

1
let [, second, , fourth ] = [1, 2, 3, 4];

Object destructuring

你也可以解構 object

1
2
3
4
5
6
7
let o = {
a: "foo",
b: 12,
c: "bar"
}

let = {a, b} = o;

你可以單純指定 a, bc 可以因為不使用而跳過

你也可以使用 ... 這個形態來指定變數

1
2
let {a, ...passthrogh} = o;
let total = passthrough.b + passthrogh.c.length;

property renaming

你可以對變數重新命名

1
let {a: newName1, b: newName2} = o;

也可以將 a:newName1 改為 a as newName1 也是一樣的效果

TypeScript 也是需要宣告類型

1
let {a, b}: {a: string, b: number} = o;

Default vaules

在你解構的時候也可以提供預設值

1
2
3
function keepWholeObject({a: string, b?: number}){
let {a, b = 1001} = wholeObject;
}

keepWholeObject 中的 b 若是有參數則指定該參數,若是 undefined 或是 null 則指定為預設值 1001

宣告 Function

解構依舊可以使用在宣告函式之中

1
2
3
4
type C = {a: string, b?:number};
function({a, b}: C){
// ...
}

也可以解構時預先放入預設值

1
2
3
4
5
function f({a, b} = {a: "", b: 0}): void{
//...
}

f(); // ok, default to {a: "", b: 0}

參數與設定型態都可以給予預設值,但是這兩個又有什麼不同呢?

1
2
3
4
5
6
function({a, b=0}: {a: ""}): void{
//...
}
f({a: 'yes'}); // ok default b = 0
f(); // default to a = {a: ""}, default b = 0;
f({}); // error 'a' is required

Spread

ES2015 中的 Spread 特性也是支援的

1
2
3
4
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
console.log(bothPlus); // [0, 1, 2, 3, 4, 5]

Spread 也可以使用在 object

1
2
3
let defaults = {food: 'spicy', price: '$$', ambiance: 'noisy'};
let search = {...defaults, food: 'rich'};
console.log(search); // {food: 'rich', price: '$$', ambiance: 'noisy'}

在上述範例中 search 會解構 defaults 而且後面的 food 因為和 defaults 中重複而且順序在 defaults 的後面,所以會被覆蓋

1
2
3
let defaults = {food: 'spicy', price: '$$', ambiance: 'noisy'};
let search = {food: 'rich', ...defaults};
console.log(search); // {food: 'spicy', price: '$$', ambiance: 'noisy'}

但是 Spread 只會繼承特性,部會繼承 Function

1
2
3
4
5
6
7
8
9
10
11
12
13
class C{
p = 12;
m(){
//...
}
}

let c = new C();

let clone = {...c};

console.log(clone.p); // 12
console.log(clone.m); // undefined

另外 TypeScript 編譯過程並不允許 generator functionSpread 參數傳遞

Leetcode-Q8

Remove Duplicates from Sorted List

題目

Given a sorted linked list, delete all duplicates such that each element appear only once.+

For example,
Given 1->1->2, return 1->2.
Given 1->1->2->3->3, return 1->2->3.

改一個排序過的連結陣列,刪除重複的節點。
範例:
[1,1,2] -> return [1,2]
[1,1,2,3,3] -> return [1,2,3]

NodeList

NodeList

Nodelist 並不是陣列,主要的區別在於 array 有 push 和 pop, 但是 NodeList 並沒有

最簡單的範例就自愛瀏覽器使用 document.querySelectorAll('class') 回傳值就是 NodeList

想法

因為輸入值已經是排序過後的 NodeList,所以只需要檢查室不是下一個跟這一個事不是相等

若是相等就略過

Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var deleteDuplicates = function(head) {
if(head === null || head.next === null){
return head;
}
let temp = head;
while(temp.next !== null){
if(temp.val === temp.next.val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return head;
};

Typescript-BasicType

翻譯來源

Basic Type

Introduction

為了要讓更清楚的使用 TypeScript 會從最基本的元件 numbers, strings, structures, boolean, values 這類的動作, TypeScript 支援宣告這類型的 types

Boolean

宣告最基本的 true/false 在 TypeScript 中稱為 boolean

1
let isDone: boolean = false;

Number

和 Javascript 一樣,在 TypeScript 中預設的是浮點數,而這個類別統一為 number 也支援十六進制,十進制,八進制,以及 binary

1
2
3
4
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

String

另外一種工程師常用的類型是字串,這種形態使用 string 來做宣告,在 TypeScript 中都允許使用單引號(‘) 或是 雙引號(“) 來做字串的宣告

1
2
let color: string = "blue";
color = 'red';

你也可以使用 template strings,宣告使用 backtick/backquote(`)

1
2
3
4
5
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName};

I'll be ${age + 1} years old next month`;

這個 template strings 也可以寫成

1
2
let sentence: string = "Hello, my name is " + fullName + ".\n\n" +
"I'll be " + (age + 1) + " years old next month.";

Array

TypeString 和 Javascirpt依樣允許你使用 Array, 宣告時使用 [] 來做類型宣告

1
2
let list: number[] = [1,2,3];
let list2: Array<number> = [1,2,3];

Tuple

Tuple 允許你宣告每一個參數的類型

1
2
3
4
let x : [string, number];

x = ['hello', 10]; //OK
x = [10, 'hello']; //Error

好處就是當你明確的定義類型的時候,使用的時候就會有明確的錯誤訊息

1
2
3
4
5
6
7
8
9
10
let x : [string, number];

x = ['hello', 10];
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
x[3] = "world"; // OK, 'string' can be assigned to 'string | number'

console.log(x[5].toString()); // OK, 'string' and 'number' both have 'toString'

x[6] = true; // Error, 'boolean' isn't 'string | number'

Enum

另外一種特別的型別事 enum 你可以定義哪些允許的類型列表

1
2
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

你也可以設定 enum 得值

1
2
3
4
5
6
7
8
9
enum Color {
Red = 1,
Green,
Blue
}
let colorName: string = Color[2];

console.log(colorName);
// Green

也可以直接定義 key值

1
2
3
4
5
6
7
8
9
enum Color {
Red = 1,
Green = 2,
Blue = 4
}
let colorName: string = Color[4];

console.log(colorName);
// Blue

Any

我們要描述一個變數,但是在攥寫程式的當下不知道他的類型,這個值是屬於動態的內容,所以可以利用 any 來做宣告

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;

any 類型相當的彈性,你也許會期望 object 也會有一樣的情況,但是 object 並不允許你呼叫任意的函式,即使他真的存在

1
2
3
4
5
6
let notSuer: any = 4;
notSure.ifItExists();
notSure.toFixed();

let prettySure: object = 4;
prettySure.toFixed(); //Error: 'toFixed' not exist on type 'Object'

Void

void 有一點像 any , 他並沒有任何類型,你可以在沒有回傳的函式中使用它

1
2
3
function warnUser():void{
alert('This is my warning message');
}

當你宣告變數為 void 的時候,你只能 assign undefined 或是 null

1
let unsable: void = undefined;

Null 和 Undefined

TypeScriptundefinednull 有他的類型就叫做 undefinednull

就像 void ,他們沒瞎小路用

1
2
let u: undefined = undefined;
let n: null = null;

預設 nullundefined 是各種類型的亞類型,也就是你可以 assign null 或是 undefinednumber 類型的變數

然而若是你在 tsconfig.json 中的選項 strictNullChecks 設定為 true 則 nullundefined 只能 assign 給 void 和他們的各自類型。

若是一個變數有多重可能,你也可以利用 union 類型

1
let notSure: string | null | undefined;

Never

never 類型代表永遠不會發生,例如你的函式確認永遠都不會回傳任何值,那麼你就可以利用 never 類型

或是某一個變數永遠都是 false 你也可以設定他為 never

1
2
3
4
5
6
7
8
9
10
11
12
13
 function error(message: string): never{
throw new Error(message);
}

function fail(){
return error("Something failed");
}

// Function returning never must have unreachable end point
function infiniteLoop(): never {
while (true) {
}
}
|