JS存在的痛点
考虑现代编程实践时,一个普遍的共识是:尽早发现错误,因为这样可以更容易、更便宜地修复它们。然而,在JavaScript等灵活性较高的语言中,传统的编译期类型检查和严格的参数限制往往缺失。这就导致了一个严峻的问题:在运行时,我们可能会面临各种各样的类型错误和不一致性,这些错误很难追踪和修复。
在JavaScript中,这种问题尤为明显。由于它是一种弱类型语言,变量的类型可以在运行时动态改变。这意味着,如果我们期望一个函数的参数是字符串,但在调用时传入了一个数字或其他类型,只有在运行时才会被发现。这种灵活性虽然方便了开发,但也增加了代码出错的可能性。
为了解决这个问题,我们需要一种方法在编译期间就发现这些类型错误。这种需求引入了静态类型检查的概念。通过引入类型检查,我们可以在代码编写的早期阶段就发现潜在的类型错误,而不是在运行时。这样,开发者就能在代码提交到版本控制系统之前,就知道代码中存在的潜在问题,避免了在项目后期才发现并修复这些问题所带来的巨大成本。
在JavaScript中,一些现代的开发工具和语言扩展(如TypeScript)已经开始支持静态类型检查。TypeScript是JavaScript的超集,它引入了静态类型系统,使得开发者可以在编写代码时就定义变量和函数的类型。这种做法不仅可以减少类型相关的错误,还可以提供更好的代码补全和文档生成等功能,从而提高了开发效率。
在现代软件开发中,采用这种静态类型检查的方法已经成为一种最佳实践。通过引入类型检查,我们可以在编译期间发现并解决大部分潜在的错误,保证代码的质量和稳定性。这种方法不仅适用于大型项目,也可以在小型项目中提供更好的开发体验和代码可维护性。
类型思维
类型思维是指在编程中,对于数据的类型、结构和限制有清晰的认知和考虑,以及能够将这种认知转化为代码实践的能力。在强类型语言(如Java、C++)中,类型思维通常是程序员的基本素养,因为在这些语言中,变量和函数参数的类型通常需要明确声明。
然而,在弱类型语言(如JavaScript)中,类型思维的要求相对较低,因为变量的类型可以在运行时动态改变。这种灵活性虽然方便了开发,但也容易导致开发者在编写代码时不够谨慎,忽视了数据类型的重要性。这就意味着,当项目变得复杂,多人协作时,容易出现由于类型错误引起的 bug。
对于前端开发人员来说,JavaScript的宽松类型约束可能导致他们对数据类型的关注较少。在很多情况下,开发者可能并不会关心一个变量到底是什么类型的,直到在程序运行时出现错误。为了解决这个问题,开发者可能会采用各种判断和验证方式来确保数据的类型和结构,这种做法增加了代码的复杂性和维护成本。
在大型项目中,缺乏类型思维可能导致以下问题:
- 代码不健壮: 缺少类型检查容易导致运行时错误,使得代码不够健壮,难以应对各种异常情况。
- 维护困难: 当多人协作开发时,没有良好的类型契约可能导致代码难以理解和维护。调用函数时,如果没有文档或注释,其他开发者难以知道该传入什么参数,期望得到什么返回值。
- 调试困难: 缺乏类型信息会增加调试的难度,因为在出错时很难追踪到错误的根源,特别是在大型项目中。
为了解决这些问题,现代的JavaScript开发实践中,开发者逐渐认识到引入类型思维的重要性。这种思维方式包括使用静态类型检查工具(如TypeScript、Flow),注重函数和变量的命名规范以明确其用途和数据类型,以及加强对数据结构和接口的设计与约束。通过引入类型思维,开发者能够更早地发现潜在问题,提高代码的质量和可维护性,使得JavaScript在大型项目中也能够得心应手。
以下列代码为例
// 当前foo函数, 在被其他地方调用时, 没有做任何的参数校验
// 1> 没有对类型进行校验
// 2> 没有对是否传入参数进行校验
function foo(message) {
if (message) {
console.log(message.length);
}
}
foo("Hello World");
foo("你好啊,李银河");
foo(123)
foo()
// 永远执行不到
console.log("去渲染界面")
段代码展示了一个常见的JavaScript开发中的问题:函数参数的类型和是否传入的校验不足。具体来说:
- 缺乏参数类型校验: foo 函数没有对 message 参数的类型进行校验。在JavaScript中,函数的参数类型是不固定的,这意味着 message 可以是任何类型的数据。虽然这在一些情况下可能是期望的行为,但如果你希望 message 必须是字符串类型,就需要加入类型校验。
- 缺乏参数是否传入的校验: foo 函数在调用时,没有检查参数是否传入。在该函数的内部,没有对 message 是否为 undefined 进行校验,这可能导致在没有传入参数的情况下引发错误。如果 message 没有被传入,调用 message.length 就会导致 TypeError。
- 后续的代码可能不会执行: 函数中的最后一行 console.log("渲染界面成千上万行的JavaScript代码需要执行, 去渲染界面") 实际上永远不会执行。因为在前面的调用中,foo 函数可能已经因为错误而停止执行,从而导致后续的代码不会被执行。
- 变量类型变化: bar 变量在代码中被赋值了不同类型的数据,这在JavaScript中是允许的。然而,这种自由的变量类型转换可能导致意外的错误,特别是在代码逻辑依赖于特定类型的情况下。
为了改善这些问题,开发者可以考虑以下做法:
- 参数类型校验: 使用函数签名或注释明确函数的参数类型,或者在函数内部进行参数类型检查,确保传入的参数符合预期。
- 参数是否传入校验: 在函数内部加入参数是否存在的校验,可以使用条件语句(如 if (typeof message !== 'undefined'))来确保函数在没有接收到参数时不会引发错误。
- 变量类型稳定性: 尽量保持变量的类型稳定,避免在不同的地方赋予不同类型的值。如果一个变量在某处是字符串,在其他地方不应该被赋值为数字或其他类型的数据,以维护代码的可读性和稳定性。
typescript类型
TypeScript 是 JavaScript 的一个超集,它添加了静态类型检查。这意味着你可以在编码的时候就能发现潜在的类型相关的错误,而不是在运行时才发现。以下是 TypeScript 中常见的类型:
基本类型:
- number: 表示数值,可以是整数或浮点数。
- string: 表示字符串。
- boolean: 表示布尔值。
- null 和 undefined: 分别表示 null 和 undefined。
- void: 表示没有任何返回值的函数。
- any: 表示任意类型,可以赋值为任何类型的值。
let num: number = 10;
let str: string = "Hello";
let isValid: boolean = true;
let nullableValue: null = null;
let someValue: any = 42;
数组:
TypeScript 允许你指定数组中元素的类型。
let numbers: number[] = [1, 2, 3, 4];
let strings: string[] = ["apple", "banana", "cherry"];
元组(Tuple):
元组是固定长度的数组,每个元素的类型都可以不同。
let tuple: [string, number] = ["hello", 10];
枚举(Enum):
枚举类型允许你为一组数值赋予友好的名字。枚举(Enum)是一种用户定义的数据类型,用于表示具名常数的集合。枚举可以帮助我们清晰地表示某个变量可能的取值范围,使得代码更易读,更易维护。
enum Color {
Red,
Green,
Blue,
}
let selectedColor: Color = Color.Red;
异构枚举即枚举中既包含字符串成员又包含数字成员。在实际使用中,尽量避免使用异构枚举,因为它容易引起混淆。
enum Status {
Success = 200,
NotFound = "NOT_FOUND"
}
常量枚举是指在编译阶段被移除的枚举。它通过 const enum 关键字定义,常量枚举不能包含计算成员。
const enum Fruit {
Apple,
Banana,
Orange
}
let fruit: Fruit = Fruit.Apple;
对象:
可以使用接口(Interface)定义对象的结构。
interface Person {
name: string;
age: number;
}
let person: Person = {
name: "Alice",
age: 30,
};
函数:
你可以指定函数的参数类型和返回值类型。
function add(x: number, y: number): number {
return x + y;
}
let result: number = add(5, 10);
类型断言(Type Assertion):
当你比 TypeScript 更了解某个值的类型时,可以使用类型断言来告诉编译器值的实际类型。类型断言(Type Assertion)是一种开发者告诉编译器某个值的类型的方法。它与类型转换在作用上是类似的,但是在编译时不会进行特殊的检查或者数据的转换。而是在编译阶段告诉编译器开发者已经进行了类型检查,使得后续的操作可以正常进行。
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
尖括号语法:在这个例子中,是尖括号语法,用于将someValue断言为string类型。这样就可以安全地调用length属性。
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
console.log(strLength); // 输出 16
as语法:在这个例子中,as语法是另一种类型断言的方式,与尖括号语法等价。它在React/JSX的语境中更为推荐,因为在JSX中,尖括号语法可能被误解为JSX的语法。
需要注意的是,类型断言不是类型转换。它不会在运行时影响对象的结构,只是在编译阶段告诉编译器开发者的意图。如果类型断言的类型与实际类型不匹配,就会导致运行时错误。因此,在使用类型断言时,开发者需要确保类型的一致性,以避免潜在的错误。
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
console.log(strLength); // 输出 16
字面量推理
在TypeScript中,字面量推理(Literal Inference)是指TypeScript根据变量的赋值来推断其具体的类型,包括字符串字面量、数字字面量、布尔字面量等。字面量推理可以帮助开发者在编写代码时更好地利用静态类型检查,提高代码的可读性和可维护性。
type Result = {
success: true;
message: string;
} | {
success: false;
error: string;
};
let response: Result = {
success: true,
message: "Data retrieved successfully."
};
在这个例子中,TypeScript通过字面量推理和联合类型推断response的类型。它可以是一个成功的响应对象,包含success: true和message: string属性,或者是一个失败的响应对象,包含success: false和error: string属性。
字面量推理使得TypeScript能够更精确地确定变量的类型,从而提供更强的类型安全性和更好的开发体验。
typescript函数
在TypeScript中,函数是一种非常重要的数据类型,它可以像JavaScript一样接受参数和返回值。以下是TypeScript函数的一些基本用法:
函数声明和定义:
// 函数声明
function add(x: number, y: number): number {
return x + y;
}
// 函数表达式
let multiply = function(x: number, y: number): number {
return x * y;
};
函数类型:
你可以为函数定义类型,包括参数和返回值类型。
type MathFunction = (x: number, y: number) => number;
let add: MathFunction = function(x, y) {
return x + y;
};
可选参数和默认参数:
在TypeScript中,你可以为函数的参数指定可选参数和默认参数。
// 可选参数
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
} else {
return `Hello, ${name}!`;
}
}
// 默认参数
function introduce(name: string, age: number = 30): string {
return `My name is ${name} and I am ${age} years old.`;
}
函数重载:
TypeScript允许你为同一个函数提供多个函数类型定义,被称为函数重载。当你调用这个函数时,TypeScript会根据传入的参数类型和数量来自动判断应该使用哪个函数定义。这样可以增强函数的灵活性和类型安全性。
function reverse(value: string): string;
function reverse(value: number): number;
function reverse(value: string | number): string | number {
if (typeof value === "string") {
return value.split("").reverse().join("");
} else if (typeof value === "number") {
return Number(value.toString().split("").reverse().join(""));
}
return value;
}
在这个例子中,reverse
函数被重载了两次。第一次重载定义了接受一个字符串参数并返回字符串的函数签名,第二次重载定义了接受一个数字参数并返回数字的函数签名。最后的实现部分是函数的主体,根据传入参数的类型分别处理。
当你调用 reverse
函数时,TypeScript 根据传入参数的类型来确定应该使用哪个重载定义。例如:
let reversedString: string = reverse("hello"); // 返回 "olleh"
let reversedNumber: number = reverse(12345); // 返回 54321
TypeScript 编译器会根据传入的参数类型选择正确的重载,提供了更好的类型检查和自动补全,确保了在不同情况下函数的安全调用。
this和箭头函数:
TypeScript中的箭头函数(Arrow Functions)不会创建自己的this上下文,它会捕获所在上下文的this值。
在TypeScript中,this 和箭头函数(Arrow Functions)的行为与JavaScript中有些许不同,特别是在类方法和回调函数的情境下。
class Counter {
count: number = 0;
increase = () => {
this.count++;
};
}
let counter = new Counter();
counter.increase();
console.log(counter.count); // 输出: 1
在JavaScript中,函数的this值是在运行时确定的,它取决于函数是如何被调用的。但是,在传统的JavaScript函数中,this的指向可能会让开发者感到困扰,特别是在回调函数或者异步操作中。为了解决这个问题,箭头函数被引入,箭头函数不会创建自己的this,而是会捕获所在上下文的this值。
在TypeScript中,箭头函数的行为与JavaScript中相同。它们不会捕获this,而是继承自包围它们的作用域。这意味着在箭头函数内部,this的值与外部作用域的this相同。、
class MyClass {
value: number = 10;
// 使用箭头函数
arrowFunction = () => {
console.log(this.value); // 正确,this指向外部作用域的this,即MyClass的实例
};
// 普通方法
normalFunction() {
console.log(this.value); // 正确,this指向当前实例
}
}
const obj = new MyClass();
obj.arrowFunction(); // 输出 10
obj.normalFunction(); // 输出 10
有时候,在TypeScript中,你可能希望明确指定一个函数中this的类型,这可以通过this
参数来实现。this参数是一个特殊的函数参数,它告诉TypeScript这个函数将在哪个上下文中被调用。
function myFunction(this: { value: number }) {
console.log(this.value);
}
const obj = { value: 42 };
myFunction.call(obj); // 输出 42
在这个例子中,myFunction
函数被明确指定为只能在具有value
属性的对象上下文中被调用。在调用函数之前,你需要使用call、apply或者bind等方法来设置函数的this值。
这种方法在TypeScript中为你提供了更多的类型安全,确保了函数在特定上下文中被正确调用。
typescript的类
在TypeScript中,你可以使用类(Classes)来创建对象,类是一种面向对象编程的基本概念。
class ClassName {
// 属性声明
propertyName: type;
// 构造函数
constructor(parameters) {
// 初始化属性
this.propertyName = value;
}
// 方法
methodName() {
// 方法体
}
}
- 属性声明: 类中的属性可以包含各种数据类型,例如number,string,boolean等。
- 构造函数: 类的构造函数在对象创建时被调用,用于初始化对象的属性。构造函数的参数可以用来接收传入的值。
- 方法: 类中的方法是一组执行特定任务的语句,可以操作类的属性,实现各种功能。
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
eating() {
console.log(this.name + " eating");
}
}
在这个代码中,我们定义了一个名为Person
的类。该类有两个属性:name
(字符串类型)和age
(数字类型)。类中还有一个构造函数(constructor),在对象创建时会被调用,用来初始化对象的属性。类还有一个名为eating的方法。
类的多态
多态是面向对象编程中一个重要的概念,它允许不同的类实现同一个接口或继承同一个父类,并且可以以相同的方式调用其中的方法。在TypeScript中,多态性可以通过类的继承和方法的重写来实现。
考虑以下的例子:
class Animal {
sound(): void {
console.log("Animal makes a sound");
}
}
class Dog extends Animal {
sound(): void {
console.log("Dog barks");
}
}
class Cat extends Animal {
sound(): void {
console.log("Cat meows");
}
}
function makeSound(animal: Animal): void {
animal.sound();
}
const dog = new Dog();
const cat = new Cat();
makeSound(dog); // 输出: Dog barks
makeSound(cat); // 输出: Cat meows
在这个例子中,Animal 类有一个 sound 方法,而 Dog 和 Cat 类继承自 Animal 类并且都重写了 sound 方法。makeSound 函数接收一个 Animal 类型的参数,当我们传入 Dog 或 Cat 的实例时,它们的 sound 方法被调用,实现了多态性。
在多态的情况下,编译器会根据传入的对象类型来确定调用哪个类的方法,这样可以实现灵活的代码结构和简化代码的复杂度。
抽象类
在TypeScript中,抽象类(Abstract Class)是一种特殊的类,它不能被直接实例化,只能被继承。抽象类通常用于定义一组相关的类的通用结构和方法,而这些方法的具体实现则由继承它的子类来完成。抽象类中的方法可以包含抽象方法,这些抽象方法只有方法签名而没有具体实现,子类必须提供具体实现。
abstract class MyAbstractClass {
abstract myAbstractMethod(): void; // 抽象方法,子类必须实现
regularMethod(): void {
console.log("This is a regular method in the abstract class.");
}
}
class MyConcreteClass extends MyAbstractClass {
myAbstractMethod(): void {
console.log("This is the concrete implementation of the abstract method.");
}
}
const myInstance = new MyConcreteClass();
myInstance.myAbstractMethod();
myInstance.regularMethod();
上述代码中,MyAbstractClass
是一个抽象类,其中包含一个抽象方法 myAbstractMethod
和一个普通方法 regularMethod
。MyConcreteClass 是一个具体的子类,它继承了 MyAbstractClass 并实现了抽象方法 myAbstractMethod。子类中的抽象方法必须提供具体实现。
关键点和用途:
- 抽象类不能被实例化,只能用作其他类的基类。
- 抽象类中可以包含抽象方法和普通方法。
- 子类必须实现抽象类中的抽象方法。
- 抽象类常用于定义通用的接口和约束,以确保子类具有特定的行为和结构。
- 抽象类在面向对象编程中有助于实现多态和封装,同时也可以提供一致的接口定义,以确保符合规范的实现。