JavaScript/TypeScript 中有很多语法细节看似简单,却极易混淆。这些“坑”往往是面试官的最爱,也是实际项目中 Bug 的常见来源。
下面我将它们分为 JavaScript 和 TypeScript 两部分,并附上解释和示例。
JavaScript 部分
1. 相等比较:== vs ===
这是最经典的一个坑。== 会进行类型转换后再比较(宽松相等),而 === 不会(严格相等)。
console.log(1 == '1'); // true (类型转换后相等)
console.log(1 === '1'); // false (类型不同)
// 更奇怪的例子
console.log(0 == false); // true (false转换为数字0)
console.log(0 === false); // false (类型不同:number vs boolean)
console.log('' == false); // true (空字符串和false转换为0后相等)
最佳实践: 几乎总是使用 === 和 !==,以避免不可预料的类型转换。
2. 变量声明:var vs let/const
var:函数作用域,会变量提升(hoisting),可重复声明。let/const:块级作用域({}内),存在暂时性死区(TDZ),不可重复声明。const用于声明常量,其引用不可更改。
// 作用域差异
if (true) {
var a = 'I am var';
let b = 'I am let';
}
console.log(a); // 'I am var' (var穿透了if块)
console.log(b); // ReferenceError: b is not defined
// 变量提升差异
console.log(x); // undefined (var被提升,但赋值未提升)
var x = 10;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;
最佳实践: 默认使用 const,需要重新赋值时使用 let,基本避免使用 var。
3. this 的指向
this 的值不是由函数定义的位置决定的,而是由调用方式决定的。这是最大的困惑点。
const obj = {
name: 'Alice',
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // 'Alice' (this指向obj)
const extractedFunc = obj.sayName;
extractedFunc(); // undefined (非严格模式下,this指向全局对象window/global)
// 解决方案:使用箭头函数或bind
const obj2 = {
name: 'Bob',
sayName: function() {
setTimeout(() => {
console.log(this.name); // 箭头函数不绑定自己的this,继承自外层obj2
}, 100);
}
};
obj2.sayName(); // 'Bob'
关键点: 普通函数的 this 是动态的,箭头函数的 this 是词法的(定义时即确定)。
4. 异步编程:回调、Promise、异步循环
- 回调地狱 (Callback Hell):多层嵌套回调,代码难以阅读和维护。
- Promise 链的错误处理:
.catch()会捕获链中上方所有的错误。
// 错误处理陷阱
someAsyncFunc()
.then(result1 => {
doSomething(result1);
return anotherAsyncFunc(); // 返回一个新的Promise
})
.then(result2 => {
// 这里的.catch能捕获then1和then2里的错误吗?
console.log(result2);
})
.catch(error => { // 是的!这个catch能捕获整个链中的任何错误
console.error('Something went wrong:', error);
});
// 异步循环:在循环中直接使用async/await可能不会按预期工作
const urls = ['url1', 'url2', 'url3'];
// 错误做法:这会同时发起所有请求,而不是顺序执行
urls.forEach(async (url) => {
await fetch(url);
});
// 正确做法:使用 for...of 循环
for (const url of urls) {
await fetch(url); // 会按顺序等待每个请求完成
}
5. 真假值判断 (Falsy Values)
在条件判断中,以下值会被当作 false:false, 0, -0, 0n, "", null, undefined, NaN。
const name = '';
if (name) {
// 这里不会执行,因为空字符串是falsy
console.log('Hello, ' + name);
}
const count = 0;
if (count) {
// 这里不会执行,因为0是falsy。如果你想检查0,这是个常见的坑!
console.log('Count is zero');
}
// 正确检查0的方法
if (count !== undefined && count !== null) { // 或者 if (count != null)
console.log('Count is provided:', count);
}
TypeScript 部分
1. type 和 interface 的区别
它们非常相似,常可互换,但有一些细微差别:
| 特性 | type (类型别名) | interface (接口) |
|---|---|---|
| 扩展 | 使用 & (交叉类型) | 使用 extends |
| 合并 | 不可合并 | 可声明合并(同名接口会自动合并) |
| 适用对象 | 能表达更广泛的类型(联合类型、元组、原始类型等) | 主要用于对象形状(Object Shape) |
// type 示例
type Name = string;
type Point = { x: number; y: number };
type ID = number | string; // 联合类型
// interface 示例
interface Point {
x: number;
y: number;
}
// 声明合并:编译器会自动将两个同名的Point接口合并
interface Point {
z?: number; // 添加一个可选属性
}
const myPoint: Point = { x: 1, y: 2, z: 3 }; // 合法
// type 无法做到声明合并
type Animal = { name: string };
type Animal = { age: number }; // Error: Duplicate identifier 'Animal'
最佳实践: 优先使用 interface 定义对象结构,直到你需要 type 的特定功能(如联合类型、元组)。
2. any vs unknown
any: 关闭类型检查。“我是任何类型,你可以对我做任何事”。应尽量避免使用。unknown: “我可能是任何类型,但你在使用我必须先证明它是什么类型”。更安全的any。
let anyValue: any = 'hello';
anyValue.foo.bar(); // TS不会报错,但运行时大概率会崩溃
let unknownValue: unknown = 'hello';
unknownValue.foo; // Error: Object is of type 'unknown'
// 使用前必须进行类型收窄 (Type Narrowing)
if (typeof unknownValue === 'string') {
console.log(unknownValue.toUpperCase()); // 现在安全了
}
最佳实践: 当你确实无法确定类型时,使用 unknown 而不是 any,迫使你自己进行类型检查。
3. 可选链 ?. 和 空值合并 ??
这两个操作符用于处理 null 或 undefined,极易混淆。
- 可选链
?.:如果前面的值是null或undefined,则表达式短路返回undefined。 - 空值合并
??:一个逻辑运算符,当左侧操作数为null或undefined时,返回其右侧操作数。
interface User {
name?: string;
address?: {
city?: string;
};
}
const user: User = {};
const city = user.address?.city; // 如果address为null/undefined,则city为undefined,不会报错
// 等价于 const city = (user.address === null || user.address === undefined) ? undefined : user.address.city;
// 空值合并 ??
const defaultValue = 'N/A';
const displayName = user.name ?? defaultValue; // 如果user.name是null/undefined,则使用'N/A'
console.log(displayName); // 'N/A'
// 与 || 的区别
const count = 0;
const result1 = count || 10; // 10 (因为0是falsy)
const result2 = count ?? 10; // 0 (??只判断null/undefined,不判断其他falsy值)
4. as 类型断言 和 ! 非空断言
类型断言是告诉编译器“你比我更清楚类型”。
as语法:明确指定一个值的类型。!非空断言:断言某个值一定不是null或undefined。慎用! 除非你 100% 确定。
// as 断言
const myElement = document.getElementById('my-input') as HTMLInputElement;
// 现在可以安全地访问.value属性
console.log(myElement.value);
// ! 非空断言
function liveDangerously(x?: number | null) {
console.log(x!.toFixed()); // 即使x可能是undefined或null,也用!断言它不为空
// 如果x确实是null/undefined,运行时将崩溃!
}
警告: 非空断言 ! 只是绕过了编译器的检查,并没有实际的运行时验证。滥用它是程序崩溃的根源。
总结
| 类别 | 易错点 | 关键理解/解决方案 |
|---|---|---|
| JS | == vs === | 总是用 === |
| JS | var vs let/const | 作用域和提升机制不同,用 let/const |
| JS | this 指向 | 由调用方式决定,箭头函数固定 this |
| JS | 异步处理 | Promise 链的错误捕获,for...of 处理异步循环 |
| JS | 真假值 | 记住 falsy 值列表,小心 0 和 "" |
| TS | type vs interface | interface 可合并,type 更灵活 |
| TS | any vs unknown | 用 unknown 更安全,需类型收窄 |
| TS | ?. 和 ?? | ?. 安全访问,?? 提供默认值(仅对 null/undefined) |
| TS | 类型断言 as 和 ! | 谨慎使用,尤其是 ! |
理解这些细节的区别,能帮助你写出更健壮、更可预测的代码,避免很多潜在的 Bug。 Happy Coding!