JS/TS 容易混淆的语法细节

57 阅读6分钟

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)

在条件判断中,以下值会被当作 falsefalse, 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. typeinterface 的区别

它们非常相似,常可互换,但有一些细微差别:

特性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. 可选链 ?. 和 空值合并 ??

这两个操作符用于处理 nullundefined,极易混淆。

  • 可选链 ?.:如果前面的值是 nullundefined,则表达式短路返回 undefined
  • 空值合并 ??:一个逻辑运算符,当左侧操作数为 nullundefined 时,返回其右侧操作数。
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 语法:明确指定一个值的类型。
  • ! 非空断言:断言某个值一定不是 nullundefined慎用! 除非你 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 ===总是用 ===
JSvar vs let/const作用域和提升机制不同,用 let/const
JSthis 指向由调用方式决定,箭头函数固定 this
JS异步处理Promise 链的错误捕获,for...of 处理异步循环
JS真假值记住 falsy 值列表,小心 0""
TStype vs interfaceinterface 可合并,type 更灵活
TSany vs unknownunknown 更安全,需类型收窄
TS?.???. 安全访问,?? 提供默认值(仅对 null/undefined)
TS类型断言 as!谨慎使用,尤其是 !

理解这些细节的区别,能帮助你写出更健壮、更可预测的代码,避免很多潜在的 Bug。 Happy Coding!