- JavaScript 学习笔记
参考教程:
- 小满TypeScript基础教程全集(完结)
- 20分钟学会TypeScript 无废话速成TS 学不会你来评论区
0 环境配置
安装node(MacOS)和TypeScript(TS)
brew install node
npm install -g typescript
在VSCode中安装插件JavaScript and TypeScript Nightly
,提供基础的代码纠错和提示。
TS代码并不能直接在浏览器中执行,它需要先编译(转化)为JS代码才可以运行。可以使用tsc
命令将TS代码编译成JS代码,运行后会在同目录下生成编译得到的同名JS文件:
tsc hello.ts
假设编辑了一个hello.ts
文件,使用下面的命令即可运行代码(可以不执行转化直接运行,node会自动完成这一步骤):
node hello.ts # 或 node hello.js
生成一个同目录下的TS配置文件tsconfig.json
:
tsc --init
1 基础变量
1.1 变量定义
TS 常见类型包括 number
、string
、boolean
、array
、tuple
、enum
、null
、undefined
等,此外也支持自定义类型。
null
类型的变量只能赋值null
,undefined
类型变量只能赋值undefined
。TypeScript支持类型推断,但是大多数时候,为了体现TypeScript的高级特性,我们可以使用类型注解使得代码更“安全”:
let s1: string; // changeable variable
const s2: string = "Hello, TypeScript!"; // constant variable
s1 = "Hello, World";
let s3 = `${s1}, TypeScript!`; // template string, ES6 feature
console.log(s1);
console.log(s2);
console.log(s3);
TS 支持类型联合,限定某个变量的类型范围或者取值范围:
let v1: string | number;
v1 = "hello";
v1 = 42;
let color: "red" | "green" | "blue";
color = "red";
color = "green";
color = "blue";
// color = "yellow"; // Error: Type '"yellow"' is not assignable to type '"red" | "green" | "blue"'.
定义数组(TS中的数组只能是单一类型的集合):
let a1: number[] = [1, 2, 3];
let a2: Array<number> = [1, 2, 3];
let a3: (number | string)[] = [1, '2', 3];
let a4: Array<number | string> = [1, '2', 3];
let a5: readonly number[] = [1, 2, 3];
let a6: ReadonlyArray<number> = [1, 2, 3];
const
声明的数组依然可以使用pop()
、push()
等方法修改数组,数组和对象仅仅不能被重新赋值。如果希望整个数组都不能被修改,需要使用readonly
关键字注明。(const
的官方名称就是只读类型)const a1 : number[] = [1, 2, 3];
const a2 : readonly number[] = [1, 2, 3];
a1.push(4); // 合法,数组和对象仅仅不能被重新赋值
// a2.push(4); // 非法,readonly数组不允许被修改
console.log(a1);
定义元组(元组定义了每个位置的元素的类型和整个元组的长度):
let a1: [number, string] = [1, '2'];
let a2: [number, string, ...boolean[]] = [1, '2', true, false];
let a3: readonly [number, string] = [1, '2'];
let a4: Readonly<[number, string]> = [1, '2'];
定义枚举:
enum Color {
Red,
Green,
Blue
};
let c: Color = Color.Green;
let c2: string = Color[2];
console.log(c); // Output: 1,Red和0对应,Green和1对应,Blue和2对应
console.log(c2); // Output: "Blue"
Node.js v24.9.0
下可能需要命令node --experimental-transform-types xxx.ts
才能执行。1.2 运算
对于常见的加减乘除,TS和其他语言并没有什么区别,在这里就不作赘述了。逆天的是,TS对于一下的代码并不会报错,而是有自己的一套运算逻辑。
let result: string = "1" + "2";
console.log(result); // "12": string
console.log(1 + '2'); // "12": string, wtf?
console.log(1 + 2); // 3: number
1.3 任意类型 any
TS中的类型有层级结构,高层次的类型可以包含底层次的类型。层次结构如下所示:
- 顶级类型 Top type:
any
,unknown
- 对象 Object:
{}
- 类型实例:Number String Boolean
- 具体类型:
number
string
boolean
- 具体变量:1 "abc" true
never
如果将所有的类型都声明为any
或者unknown
,那么TS和JS就没什么差别了。
unknown
类型智能赋值给unknown
类型或者any
类型。并且unknown
类型的对象不能访问任何的属性或者方法。所以unknown
比any
更加安全。1.4 Object
2 函数
定义函数时需要指明参数和返回值的类型。
function MyFunc (a: number, b: number): number {
return a + b;
}
TS里面参数可以是可选的,也可以是有默认值的,甚至也可以接收多余的参数到一个数组中(注意必选参数定义时需要写在左侧):
function MyFunc(a: number, b: number = 10, c?: number, ...rest: number[]) : string {
return `Hello, TypeScript! a: ${a}, b: ${b}, c: ${c}, rest: ${rest.join(", ")}`;
}
console.log(MyFunc(5));
console.log(MyFunc(5, 20));
console.log(MyFunc(5, 20, 30));
console.log(MyFunc(5, 20, 30, 40, 50, 60));
❯ node hello.ts
Hello, TypeScript! a: 5, b: 10, c: undefined, rest:
Hello, TypeScript! a: 5, b: 20, c: undefined, rest:
Hello, TypeScript! a: 5, b: 20, c: 30, rest:
Hello, TypeScript! a: 5, b: 20, c: 30, rest: 40, 50, 60
TS支持使用function
关键字定义匿名函数,将这个匿名函数赋值给一个变量以实现调用。
let func = function () {
console.log("Hello, World!");
};
func();
TS也支持箭头函数(Lambda函数,或者可以理解为闭包),它是函数的一种简写形式,往往仅仅定义了输入参数和返回值的关系:
let result = (a: number, b: number): number => {
return a + b;
};
let result2 = (a: number, b: number): number => a + b; // 隐式 return
let str = () => "hello"; // JS 风格
console.log(result(5, 10));
console.log(result2(5, 10));
console.log(str());
3 接口、类和对象
3.1 接口 interface
interface
和Rust中的struct
和trait
类似,规定了一种类型中需要定义的变量或者方法。赋值的变量不能多也不能少。
interface Person {
name: string;
age: number;
}
const greet = (person: Person): string => {
return `Hello, ${person.name}! You are ${person.age} years old.`;
};
const user: Person = { name: "Alice", age: 30 };
console.log(greet(user));
interface
可以定义方法,初始化时需要提供方法的实现(这里就相当于rust中的trait
了)
interface Person {
name: string;
age: number;
sayHello(): void; // 无参数,返回值是 void
}
const person: Person = {
name: "Alice",
age: 30,
sayHello() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
};
person.sayHello();
interface
还可以扩展和继承:
interface Person {
name: string;
age: number;
}
// 扩展 Person 接口,添加新的属性和方法
interface Person {
sayHello(): void;
}
interface Employee extends Person {
employeeId: number;
department: string;
}
const employee: Employee = {
name: "Alice",
age: 30,
employeeId: 12345,
department: "Engineering",
sayHello() {
console.log(`Hello, my name is ${this.name}, I work in the ${this.department} department.`);
}
};
employee.sayHello(); // 输出: Hello, my name is Alice, I work in the Engineering department.
有时候我们从后端接受的数据可以放在一整个interface
中,但是我们只关心其中的某些变量,这时可以使用索引签名:
interface Person {
name: string;
age: number;
[key: string]: any; // Index signature 索引签名
}
// 我们只关心 name 和 age 属性
const person: Person = {
name: "Alice",
age: 30,
occupation: "Engineer" // 可以添加额外的属性,可以是任何类型
};
interface Person {
name: string;
age: number;
readonly sayHello: () => void; // readonly,只能用于箭头函数定义的方式
}
const p: Person = {
name: 'Alice',
age: 30,
sayHello: () => {
console.log('Hello');
}
};
// p.sayHello = () => { console.log('Hi'); }; // Error: Cannot assign to 'sayHello' because it is a read-only property
p.sayHello(); // Output: Hello
有时候,我们可以使用接口规定特定的“函数类型”,使其具有特定的参数类似和返回值类型:
interface Fn {
(a: string): number; // Function type with a string parameter and number return type
}
const fn: Fn = (a) => {
return a.length;
};
console.log(fn("hello")); // Output: 5
3.2 类 class
3.2.1 类的基础用法
类是ES6才拥有的关键字。使用下面的方式创建一个类:类中可以有属性,也可以有方法。其中constructor
是一个特殊的方法,它是构造函数。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet(): void {
console.log(`Hello, my name is ${this.name}`);
}
}
const person = new Person("Alice");
person.greet();
可以使用interface
对类进行约束,使其必须拥有特定的属性和方法实现;也可以和interface
一样进行继承,并且子类可以重写父类的方法:
interface Greetable {
name: string;
greet(): void;
}
class Person implements Greetable {
name: string;
constructor(name: string) {
this.name = name;
}
greet(): void {
console.log(`Hello, my name is ${this.name}`);
}
}
class Employee extends Person {
jobTitle: string;
constructor(name: string, jobTitle: string) {
super(name);
this.jobTitle = jobTitle;
}
greet(): void {
console.log(`Hello, my name is ${this.name} and I am a ${this.jobTitle}`);
}
}
const person = new Person("Alice");
person.greet(); // Output: Hello, my name is Alice
const employee = new Employee("Bob", "Software Engineer");
employee.greet(); // Output: Hello, my name is Bob and I am a Software Engineer (调用了重写的方法)
当然,在上面的例子中,Empolyee
类肯定也实现了Greetable
接口,这由编译器自动推断。当然也可以手动标注:
class Employee extends Person implements Greetable {
// ...
}
类可以使用private
、protected
、public
规定属性对外部的可见范围:
class Person {
protected readonly name: string; // 使用 protected 使得子类可以访问
constructor(name: string) {
this.name = name;
}
public greet(): void {
console.log(`Hello, my name is ${this.name}`);
}
}
class Employee extends Person {
private readonly jobTitle: string; // 使用 private 使得外部无法访问
constructor(name: string, jobTitle: string) {
super(name);
this.jobTitle = jobTitle;
}
public greet(): void {
console.log(`Hello, my name is ${this.name} and I am a ${this.jobTitle}`);
}
}
const person = new Person("Alice");
person.greet(); // Output: Hello, my name is Alice
const employee = new Employee("Bob", "Software Engineer");
employee.greet(); // Output: Hello, my name is Bob and I am a Software Engineer (调用了重写的方法)
interface
和class
在某些功能上是相似甚至重复的。但是interface
更加强调对数据的一种约束,它可以用于对对象{}
的约束,也可以用于对类的约束。这一点功能的划分上我认为TS做的不好,所以能使用类的时候使用类最好,毕竟每种抽象方式都应该有其对应的用途。