MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Typescript与JavaScript的差异

2024-11-135.3k 阅读

一、类型系统

1.1 静态类型与动态类型

JavaScript 是一门动态类型语言,这意味着变量的类型在运行时才确定。例如:

let num;
num = 10;
num = 'ten';

在上述代码中,变量 num 先被赋值为数字 10,随后又被赋值为字符串 'ten',JavaScript 运行时并不会对这种类型的改变抛出错误。

而 TypeScript 是静态类型语言,变量在声明时就需要指定类型,或者通过类型推断确定类型,一旦类型确定,后续赋值必须符合该类型。比如:

let num: number;
num = 10;
// num = 'ten'; // 这行代码会报错,因为不能将字符串赋值给number类型的变量

这里明确声明 numnumber 类型,当试图将字符串赋值给它时,TypeScript 编译器就会报错,提示类型不匹配。

1.2 类型注解与类型推断

在 TypeScript 中,类型注解是程序员显式指定变量类型的方式。比如:

let str: string = 'hello';

这里通过 : string 明确指定了 str 变量的类型为字符串。

类型推断则是 TypeScript 编译器自动根据变量的初始值推断出其类型。例如:

let num = 10; // 这里虽然没有显式指定类型,但TypeScript编译器会推断num为number类型

在函数参数和返回值中,同样可以使用类型注解和类型推断。对于函数参数,类型注解明确了传入参数应有的类型:

function add(a: number, b: number): number {
    return a + b;
}

这里明确要求 add 函数接收两个 number 类型的参数,并返回 number 类型的值。

而在一些情况下,TypeScript 也能根据函数实现推断出返回值类型,例如:

function multiply(a, b) {
    return a * b;
}
// 这里TypeScript能推断出multiply函数接收两个any类型参数并返回any类型值

但为了代码的可读性和维护性,建议显式写出函数的参数和返回值类型。

1.3 联合类型与交叉类型

联合类型允许一个变量有多种类型。比如:

let value: string | number;
value = 'hello';
value = 10;

这里 value 可以是 string 类型或者 number 类型。

交叉类型则是将多个类型合并为一个类型,新类型包含了所有类型的特性。例如:

interface A {
    name: string;
}
interface B {
    age: number;
}
let person: A & B = { name: 'John', age: 30 };

person 变量同时具备 AB 接口的属性,即 name 字符串属性和 age 数字属性。

JavaScript 本身没有联合类型和交叉类型的概念,在处理复杂数据类型组合时,通常需要更多的逻辑判断来模拟类似功能。

二、接口与类型别名

2.1 接口(Interface)

在 TypeScript 中,接口用于定义对象的形状(shape),即对象应该包含哪些属性以及这些属性的类型。例如:

interface User {
    name: string;
    age: number;
    email: string;
}
let user: User = { name: 'Alice', age: 25, email: 'alice@example.com' };

接口还可以定义函数类型:

interface AddFunction {
    (a: number, b: number): number;
}
let add: AddFunction = function (a, b) {
    return a + b;
};

这里 AddFunction 接口定义了一个接收两个 number 类型参数并返回 number 类型值的函数类型。

2.2 类型别名(Type Alias)

类型别名可以给一个类型起一个新名字。它不仅可以用于对象类型,还可以用于基本类型、联合类型等。例如:

type StringOrNumber = string | number;
let value: StringOrNumber = 10;
value = 'hello';

对于对象类型,类型别名也能达到类似接口的效果:

type UserType = {
    name: string;
    age: number;
    email: string;
};
let user: UserType = { name: 'Bob', age: 30, email: 'bob@example.com' };

接口和类型别名有一些区别。接口只能用于定义对象类型,而类型别名可以用于任何类型。接口具有可扩展性,即可以通过声明合并来扩展:

interface User {
    name: string;
}
interface User {
    age: number;
}
let user: User = { name: 'Charlie', age: 28 };

而类型别名不能通过声明合并扩展。

在 JavaScript 中没有接口和类型别名的概念,在处理对象结构和类型定义时,通常依靠约定俗成的方式或者使用第三方库来进行类似功能的模拟。

三、类与模块

3.1 类(Class)

JavaScript 在 ES6 引入了类的概念,TypeScript 在此基础上对类进行了更完善的类型支持。

在 JavaScript 中定义类如下:

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
let dog = new Animal('Buddy');
dog.speak();

在 TypeScript 中,类的定义可以更严格,比如对属性和方法进行类型标注:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak(): void {
        console.log(`${this.name} makes a sound.`);
    }
}
let dog: Animal = new Animal('Buddy');
dog.speak();

TypeScript 还支持类的继承、访问修饰符等特性。继承通过 extends 关键字实现:

class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }
    bark(): void {
        console.log(`${this.name} barks.`);
    }
}
let myDog = new Dog('Max');
myDog.speak();
myDog.bark();

访问修饰符如 public(默认,可在类内外访问)、private(只能在类内部访问)、protected(只能在类内部及子类中访问),例如:

class Animal {
    private name: string;
    constructor(name: string) {
        this.name = name;
    }
    public speak(): void {
        console.log(`${this.name} makes a sound.`);
    }
}
// let name = myDog.name; // 这行代码会报错,因为name是private属性

3.2 模块(Module)

JavaScript 在 ES6 引入了模块系统,TypeScript 同样支持并进行了增强。

在 JavaScript 中,使用 exportimport 关键字来导出和导入模块。例如,有一个 math.js 文件:

// math.js
export function add(a, b) {
    return a + b;
}
export function subtract(a, b) {
    return a - b;
}

在另一个文件中导入使用:

// main.js
import { add, subtract } from './math.js';
console.log(add(5, 3));
console.log(subtract(5, 3));

TypeScript 中,模块的使用类似,但在类型检查方面更严格。例如,在 math.ts 文件中:

// math.ts
export function add(a: number, b: number): number {
    return a + b;
}
export function subtract(a: number, b: number): number {
    return a - b;
}

main.ts 文件中导入使用:

// main.ts
import { add, subtract } from './math.ts';
console.log(add(5, 3));
console.log(subtract(5, 3));

TypeScript 还支持命名空间(Namespace),它可以将相关代码组织在一起,避免命名冲突。例如:

namespace Geometry {
    export function calculateArea(shape: 'circle' |'square', value: number): number {
        if (shape === 'circle') {
            return Math.PI * value * value;
        } else {
            return value * value;
        }
    }
}
let circleArea = Geometry.calculateArea('circle', 5);

虽然 JavaScript 没有命名空间的概念,但可以通过立即执行函数表达式(IIFE)模拟类似的作用域隔离效果。

四、函数相关差异

4.1 函数参数默认值

在 JavaScript 中,函数参数可以有默认值。例如:

function greet(name = 'Guest') {
    console.log(`Hello, ${name}!`);
}
greet();
greet('John');

TypeScript 同样支持函数参数默认值,并且在类型检查方面更严格。例如:

function greet(name: string = 'Guest') {
    console.log(`Hello, ${name}!`);
}
greet();
greet('John');

如果在 TypeScript 中,给函数传递的参数类型与声明的参数类型不匹配,编译器会报错。

4.2 剩余参数与展开运算符

JavaScript 中使用剩余参数(Rest Parameters)收集多余的参数,使用展开运算符(Spread Operator)将数组展开为参数。例如:

function sum(...nums) {
    return nums.reduce((acc, num) => acc + num, 0);
}
let result = sum(1, 2, 3);
let arr = [4, 5, 6];
let newResult = sum(...arr);

在 TypeScript 中,剩余参数也需要指定类型:

function sum(...nums: number[]): number {
    return nums.reduce((acc, num) => acc + num, 0);
}
let result = sum(1, 2, 3);
let arr: number[] = [4, 5, 6];
let newResult = sum(...arr);

这样确保了传入的参数类型符合预期,增强了代码的健壮性。

4.3 函数重载

函数重载是指在同一个作用域内,可以有多个同名函数,但它们的参数列表不同。JavaScript 本身不支持函数重载,而 TypeScript 支持。例如:

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a ==='string' && typeof b ==='string') {
        return a + b;
    }
}
let numResult = add(1, 2);
let strResult = add('Hello, ', 'world');

这里定义了两个 add 函数的重载,一个接收两个数字参数返回数字,另一个接收两个字符串参数返回字符串。实际的实现函数根据传入参数的类型来执行不同的逻辑。

五、类型兼容性

5.1 结构类型系统

TypeScript 使用结构类型系统(Structural Type System),即只要两个类型的结构兼容,它们就是兼容的,而不要求类型完全相同。例如:

interface Point {
    x: number;
    y: number;
}
interface Point3D {
    x: number;
    y: number;
    z: number;
}
let point: Point = { x: 1, y: 2 };
let point3D: Point3D = { x: 1, y: 2, z: 3 };
point = point3D; // 这里是允许的,因为Point3D的结构包含了Point的结构

在上述代码中,Point3D 类型的变量可以赋值给 Point 类型的变量,因为 Point3D 包含了 Point 所有的属性。

5.2 函数参数的双向协变

在 TypeScript 中,函数参数类型是双向协变的。这意味着如果一个函数期望接收一个较宽泛类型的参数,那么实际传入一个较具体类型的参数是允许的,反之亦然。例如:

function printValue(value: any) {
    console.log(value);
}
function printNumber(num: number) {
    console.log(num);
}
let printAny: (value: any) => void = printNumber; // 允许,因为number是any的子类型

然而,双向协变可能会导致一些潜在的问题,比如传入的参数类型不符合预期。在严格模式下,TypeScript 会对函数参数进行更严格的检查,避免这种情况。

JavaScript 没有明确的类型兼容性概念,因为它是动态类型语言,在运行时才检查类型,只要函数在运行时能正确处理传入的参数,就不会报错。

六、编译与运行

6.1 编译过程

TypeScript 代码需要经过编译才能在 JavaScript 环境中运行。编译过程主要是将 TypeScript 代码转换为等价的 JavaScript 代码,并进行类型检查。例如,有如下 TypeScript 代码:

let num: number = 10;
function add(a: number, b: number): number {
    return a + b;
}
let result = add(num, 5);

使用 TypeScript 编译器(tsc)编译后,会生成如下 JavaScript 代码:

var num = 10;
function add(a, b) {
    return a + b;
}
var result = add(num, 5);

可以看到,编译后的 JavaScript 代码去除了类型相关的信息。

在编译过程中,如果代码存在类型错误,TypeScript 编译器会报错,提示错误信息,帮助开发者定位和修复问题。例如,将上述代码改为:

let num: number = 10;
function add(a: number, b: number): number {
    return a + b;
}
let result = add(num, '5'); // 这里字符串类型与参数预期的number类型不匹配

编译时会报错:Argument of type'string' is not assignable to parameter of type 'number'.

6.2 运行环境

JavaScript 可以直接在浏览器、Node.js 等环境中运行,因为这些环境内置了 JavaScript 引擎。而 TypeScript 代码不能直接在这些环境中运行,必须先编译为 JavaScript 代码。

在浏览器环境中,通常将编译后的 JavaScript 文件引入 HTML 页面:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>TypeScript Example</title>
</head>

<body>
    <script src="compiled.js"></script>
</body>

</html>

在 Node.js 环境中,同样运行编译后的 JavaScript 文件:

node compiled.js

通过这种方式,开发者可以利用 TypeScript 的类型安全等优势,同时在现有的 JavaScript 运行环境中部署和运行代码。

七、工具与生态

7.1 开发工具支持

JavaScript 由于其广泛应用,几乎所有主流的代码编辑器和集成开发环境(IDE)都提供了良好的支持,如 Visual Studio Code、WebStorm 等。这些工具提供代码高亮、代码补全、语法检查等功能。

TypeScript 在开发工具支持方面更进一步。由于其静态类型特性,编辑器和 IDE 可以提供更智能的代码补全、类型提示、跳转到定义等功能。例如,在 Visual Studio Code 中,当使用 TypeScript 编写代码时,编辑器能根据类型注解和类型推断,准确地提示变量、函数的可用属性和方法,大大提高开发效率。同时,编辑器能实时检测代码中的类型错误,并给出详细的错误提示,方便开发者及时修复。

7.2 生态系统

JavaScript 拥有庞大且成熟的生态系统,有丰富的第三方库和工具,如 React、Vue.js、Node.js 等。这些库和工具在 Web 开发、后端开发等领域广泛应用。

TypeScript 与 JavaScript 生态系统高度兼容,大部分 JavaScript 库都可以在 TypeScript 项目中使用。同时,TypeScript 也有自己的生态系统,许多库和框架开始提供 TypeScript 类型定义文件(.d.ts),使得在 TypeScript 项目中使用这些库时能获得更好的类型支持。例如,React 官方提供了对 TypeScript 的支持,开发者可以使用 TypeScript 编写 React 应用,享受类型安全带来的好处。此外,一些新的库和工具也专门为 TypeScript 开发,进一步丰富了 TypeScript 的生态。

综上所述,TypeScript 和 JavaScript 在类型系统、接口与类型别名、类与模块、函数、类型兼容性、编译运行以及工具生态等方面存在诸多差异。TypeScript 基于 JavaScript 进行扩展,通过引入静态类型系统等特性,提高了代码的可维护性、健壮性和开发效率,适用于大型项目和对代码质量要求较高的场景。而 JavaScript 则以其灵活性和动态性,在一些小型项目或快速迭代的项目中仍有其优势。开发者可以根据项目的具体需求选择合适的语言或在项目中结合使用两者。