Typescript

详见官网文档,这里我们摘一部分

不需要掌握的部分

官网中的内容比较多,有关类部分的内容可以不学习,初步会声明并调用函数即可。

类型/Type

常用基础类型

其它类型

变量

TypeScript的数据类型包括以下几类:

字符串

三种写法,使用单引号(')、双引号(")或是反撇号(`)包括的部分就是字符串,以下均为合法字符串:

const str1 = 'mm';
const str2 = "mm";
const str3 = `mm`;

如果字符串内部包含有单引号,双引号,或是反撇号怎么办?需要使用转义符\:

const str1 = 'Tom said: "I\'m tom".';
const str2 = "Tom said: \"I\'m tom\".";
const str3 = `Tom said: "I'm tom".`;
const str4 = `const str3 = \`Tom said: "I'm tom".\`;`;

在单引号中有双引号或是双引号中有单引号时,转义符可要可不要。

反撇号还有一种用法,就是可以方便进行字符串合并的,写出来的代码既简洁又易懂:

const name = 'Tom';
const message = `${name} said: "I'm ${name}."`;

等价于:

const name = 'Tom';
const message = name + ' said: "I\'m "' + name + '.';

约定在进行实际开发中,使用约定的一种代码书写方法,使得代码可维护性更高。以下为我个人推荐的字符串的约定规范。

  1. 如果有字符串拼接,使用反撇号:

    const name = 'tom';
    const age = 3;
    const message = `I'm ${name} and I'm ${age} years old.`;
    
  2. 如果字符串中有单引号或双引号,使用反撇号,如上面的例子中str4.

  3. 简单字符串,使用单引号,如上面例子中name。之所以这样写,是因为一般来说,单引号在键盘上敲击的效率要高于双引号和反撇号。

数字

数字类型也是比较常见的一种类型,在业务系统中,几乎所有的统计都基于数字

const num1 = -123;
const num2 = 123.456;
const num3 = -123.456;
const num4 = 123e4;
const num5 = 123456789012345678901234567890123456789012345678901234567890n;

数字分为3类:整数、小数、科学计数、长整数(bigint)。以上几种类型中,整数和小数经常被用到,但是科学计数法和长整数不经常使用。 科学计数法在开发过程中被使用的可能性几乎为0,只需要知道有这么一种类型即可。例如,如果一个数值为123456789012345678901234567890,那么它展示出来的结果就会是1.2345678901234568e+29,这就是一种科学计数法的表示。 长整数使用的场景是一个数字非常大。已经超出整数可表达的范围了。我们举一个非常有意思的示例。

const n = 123456789012345678901234567890123456789012345678901234567890;
n+10>n

这个式子的结果为false,而

const n = 123456789012345678901234567890123456789012345678901234567890n;
n+10n>n

的结果就是true。这个问题要解释起来有点儿关联的东西有些多,为了不把问题搞得过于复杂,对于初学者,我们知道有这么一个有意思的示例就足够了。

NaN

这是个数值,它表示“不能表达的数字”(not a number)。看下面这个式子:

0/NaN   // NaN
NaN/0   // NaN
NaN === NaN // false

是不是很有意思?

数字的转换

有时候我们会需要进行整数、小数、以及字符串间的转换。比如说我们拿到一个数字,为了展示给用户,我们就需要把它转换为一个字符串来表示。而对于用户输入的一些文本,我们又需要把这些数字的文本转换为真正的数字来存储起来。

空值

与其它开发语言相比较,TypeScript/JavaScript中空值的表示方法有些特别,有两种:

  1. null
  2. undefined

它们多数时候可以看作是相同的,但有时候确实又是不同的(比如使用typeof时)。undefined字面意思就是“变量的值没有定义”,null明确是“变量的值被定义为空值”。虽然在JavaScript中,这两种不同的类型确实有它一定的用处,但它们给开发人员带来的困惑的代价较高,个人建议在TypeScript中尽量不要使用undefined,而使用null。

对象

Object对象类型与Java中的Object类型可说是非常相似,我们常用的方式就是直接使用花括号将一些键值对括起来。

const o = {
    str: 'mm',
    num: 123,
    b: true,
    obj: {
        foo: 'bar'
    },
    arr: [1,2,3]
};

这有点像Java中的Map或是C#中的Dictionary亦或是stl的map,但又不全是,因为TypeScript/JavaScript中有Map类型,下面会讲到。实际应用中,对象类型往往会存储一组与数据库表相同(或相似)的数据。

获取对象的键

const obj = {
    foo: 'bar'
};
Object.keys(obj);   // ["foo"]

合并两个(多个)对象

const obj1 = {
    str: 'mm'
};
const obj2 = {
    num: 123
};
const obj3 = {
    b: true
};
const obj = {
    ...obj1,
    ...obj2,
    ...obj3,
    obj:{
        foo: 'bar'
    }
};  // obj 合并了obj1,obj2,obj3。

有人会说可能还有其它方法来完成这个事情,但我个人推荐的编程的思想是:如果达到一个目的有多种方法,我们选用并固定使用其中一种,抛弃其它几种。相信我,这对后期项目的维护是非常有好处的。在很多时候,我们宁愿统一、简化编码的方式,而不愿让我们组内的开发人员去学习更多的所谓的技巧。因为在一些开发语言中,实际上存在着过度设计和由历史原因造成的多余的东西,我们的开发人员把精力浪费到纯粹的“开发”中而不是实际的解决实际项目问题上时,会造成人力资源的无谓浪费。我认为开发人员应该把精力用在真正的解决问题上,而不是为了编程而编程。你同意我的观点吗?

对象中键中含有特殊字符

加上引号

const obj = {
    'str-val': 'str',
    'num-val': 123
};

数组

实际上,数组并不能算作一种数据类型,它只是对象类型的一种扩展或子集。但在实际应用当中,我们经常会需要把它跟对象类型区分开,所以我们这里把它当作一种独立类型来说明。

const arr = [1,2,3];

一般地,不建议在一个数组中存放多种类型的数据,比如:

const arr = ['str', 123, true];

这种写法之所以不推荐,是因为这种类型的数据在维护时的可读性较差,特别是数组长度很长的时候。

布尔

表示“真”和“假”

const b1 = true;
const b2 = false;

这个类型通常在使用过程中会进行隐式的转换,所以我们需要知道别的类型转换为布尔类型时结果是什么

// undefined
Boolean();  // false
// null
Boolean(null);  // false
// 数字0
Boolean(0); // false
// 空字符串
Boolean('');    // false

// 非空字符串
Boolean('0');   // true
// 非0数字
Boolean(1); // true
// 对象
Boolean({});    // true
// 数组
Boolean([]);    // true

日期Date

日期类型也是一种常用类型

const dt = new Date();

由于在前面JSON中介绍的原因,在实际开发中遇到这种类型,因为它往往会是数字和字符串的转换,所以要考虑借助第三方库来完成这类操作(日期类型转换字符串牵涉到国际化的知识,自行简单实现弊端还是挺多的)。

Map

Map类型与对象类型在大部分时候比较像,除了两点:1.对象的键名必须是字符串,而Map没有这个限制。2.对象的键没有顺序

以下为Map常用的几个方法:

const map = new Map<string, number>();

map.set('key1', 123);
map.set('key2', 456);

map.has('key1');    // true
map.has('key3');    // false

map.get('key1');    // 123
map.get('key3');    // undefined

map.delete('key1');
map.has('key1');    // false

Set

与Map相似,这里不赘述。就当留为作业吧。

Function

这个类型虽然实际应用很多(后面会有单独的章节来讲函数),但它单独作为类型来讲也很少用。一般用来动态地根据字符串创建函数。用这种方法制作一个“计算器”,想必会比静态语言要方便地多。

Symbol

这个类型也很少用,这里提一提,有兴趣的人可以自行了解,没兴趣的人直接跳过去往下看也没有问题。

interface接口

这其实是面向对象语言中的概念。我觉得对于没有或尚未形成面向对象思想的人来讲,我们不妨把接口理解为一种可变的,用户可自定义的对象的类型。比如我们有一个人的信息如下:

const person = {
    name: '王二小',
    first_name: '二小',
    last_name: '王',
    age: 80,
    sex: 'male',
    job: 'shephert'
};

我们要给这个数据定义一种类型(比如当你需要把这个数据当作参数传给某个函数)的话,怎么定义呢?

interface Person {
    name: string;
    first_name: string;
    last_name: string;
    age: number;
    sex: 'male' | 'female',
    job: string;
}

Type

有点儿像C++中的typedef,不常用。上面接口的例子,也可以这样定义:

type Sex = 'male'| 'female';

interface Person {
    name: string;
    first_name: string;
    last_name: string;
    age: number;
    sex: Sex,
    job: string;
}

Enum枚举

上面接口的例子,也可以这样定义

enum Sex {
    male = 'male',
    female = 'female'
}

interface Person {
    name: string;
    first_name: string;
    last_name: string;
    age: number;
    sex: Sex,
    job: string;
}

枚举类型不仅可以是数字,也可以是字符串。

思考以上三种Person的定义的写法中,哪一种方法较好?如果你有足够的理由,我建议你在以后的编码过程中和团队中的小伙伴们约定一种固定的写法吧。

运算符

计算机编程语言中,不只是“加,减,乘,除”这类数学运算是运算符,有些看起来不像是“运算”的符号其实也是运算符。我们在这里大概介绍一下:

算术运算符

// +,-,*,/,%,++,--
(1 + 2 - 3) * 4 / 5
5 % 6   // 取余(模)
let n = 0;
1 + ++n;    // 1 + 1 = 2 n=1
2 + n++;    // 2 + 1 = 3 n=2
3 + --n;    // 3 + 1 = 4 n=1
4 + n--;    // 4 + 1 = 5 n=0

这里需要提及的是:括号也是运算符,并且经过运算,表达式也有值的.下面的示例请大家新手试一下,并想一想这个现象是为什么?

1.toString();   // 出错了
(1).toString(); // 没出错,输出"1"

关系运算符

1 === 1;    // true
1 == 1;     // true

1 == '1';   // true
1 === '1';  // false

null == undefined;  // true
null === undefined; // false

1 != 1; // false
1 !== 1;    // false

1 != '1';   // false;
1 !== '1';  // true;

null != undefined;  // false
null !== undefined; // true

1 > 2;  // false
1 < 2;  // true

1 >= 1; // true
1 <= 1; // true
1 >= 2; // false
1 <= 2; // true

逻辑运算符

true && true;   // true
true && false;  // false
false && true;  // false
false && false; // false

true || true;   // true
true || false;  // true
false || true;  // true
false || false; // false

!true;  // false
!false; // true

位运算符

为了方便说明位运算符的作用,我这里直接使用二进制书写数值。如下例中0b010其实就相当于二制的2

0b000 & 0b111;  // 0b000
0b001 & 0b001;  // 0b001
0b001 & 0b110;  // 0b000
0b101 & 0b110;  // 0b100
0b111 & 0b111;  // 0b111

0b000 | 0b111;  // 0b111
0b001 | 0b001;  // 0b001
0b001 | 0b110;  // 0b111
0b101 | 0b110;  // 0b111
0b111 | 0b111;  // 0b111

0b001^0b111;    // 0b110;
0b001^0b000;    // 0b001;
0b111^0b111;    // 0b000;
0b111^0b000;    // 0b111;
0b000^0b111;    // 0b111;
0b000^0b000;    // 0b000;

// 因取反操作会改变最高位(表示正0负1),负数存在补码的问题,所以实际值跟期望值可能会有出入。不太好理解,这部分对于大部分开发人员可以跳过去。
~0b000; // 类似于0b111 实际值 -0b000 - 1
~0b101; // 类似于0b010 实际值 -0b101 - 1
~0b110; // 类似于0b001 实际值 -0b110 - 1

0b001 >> 1; // 0b000
0b001 >>> 1;    // 0b000
-0b001 >> 1;    // -0b001
-0b001 >>> 1;   // 0b1111111111111111111111111111111
0b001 << 1; // 0b010
-0b001 << 1;    // -0b010

赋值运算符

const n1 = 1;
let n2 = 2;
n2 += 1;    // 3
n2 -= 1;    // 2
n2 *= 2;    // 4
n2 /=2;     // 2

需要说明的是,赋值运算符所组成的表达式是有值的,它的值为赋值运算结束后被赋值变量的值。

三元运算符 (?)

const n = 1000;
let s = 0;
for (let i = 0; i < n; ++i) {
    s += i % 2 ? i : 0;
}

以上代码的结果n求的是什么值?对算法有兴趣的各位老师可以设计一个更高效的算法来求这个值。

类型运算符

typeof 2;   // 'number'
typeof 'mm';    // 'string'
typeof false;   // 'boolean'
typeof {};  // 'object'
typeof null;    // 'object'
typeof undefined;   // 'undefined'
typeof [];  // 'object'

注意,比较null,[]的类型比较奇特,null在前面章节讲到了一点,这里注意一下它和undefined的区别就好,重点要说明的是[],有些人可能会期望它会是array之类的,但事实上它不会,因为从原理上来讲,数组就是一个特殊的对象。那么我们如果要判定一个对象是不是数组要怎么做呢?答案是使用Array.isArray()这个静态方法:

Array.isArray([]);  // true
Array.isArray({});  // false

其它运算符

字符串运算符

const name = 'Tom';
const age = 3;
const message1 = `${name} said: "I'm ${name}."`;
const message2 = name + ' said: "I\'m "' + name + '.';
const message3 = `I'm ${name} and I'm ${age} years old.`;

正号运算符

const n1 = '3';
const n2 = +n1;

负号运算符

const n1 = '3';
const n2 = -n1;
const n3 = 4;
const n4 = -n3;

逗号运算符

const i = 1, j = 2;

像赋值运算符一样,逗号运算符也是有值的,它的值为最右端表达式的值。另外,初始化变量不推荐以上写法。

表达式

运算符+常量/变量+行末分号就组成了表达式。上面运算符的例子都是表达式。

关键字

像前面出现的let,const,for,typeinterfaceenum等,以及没有提到的instanceofif,return都是关键字,关键字不能作为变量名来命名变量。

变量作用域

变量作用域是指一个变量在代码中的“可见范围”。这是个相似简单但非常容易出问题的问题。我们的在业务代码中,应该秉承“永远不要使用全局变量”的原则。

变量声明

为了避免变量给我们在开发过程中可能会带来的副作用,建议在声明变量的时候尽量避免使用varlet关键字,而是使用const。for语句中的循环变量除外,事实上,在实际业务系统中,数组循环基本上都可以使用数组的forEach方法(只有一种情况除外,就是当需要数组中的元素按数组顺序异步执行时)。推荐尽量使用const的原因是:使用var,let时,我们不知道变量在什么时候被修改了,尤其是当某块代码非常复杂时,这会给代码的维护带来很大的困扰,使用常量就不会存在这问题(引用类型变量仍然存在这种问题,这就需要我们在使用中注意,尽量不要去修改引用类型变量的内部元素的值)。有人会问:我们怎么避免变量的修改呢?一种简单的思路就是当需要修改它时重新创建一个常量副本。它固然会带来运行内存的升高,但在多数时候它带来的便利要好过于变量的不知处的修改带来程序维护查找追踪问题所花的代价。

为了说明这问题,我们举个例子:

const n = 1000;
let s = 0;
for (let i = 0; i < n; ++i) {
    s += i % 2 ? i : 0;
}

如果我们要把s声明为const常量要怎么做呢?

const n = 1000;
const s = (() => {
    let s1 = 0;
    for (let i = 0; i < n; ++i) {
        s1 += i % 2 ? i : 0;
    }
    return s1;
})();

有人会说我们并没有消除let,只是把它放进了一个自执行函数(参见函数部分章节)而已。但是对于使用变量s处,我们的确是不担心它会被修改了,不是吗?

进一步,我们如果要完全不使用let要怎么做呢?看以下代码:

const n = 1000;
const s = ((n) => {
    return new Array(n).fill(0).reduce((pre, cur, i)=>{
        return pre + (i % 2 ? i : 0);
    }, 0);
})(n);

这时,我们会发现我们添加的自执行函数也不是必要的了,所以最终的代码就变为:

const n = 1000;
const s =  new Array(n).fill(0).reduce((pre, cur, i)=>{
    return pre + (i % 2 ? i : 0);
}, 0);

但是这么修改完之后,这段代码似乎没有之前易懂了,不是吗?所以大家在实际使用过程中如果要坚定不使用let的话,像这样的情况,在这里加上一句注释会比较好一些。否则的话,我建议像这种情形,for语句中的循环变量就不要固执使用const了。当然,在实际业务系统中,像以上的例子是非常少见的,它经常只出现在教程中,所以我们不必过于担心。

循环

多数时候建议使用数组的forEach等方法替代

let sum = 0;
for (let i = 0; i < 100; ++i) {
    sum += i;
}

此外,还有while,do whilefor in,foreach等循环的方法,这里不再过多介绍。

控制

ifif elseif elseif else

const arr = [1, 2, 3, 4, 5];
let sum = 0;
for (let i = 0; i < arr.length; ++i){
    const it = arr[i];
    if(it % 2){
        sum += it;
    }
}

以上代码的作用同于下面这段代码,有人能说出其间的差别并说明其优缺点吗?

const arr = [1, 2, 3, 4, 5];
const sum = arr.filter((it)=>{
    return it % 2;
}).reduce((pre, cur)=>{
    return pre + cur;
}, 0);

switch

let sum1 = 0;
let sum2 = 0;
for (let i = 0; i < 100; ++i){
    const it = arr[i];
    switch(it % 2) {
        case 0:
            sum1 += it;
            break;
        case 1:
            sum2 += it;
            break;
        default:
            break;
    }
}