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

TypeScript中null和undefined的区别与应用

2023-03-317.8k 阅读

null 和 undefined 的基础定义

在 TypeScript 中,nullundefined 都属于基本数据类型,它们在 JavaScript 中同样存在,TypeScript 对其进行了类型系统的强化。

undefined 表示一个变量声明了但未初始化。例如:

let a: number;
console.log(a); // 输出 undefined

在上述代码中,变量 a 被声明为 number 类型,但没有被赋值,此时它的值就是 undefined

null 表示一个空值,它是一个有意赋值的空对象指针。在 JavaScript 最初的设计中,null 被设计用来表示一个空的对象引用。虽然这在某种程度上是一个设计失误(typeof null 返回 "object"),但在 TypeScript 中,它依然保持着这样的语义。例如:

let b: string | null;
b = null;
console.log(b); // 输出 null

这里变量 b 被声明为可以是 string 类型或者 null 类型,之后 b 被赋值为 null

类型系统中的表现

严格模式与宽松模式

TypeScript 中有一个 strictNullChecks 编译选项,它对 nullundefined 的类型处理有着重要影响。

在宽松模式下(strictNullChecksfalse),nullundefined 可以赋值给几乎任何类型。例如:

let num: number;
num = undefined; // 不会报错,宽松模式下允许
num = null; // 不会报错,宽松模式下允许

这种宽松的行为可能会导致运行时错误,因为在后续代码中可能会假设这个变量是一个有效的 number 类型值,而实际上它可能是 nullundefined

在严格模式下(strictNullCheckstrue),nullundefined 只能赋值给它们自己的类型以及 any 类型。例如:

let str: string;
// str = null; // 报错,严格模式下不允许将 null 赋值给 string 类型
// str = undefined; // 报错,严格模式下不允许将 undefined 赋值给 string 类型
let nullableStr: string | null | undefined;
nullableStr = null; // 允许,因为类型声明中包含了 null
nullableStr = undefined; // 允许,因为类型声明中包含了 undefined

严格模式有助于在编译阶段捕获可能的空值引用错误,提高代码的健壮性。

类型推断

TypeScript 的类型推断机制在处理 nullundefined 时也会有所不同。

当一个变量没有显式类型注解时,TypeScript 会根据赋值情况进行类型推断。如果变量被赋值为 undefined,那么它的类型会被推断为 undefined。例如:

let var1;
var1 = undefined;
// 此时 var1 的类型为 undefined

如果变量被赋值为 null,那么它的类型会被推断为 null。例如:

let var2;
var2 = null;
// 此时 var2 的类型为 null

当函数返回值没有显式类型注解时,如果函数有可能返回 nullundefined,则需要特别注意。例如:

function getValue(): number | null {
    const random = Math.random();
    if (random > 0.5) {
        return 10;
    } else {
        return null;
    }
}
let result = getValue();
if (result!== null) {
    console.log(result.toFixed(2)); // 只有在 result 不为 null 时才调用 toFixed 方法
}

在上述代码中,getValue 函数返回值类型被推断为 number | null,调用该函数后,需要对返回值进行 null 检查,以避免运行时错误。

在函数中的应用

参数与返回值

在函数参数中,明确 nullundefined 的类型可以有效避免空值错误。例如,假设有一个计算两个数之和的函数:

function add(a: number, b: number): number {
    return a + b;
}
// add(1, null); // 报错,严格模式下 null 不能作为 number 类型参数
// add(1, undefined); // 报错,严格模式下 undefined 不能作为 number 类型参数

如果函数参数允许 nullundefined,则需要在类型声明中体现。例如:

function addNullable(a: number | null | undefined, b: number | null | undefined): number | null {
    if (a === null || a === undefined || b === null || b === undefined) {
        return null;
    }
    return a + b;
}
let sum1 = addNullable(1, 2);
let sum2 = addNullable(null, 2);
console.log(sum1); // 输出 3
console.log(sum2); // 输出 null

在函数返回值方面,如果函数可能返回 nullundefined,调用者必须做好相应的处理。例如,从数组中获取特定索引位置的元素:

function getElementFromArray(arr: number[], index: number): number | null {
    if (index < 0 || index >= arr.length) {
        return null;
    }
    return arr[index];
}
let arr = [1, 2, 3];
let element1 = getElementFromArray(arr, 1);
let element2 = getElementFromArray(arr, 10);
if (element1!== null) {
    console.log(element1); // 输出 2
}
if (element2!== null) {
    console.log(element2); // 不会输出,因为 element2 为 null
}

可选参数与默认参数值

TypeScript 中的函数可以有可选参数,可选参数在本质上就是允许传入 undefined。例如:

function greet(name: string, message?: string) {
    if (message) {
        console.log(`Hello, ${name}! ${message}`);
    } else {
        console.log(`Hello, ${name}!`);
    }
}
greet('John'); // 输出 Hello, John!
greet('Jane', 'How are you?'); // 输出 Hello, Jane! How are you?

在上述代码中,message 是可选参数,调用函数时可以不传入该参数,此时它的值就是 undefined

函数还可以有默认参数值,当没有传入相应参数时,会使用默认值。例如:

function calculate(a: number, b: number = 10): number {
    return a + b;
}
let result1 = calculate(5); // 相当于 calculate(5, 10),result1 为 15
let result2 = calculate(5, 20); // result2 为 25

这里 b 有默认值 10,如果调用 calculate 函数时只传入一个参数,b 就会使用默认值,而不是 undefined

在对象和数组中的应用

对象属性

在对象中,属性的值可能为 nullundefined。例如:

interface User {
    name: string;
    age?: number;
}
let user: User = { name: 'Alice' };
// console.log(user.age.toFixed(2)); // 报错,因为 user.age 可能为 undefined
if (user.age!== undefined) {
    console.log(user.age.toFixed(2)); // 只有在 user.age 不为 undefined 时才调用 toFixed 方法
}

在上述代码中,User 接口中 age 属性是可选的,所以它可能为 undefined。在使用 user.age 时,需要进行 undefined 检查。

如果对象属性允许为 null,同样需要在类型声明中体现。例如:

interface Product {
    name: string;
    price: number | null;
}
let product: Product = { name: 'Book', price: null };
if (product.price!== null) {
    console.log(`The price is ${product.price}`);
} else {
    console.log('The price is not available.');
}

这里 Product 接口中 price 属性可以为 null,在使用 product.price 时,需要进行 null 检查。

数组元素

数组中的元素也可能是 nullundefined。例如:

let arr1: (number | null)[] = [1, null, 3];
let sum = 0;
for (let i = 0; i < arr1.length; i++) {
    if (arr1[i]!== null) {
        sum += arr1[i];
    }
}
console.log(sum); // 输出 4

在上述代码中,arr1 数组的元素类型为 number | null,在遍历数组计算总和时,需要对 null 值进行检查。

如果数组元素可能为 undefined,也类似。例如:

let arr2: (string | undefined)[] = ['a', undefined, 'c'];
let newArr = arr2.filter((element) => element!== undefined);
console.log(newArr); // 输出 ['a', 'c']

这里使用 filter 方法过滤掉了数组中的 undefined 元素。

类型保护与类型断言

类型保护

类型保护是一种 TypeScript 中的机制,用于在运行时检查变量的类型。在处理 nullundefined 时,类型保护非常有用。

常用的类型保护方法有 typeofinstanceof 以及自定义类型保护函数。

typeof 类型保护:

function printValue(value: string | number | null | undefined) {
    if (typeof value ==='string') {
        console.log(value.toUpperCase());
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2));
    } else if (value === null) {
        console.log('Value is null');
    } else {
        console.log('Value is undefined');
    }
}
printValue('hello'); // 输出 HELLO
printValue(10); // 输出 10.00
printValue(null); // 输出 Value is null
printValue(undefined); // 输出 Value is undefined

在上述代码中,通过 typeofvalue 进行类型判断,从而执行不同的操作。

自定义类型保护函数:

function isNumber(value: any): value is number {
    return typeof value === 'number';
}
function processValue(value: string | number | null | undefined) {
    if (isNumber(value)) {
        console.log(value.toFixed(2));
    } else {
        console.log('Not a number');
    }
}
processValue(10); // 输出 10.00
processValue('abc'); // 输出 Not a number

在这个例子中,isNumber 函数是一个自定义类型保护函数,它返回一个类型谓词 value is number,在 processValue 函数中可以基于此进行类型判断。

类型断言

类型断言是一种告诉编译器“相信我,我知道自己在做什么”的方式。在处理 nullundefined 时,类型断言可以在确定变量不是 nullundefined 的情况下,跳过类型检查。

非空断言操作符 !

let str1: string | null | undefined;
str1 = 'test';
let length1 = str1!.length; // 使用非空断言操作符,告诉编译器 str1 不为 null 或 undefined
console.log(length1); // 输出 4

需要注意的是,使用非空断言操作符时,如果实际上变量是 nullundefined,会导致运行时错误。所以只有在非常确定变量不为空的情况下才能使用。

类型断言语法 <type>valuevalue as type

let value: any = 'hello';
let str2 = <string>value;
let length2 = str2.length;
console.log(length2); // 输出 5

在这个例子中,将 any 类型的 value 断言为 string 类型,从而可以访问 length 属性。不过同样,如果 value 实际上不是 string 类型,会导致运行时错误。

在异步操作中的应用

Promise

在使用 Promise 进行异步操作时,nullundefined 也需要妥善处理。例如,假设有一个异步获取用户信息的函数:

function getUserInfo(): Promise<string | null> {
    return new Promise((resolve) => {
        setTimeout(() => {
            const random = Math.random();
            if (random > 0.5) {
                resolve('User information');
            } else {
                resolve(null);
            }
        }, 1000);
    });
}
getUserInfo().then((info) => {
    if (info!== null) {
        console.log(info);
    } else {
        console.log('User information not available');
    }
});

在上述代码中,getUserInfo 函数返回一个 Promise,其 resolve 值可能为 stringnull。在 then 回调中,需要对 null 值进行检查。

async/await

async/await 是基于 Promise 的更简洁的异步编程方式,同样需要处理 nullundefined。例如:

async function fetchData(): Promise<number | undefined> {
    const response = await fetch('https://example.com/api/data');
    if (!response.ok) {
        return undefined;
    }
    const data = await response.json();
    return data.value;
}
async function processData() {
    const result = await fetchData();
    if (result!== undefined) {
        console.log(`The result is ${result}`);
    } else {
        console.log('Data fetching failed');
    }
}
processData();

在这个例子中,fetchData 函数可能返回 undefined,在 processData 函数中使用 await 获取结果后,需要对 undefined 进行检查。

与其他类型的交互

联合类型与交叉类型

联合类型可以将 nullundefined 与其他类型组合。例如:

let value1: string | null;
value1 = 'text';
value1 = null;

这里 value1 可以是 string 类型或者 null 类型。

交叉类型通常不会直接与 nullundefined 组合,因为交叉类型是将多个类型合并为一个类型,而 nullundefined 通常不适合与其他类型进行这样的合并。不过在一些复杂的类型组合场景下,也可能间接涉及。例如:

interface A {
    prop1: string;
}
interface B {
    prop2: number;
}
let obj: (A & B) | null;
obj = null;
obj = { prop1: 'hello', prop2: 10 };

这里 obj 可以是 AB 交叉类型的对象,也可以是 null

泛型

在泛型中,nullundefined 同样需要考虑。例如,定义一个简单的泛型函数来获取数组中的第一个元素:

function getFirst<T>(arr: T[]): T | null {
    if (arr.length > 0) {
        return arr[0];
    }
    return null;
}
let arr3 = [1, 2, 3];
let first1 = getFirst(arr3);
if (first1!== null) {
    console.log(first1); // 输出 1
}
let arr4: string[] = [];
let first2 = getFirst(arr4);
if (first2!== null) {
    console.log(first2); // 不会输出,因为 first2 为 null
}

在上述代码中,getFirst 函数返回值类型为 T | null,因为数组可能为空,此时返回 null

常见错误与避免方法

空值引用错误

空值引用错误是在处理 nullundefined 时最常见的错误。例如:

let str3: string | null;
// console.log(str3.length); // 报错,str3 可能为 null
if (str3!== null) {
    console.log(str3.length);
}

为了避免这种错误,在使用可能为 nullundefined 的变量前,一定要进行检查。

类型不匹配错误

在类型声明和实际使用中,如果对 nullundefined 的处理不一致,会导致类型不匹配错误。例如:

function divide(a: number, b: number): number {
    return a / b;
}
// divide(10, null); // 报错,null 不能作为 number 类型参数

要避免这种错误,确保函数参数和返回值的类型声明准确反映可能出现的 nullundefined 情况。

错误的类型断言

错误的类型断言也会导致问题。例如:

let value2: any = null;
let str4 = <string>value2;
// console.log(str4.length); // 运行时错误,因为 value2 实际为 null

为了避免错误的类型断言,在进行断言前,要确保变量的实际类型符合断言的类型。

通过深入理解 nullundefined 在 TypeScript 中的区别与应用,开发者可以编写出更健壮、更少错误的前端代码。在实际项目中,合理处理 nullundefined 是提高代码质量和稳定性的关键环节。无论是在函数参数、对象属性、数组元素,还是在异步操作等场景中,都需要根据具体情况进行妥善处理,结合类型保护、类型断言等机制,确保代码在各种情况下都能正确运行。