Skip to main content

语法

标识符

标识符指的是用来识别各种值(变量、函数、属性、类)的合法名称。

  • 第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(_
  • 第二个字符及后面的字符,除了上述字符外,还可以用数字 0-9
const $el = document.querySelector("#app"); // $ 开头合法,常见于 jQuery/DOM 引用
const _private = 1; // _ 开头常表示"内部使用"
const camelCase = 1; // 驼峰是 JS 主流命名风格
const 2foo = 2; // ❌ SyntaxError:数字不能开头
const if = 1; // ❌ SyntaxError:不能使用保留字

JavaScript 有一些保留字(如 classletreturnenumawait 等),不能用作标识符。完整列表见 MDN 保留字

变量

声明

  • let。声明块级作用域的变量,可重新赋值
  • const。声明常量绑定,声明后不能再重新赋值;但对对象/数组等引用类型,其内部属性仍可修改(若想冻结需用 Object.freeze()
  • var。声明函数作用域(全局作用域)的变量,已被 let/const 取代,仅在维护老代码时遇到
const obj = { a: 1 };
obj.a = 2; // ✅ 允许(修改属性)
obj = {}; // ❌ TypeError: Assignment to constant variable(不能重新绑定)

const frozen = Object.freeze({ a: 1 });
frozen.a = 2; // ⚠️ 静默失败(严格模式下抛 TypeError)

提升与暂时性死区(TDZ)

var 声明的变量会经历"提升(Hoisting)":在声明语句之前就能访问,值为 undefinedlet / const 也会被"提升",但不会初始化——从块级作用域开始到声明语句之间的区域就是暂时性死区(TDZ),访问会抛 ReferenceError

// var:声明提升 + 初始化为 undefined
console.log(a); // undefined
var a = 1;

// let/const:仅提升"绑定",未初始化前处于 TDZ
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 1;

console.log(c); // ReferenceError
const c = 1; // const 还必须在声明时立即赋值

选型建议

场景选择原因
默认情况const让"不可变"成为承诺,最易重构
需要重新赋值(循环计数、累加器)letconst 多一次写权限
维护 IE10- 老代码var 兜底新项目不推荐使用

Q&A

下面的输出是什么,如何修改才能正确输出 0-4?

for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 输出: 5, 5, 5, 5, 5

修改方案 1:将 var 替换为 let(利用块级作用域)。

for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}

原理解析(伪代码): 为什么 let 可以,而 var 不行? 因为 var 是函数作用域/全局作用域,循环 5 次实际上修改的是同一个内存地址的 i。而 let 是块级作用域,JS 引擎在每次循环时,都会为 let 重新创建一个新的词法环境(Lexical Environment),把当前的 i 值保存下来供闭包使用。引擎底层的处理逻辑类似于下面的伪代码:

// var 的底层情况:全局只有一个 i
var i = 0;
setTimeout(() => console.log(i), 0);
i = 1;
setTimeout(() => console.log(i), 0);
// ...当所有 setTimeout 真正执行时,i 早就变成了 5

// let 的底层情况:每次循环都生成了独立的作用域块
{
let i = 0;
setTimeout(() => console.log(i), 0);
}
{
let i = 1;
setTimeout(() => console.log(i), 0);
}
{
let i = 2;
setTimeout(() => console.log(i), 0);
}
// ...当 setTimeout 执行时,它们沿着各自的块级作用域链向上找,找到了当时被锁住的独立变量 i

修改方案 2:使用 IIFE(立即调用的函数表达式)形成闭包。

for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, 0);
})(i);
}

关键字

按用途分组(详细分类见 MDN 关键字与保留字):

  • 声明类varletconstfunctionclassasync functionfunction*(生成器)
  • 控制流ifelseswitchforwhiledobreakcontinuereturnthrowtrycatchfinally
  • 对象/上下文thissupernewdeleteininstanceoftypeofvoid
  • 异步asyncawait
  • 模块importexport
  • 其他yielddebuggerwith(严格模式禁用)

面试高频:箭头函数与普通函数的区别

在 ES6 中引入了箭头函数(Arrow Function),它不仅仅是普通函数的语法糖,在底层行为上有极其严格的区别。这是前端面试中几乎必问的考点。

特性普通函数 (function)箭头函数 (() => {})
this 的指向动态绑定。由调用方式决定(如作为对象方法调用、call/applynew 等)。静态绑定(词法作用域)。没有自己的 this,始终继承自外层执行环境的 this,且永远无法被 call/apply/bind 改变
arguments 对象有自己的 arguments 类数组对象,包含所有传入的参数。没有 arguments 对象。如果要获取不定参数,必须使用剩余参数(...args)。
作为构造函数 (new)可以被 new 调用,生成实例对象。不能被 new 调用,会抛出 TypeError。因为箭头函数没有 [[Construct]] 方法和 prototype 属性。
prototype 属性prototype 属性。没有 prototype 属性
yield 关键字可以作为 Generator 函数使用(function*)。不能用作 Generator 函数,内部不能使用 yield 关键字。

常见陷阱题:

const obj = {
name: "Jackson",
sayHi: () => {
console.log(this.name);
},
};
obj.sayHi(); // 输出什么?

答案:输出 undefined(或全局的 name)。因为对象字面量 {} 不产生作用域,箭头函数的 this 会向上寻找到全局作用域(浏览器中是 window),而不是 obj。如果要在对象方法中使用 this,必须使用普通函数。

表达式

表达式是有返回值的代码单元。常见分类:

  • 基本表达式:字面量(1"abc"true)、标识符(x)、this
  • 属性访问obj.aobj["a"]
  • 对象/数组初始化{}[]
  • 正则表达式字面量/ab+c/i
  • 模板字符串`Hello, ${name}`(支持插值与标签模板)
  • 解构赋值const [a, b] = [1, 2]const { x, y } = obj
  • 箭头函数表达式() => x * 2
  • async / 生成器表达式async () => 1function* () { yield 1 }
  • 展开 / 剩余[...arr, 4, 5](...args) => args
  • 一元 / 二元 / 三元运算表达式
  • 逗号表达式(a, b) 返回最后一个值
  • new 表达式new Foo()

运算符

优先级总表(高 → 低)

优先级运算符说明
19()[]..?.new Foo(args)() 函数调用成员访问与函数调用
18++--!+(单目)、-(单目)、typeofvoiddeleteawait一元运算
17**幂运算(右结合)
16*/%乘除模
15+(双目)、-(双目)加减
14<<>>>>>移位
13<<=>>=ininstanceof比较
12==!====!==相等
11&按位与
10^按位异或
9\|按位或
8&&逻辑与
7\|\|??逻辑或 / 空值合并(不可混用,需括号隔离)
6?:三元
5=+=-=**=\|\|=&&=??=赋值
4...(展开)数组/对象展开
1,逗号

速记口诀:单目 > 算术 > 比较 > 相等 > 位运算 > 逻辑 > 三元 > 赋值 > 展开 > 逗号。掌握到这个粒度,90% 的优先级题都能过。

常用运算符分类

  • 算术:+-*/%**
  • 赋值:=、复合赋值(+=-=*=/=%=**=
  • 自增减:++--(注意前缀 vs 后缀的求值时机)
  • 比较:<<=>>===!====!==
  • 逻辑:&&||!??
  • 三元:?:
  • 展开 / 剩余:...

+ 的两重身份

  • 算术加法
  • 字符串拼接(任一侧为字符串就走拼接):5 + """5"
  • 单目 +Number() 的语法糖:+'5'5+false0+null0+undefinedNaN

&& / || / ?? 的短路与陷阱

// || 短路:左侧为 falsy 时返回右侧
let name = a || "default"; // 注意:0 / "" 也会被替换

// ?? 空值合并:仅在左侧为 null / undefined 时返回右侧
0 ?? 10; // 0 (0 不是 null/undefined)
"" ?? "x"; // "" (同上)
null ?? "x"; // "x"

// ⚠️ ?? 不能与 || 或 && 直接混用,必须用括号隔离
// a || b ?? c → SyntaxError
// (a || b) ?? c ✅

现代运算符速览(ES2020+)

运算符用途引入版本
?. 可选链a?.b?.c 中途遇 null/undefined 就停下,不抛错ES2020
?? 空值合并仅在左侧为 null/undefined 时返回右侧ES2020
\|\|=&&=??= 逻辑赋值a ??= b 等价于 a = a ?? bES2021
** 幂运算2 ** 10 === 1024ES2016
数字分隔符1_000_000ES2021
// 可选链
const city = user?.address?.city; // 中间任意一环为 null/undefined 都返回 undefined

// 逻辑赋值(少写一行,性能也更优)
let count = 0;
count ??= 10; // 0 ?? 10 = 0,count 不变
count ||= 10; // 0 || 10 = 10,count 变 10

展开 vs 剩余(同一个 ...

  • 展开(Spread):把"可迭代对象 / 对象"拆成独立元素
  • 剩余(Rest):把"零散元素"收集成数组 / 对象
// 展开
const arr2 = [...arr1, 4, 5];
const obj2 = { ...obj1, x: 1 };

// 剩余
function f(...args) {
/* args 是数组 */
}
const { a, ...rest } = obj;

new. 的优先级(面试陷阱)

new 表达式根据括号位置有两种解析方式:

// 形式 A:new 后面跟一个带括号的函数调用 → 整体优先级高
new Foo().getName(); // 等价于 (new Foo()).getName()

// 形式 B:new 后面跟一个成员访问 → 成员访问优先级高
new Foo.getName(); // 等价于 new (Foo.getName)()

本质是运算符优先级:new Foo(args) 优先级高(带括号),而 new 单目(无括号)优先级低于 .

以上只是罗列了一些常用表达式和运算符,更详细的参考 表达式和运算符

语句

  • 条件:if / else if / elseswitch
  • 循环:forwhiledo-while
  • 遍历:for...in(遍历对象的,含原型链)、for...of(遍历可迭代对象的)、for await...of(异步迭代器)
  • 跳转:breakcontinuereturnthrow
  • 标签:label(配合 break/continue 实现多层跳出)
  • 异常捕获:try / catch / finally
  • 调试:debugger(触发断点,前提是 DevTools 已打开)
// for...of:遍历值(Array / String / Map / Set / Generator)
for (const v of [1, 2, 3]) console.log(v); // 1 2 3

// for...in:遍历键(普通对象慎用,会带原型链上的可枚举属性)
for (const k in { a: 1, b: 2 }) console.log(k); // a b

// for await...of:异步迭代
for await (const chunk of fetchStream()) {
console.log(chunk);
}

面试高频:for...in vs for...of

  • for...in 拿到的是(字符串),可枚举属性都会被遍历,原型链上的属性也算上
  • for...of 拿到的是,要求对象实现了 Symbol.iterator
  • 遍历对象请用 Object.keys() / Object.entries() + for...of,避开 for...in 的原型链陷阱

注释

  • 单行:// comment
  • 多行:/* comment */
  • JSDoc:/** 函数/类型注释 */,被 VSCode / TSC 解析,可以提供类型提示与静态检查
// 单行注释
/* 多行注释 */

/**
* 计算两数之和
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}