跳转到内容
关系图谱

01 JavaScript

4,005 字 14 分钟

属性 1
series
Vue3.5 + Electron + 大模型跨平台实战
  • JavaScriptJS)是一种具有函数优先特性的轻量级、解释型或者说即时编译型的编程语言。
  • JavaScript 是一种基于原型、多范式、单线程动态语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。
  • JavaScript 的动态特性包括运行时对象的构造、变量参数列表、函数变量、动态脚本创建(通过 eval)、对象内枚举(通过 for...inObject 工具方法)和源代码恢复(JavaScript 函数会存储其源代码文本,可以使用 toString() 进行检索)。
  • JavaScript 标准是 ECMAScript 语言规范(ECMA-262)和 ECMAScript 国际化 API 规范(ECMA-402)。
  • 不要将 JavaScript 与 Java 编程语言弄混——JavaScript *不是*“解释型的 Java”。“Java”和“JavaScript”都是 Oracle 公司在美国和其他国家注册(或未注册)的商标。然而,这两门编程语言有着非常不同的语法、语义和用途。

什么是 JavaScript

  • JavaScript 是一门跨平台、面向对象的脚本语言,它能使网页可交互(例如,拥有复杂的动画、可点击的按钮、弹出菜单等)。
  • 还有一些更高级的服务器端 JavaScript 版本,如 Node.js,它们允许你为网站添加添加更多功能,而不仅仅是下载文件(例如,多台计算机之间的实时协作)。在宿主环境(例如 Web 浏览器)中,可以将 JavaScript 和宿主环境的对象连接起来,并以编程的方式控制这些对象。
  • JavaScript 包含一个对象标准库(例如,ArrayDateMath)和一套核心语言元素(例如,运算符、控制结构和语句)。可以通过用额外的对象实现各种目的对核心 JavaScript 进行扩展;例如:
    • 客户端 JavaScript 通过提供控制浏览器及其文档对象模型(DOM)的对象对核心语言进行扩展。例如:客户端扩展允许应用程序将元素放在某个 HTML 表单中,并且支持响应用户事件,比如鼠标点击、表单输入和页面导航。
    • 服务器端 JavaScript 则通过提供和在服务器上运行 JavaScript 有关的对象对核心语言进行扩展。例如:服务器端扩展支持应用程序和数据库通信,提供应用不同调用间的信息连续性,或者在服务器上执行文件操作。

JavaScript 和 Java

  • JavaScript 语言类似 Java 但是并没有 Java 的静态类型和强类型检查。
  • JavaScript 遵循大部分 Java 表达式语法、命名规范以及基础控制流结构,这也是 JavaScript 从 LiveScript 更名的原因。
  • 与 Java 通过声明的方式构建类的编译时系统不同,JavaScript 支持的是基于少量的数据类型(表示数字、布尔、字符串值的数据类型)的运行时系统。JavaScript 的对象模型是基于原型的,而不是基于更常见的类。基于原型的模型提供了动态继承;也就是说,每个对象继承的内容可以不同。JavaScript 支持不需要特殊声明要求的函数。函数也可以作为对象的属性被当做松散类型的方法执行。
  • 与 Java 相比,Javascript 是一门形式自由的语言。你不必声明所有的变量、类和方法。你不必关心方法是否是公有、私有或者受保护的,也不需要实现接口。无需显式指定变量、参数、方法返回值的类型。
  • Java 是基于类的编程语言,设计的初衷就是为了确保快速执行和类型安全。类型安全,举个例子,你不能将一个 Java 整数变量转化为一个对象引用,或者通过破坏 Java 字节码访问私有内存。Java 基于类的模型,意味着程序包含专有的类及其方法。Java 的类继承和强类型通常要求紧耦合的对象层级。这些要求使得 Java 编程比 JavaScript 编程要复杂的多。
  • 相比之下,JavaScript 传承了 HyperTalk 和 dBASE 语句精简、动态类型等精髓。这些脚本语言由于其更简单的语法、专门的内置功能以及对象创建的最小要求,为更广泛的受众提供了编程工具。
JavaScriptJava
面向对象。不区分对象类型。继承通过原型机制实现,可以向对象动态地添加属性和方法。基于类。对象分为类和实例,所有的继承通过类层级实现。不能向类和实例动态地添加属性或方法。
不需要声明变量数据类型(动态类型,松散类型)。必须声明变量数据类型(静态类型,强类型)。
不能自动写入硬盘。可以自动写入硬盘。

JavaScript 和 ECMAScript 规范

  • JavaScript 的标准化组织是 Ecma 国际——这个欧洲标准化信息与通信系统协会提供基于 Javascript 的标准化、国际化编程语言(ECMA 原先是欧洲计算机制造商协会的首字母缩写)。
  • 这个名为 ECMAScript 的 JavaScript 标准化版本,在所有支持该标准的应用程序中以相同的方式工作。公司可以使用开放标准语言来开发其 JavaScript 实现。ECMAScript 标准的文档位于 ECMA-262 规范中。

语法和数据类型

基础语法

  • JavaScript 的大部分语法借鉴自 Java、C 和 C++,也受到 Awk、Perl 和 Python 的影响。
  • JavaScript 区分大小写,并使用 Unicode 字符集(frühFrüh 是两个不同的变量)。
  • 指令被称为语句,用分号(;)分隔;语句独占一行时分号可省略(ASI 自动分号补全),但始终在语句末尾写分号是最佳实践,可减少代码出问题的可能性。
  • 源文本从左往右扫描,被转换为 token、控制字符、行终止符、注释和空白字符组成的输入元素序列。

注释

  • // 单行注释;/* */ 多行注释,块注释不能嵌套(注释内出现 */ 会提前结束注释)。
  • 第三种注释:hashbang 注释(如 #!/usr/bin/env node),只出现在文件开头,用于指定执行脚本的 JavaScript 引擎路径。
  • 注释的行为类似空白字符,执行时被忽略。

声明

  • 三种变量声明方式:
    • var:声明一个变量,可选初始化;非块级作用域(属于函数或全局作用域)。
    • let:声明块级作用域的局部变量,可选初始化。
    • const:声明块级作用域的只读命名常量,必须初始化。
  • 标识符规则:以字母、下划线(_)或美元符号($)开头,后续字符还可以是数字;也可使用大部分 Unicode 字母。
  • 支持解构声明:const { bar } = foo
  • 给未声明的变量赋值会创建未声明的全局变量,严格模式下报错,应彻底避免。
  • 声明与初始化器:let x = 42let x声明= 42初始化器var / let 未初始化时值为 undefinedconst 缺少初始化器直接 SyntaxError
js
let x // 只有声明,没有初始化器
console.log(x) // undefined —— 未初始化的变量默认值为 undefined

let y = 42 // “let y” 是声明,“= 42” 是初始化器
console.log(y) // 42

// const 缺少初始化器直接报语法错误(解析期就失败,无法被 try/catch 捕获)
// const z // SyntaxError: Missing initializer in const declaration

变量作用域与提升

  • 作用域分为全局、函数(局部)与块级:let / const 受块语句限制;var 无视块,只属于所在函数或全局作用域。
  • 变量提升(hoisting)var 只有声明(并默认初始化为 undefined)被提升,赋值不提升——声明前访问得到 undefined
  • let / const 在声明前访问会抛出 ReferenceError:从块开始到声明处的区间是「暂时性死区」(TDZ)。
  • 函数声明整体提升——可在其作用域内任何位置安全调用。
  • 全局变量实际是全局对象的属性:网页中是 windowwindow.variable);跨运行时统一用 globalThis
js
if (true) {
  var x = 5 // var 无视块,属于所在函数(或全局)作用域
  let y = 6 // let 被限制在块语句内
}

console.log(x) // 5 —— 块外仍能访问

try {
  console.log(y)
} catch (e) {
  console.log(`${e.name}: ${e.message}`) // ReferenceError: y is not defined
}
js
console.log(x === undefined) // true —— 声明(并默认初始化为 undefined)被提升,赋值不提升
var x = 3

;(function () {
  console.log(x) // undefined —— 函数内的 var x 声明被提升到函数顶部,遮蔽了外层 x
  var x = '局部值'
})()
js
try {
  console.log(x) // 从块开始到声明处是“暂时性死区”,访问即抛错
  const x = 3
} catch (e) {
  console.log(`${e.name}: ${e.message}`) // ReferenceError: Cannot access 'x' before initialization
}

// 函数声明是整体提升:声明之前也能安全调用
console.log(add(1, 2)) // 3
function add(a, b) {
  return a + b
}

常量

  • const 仅阻止重新赋值不阻止修改:被赋值为常量的对象属性、数组元素仍然可变(MY_OBJECT.key = "其他值"MY_ARRAY.push(...) 都合法)。
  • 同一作用域中,不能用与变量名或函数名相同的名字声明常量。
js
const PI = 3.14

try {
  PI = 3.14159 // const 禁止重新赋值
} catch (e) {
  console.log(`${e.name}: ${e.message}`) // TypeError: Assignment to constant variable.
}
js
// const 仅阻止“重新赋值”,不阻止“修改”
const MY_OBJECT = { key: '值' }
MY_OBJECT.key = '其他值' // 对象属性不受保护
console.log(MY_OBJECT) // { key: '其他值' }

const MY_ARRAY = ['HTML', 'CSS']
MY_ARRAY.push('JAVASCRIPT') // 数组元素同样不受保护
console.log(MY_ARRAY) // [ 'HTML', 'CSS', 'JAVASCRIPT' ]

数据结构和类型

  • 最新 ECMAScript 标准定义了 8 种数据类型:7 种基本类型 + Object。
    • Boolean:truefalse
    • null:表示空值的特殊关键字(大小写敏感,不是 Null / NULL)。
    • undefined:值未定义。
    • Number:整数或浮点数,如 423.14159
    • BigInt:任意精度整数,如 9007199254740992n
    • String:字符串,如 "Howdy"
    • Symbol:实例唯一且不可变的类型。
    • Object。
  • 函数从技术上讲也是一种对象:对象是存放值的命名容器,函数是程序能够执行的过程。

动态类型与类型转换

  • JavaScript 是动态类型语言:声明变量时不必指定类型,执行期间按需自动转换(同一个变量可以先赋数字再赋字符串,不会报错)。
  • + 运算符中数字遇到字符串时,数字被转换成字符串"37" + 7 // "377";其他运算符不会转换:"37" - 7 // 30"37" * 7 // 259
  • 字符串转数字:
    • parseInt():只返回整数;最佳实践是总是带上进制参数,如 parseInt("101", 2) // 5
    • parseFloat():解析浮点数。
    • 一元 + 运算符:(+"1.1") + (+"1.1") // 2.2
js
// “+” 运算符中数字遇到字符串:数字被转换成字符串
console.log('答案是 ' + 42) // 答案是 42
console.log(42 + ' 是答案') // 42 是答案
console.log('37' + 7) // 377

// 其他运算符不会把数字转成字符串,而是把字符串转成数字
console.log('37' - 7) // 30
console.log('37' * 7) // 259
js
// parseInt 最佳实践:总是带上进制参数
console.log(parseInt('101', 2)) // 5 —— 按二进制解析
console.log(parseInt('101', 10)) // 101 —— 按十进制解析
console.log(parseFloat('3.14')) // 3.14

// 一元 “+” 也能把字符串转为数字
console.log('1.1' + '1.1') // 1.11.1 —— 字符串拼接
console.log(+'1.1' + +'1.1') // 2.2 —— 先各自转成数字再相加

字面量

  • 数组字面量 [...]:每次求值都创建新数组;连续逗号会留下空槽(empty,与 undefined 不同,遍历方法如 map 会跳过空槽,但索引访问返回 undefined);只有最后一个尾逗号被忽略;书写时应显式写 undefined 或用注释标出缺失元素。
  • 布尔字面量truefalse(不要与 Boolean 包装对象混淆)。
  • 数字字面量:整数与 BigInt 可用十进制、十六进制(0x)、八进制(0o)、二进制(0b)表示,BigInt 加后缀 n;浮点数至少一位数字,且必须带小数点或指数部分(e / E),如 3.1E+12.1e-23
  • 对象字面量 {...}:不能出现在语句开头({ 会被解析为语句块);属性名不是合法标识符或数字时必须加引号,且只能用方括号 [] 访问;增强的对象字面量支持 __proto__ 设置原型、foo: foo 同名简写、方法简写、super 调用和计算属性名。
  • RegExp 字面量:由正斜杠围成,如 const re = /ab+c/;
  • 字符串字面量:由成对的双引号或单引号括起;可直接调用 String 对象的所有方法(JavaScript 自动转换为临时字符串对象,用完即弃);模板字面量用反引号包围,支持多行字符串与 ${} 插值,带标签的模板是函数调用的语法糖;特殊字符用反斜杠转义(\n\t\uXXXX\u{XXXXX} 等),反斜杠还可转义引号、反斜杠自身与换行。
js
const fish = ['Lion', , 'Angel'] // 连续逗号会留下空槽(empty)
console.log(fish) // [ 'Lion', <1 empty item>, 'Angel' ]
console.log(fish[1]) // undefined —— 索引访问返回 undefined
console.log(fish.map((x) => x.toUpperCase())) // 空槽被 map 等遍历方法跳过

const myList = ['home', , 'school', ,] // 只有最后一个尾逗号被忽略
console.log(myList.length) // 4
js
const handler = () => 'handled'

const obj = {
  handler, // “handler: handler” 赋值的简写
  greet() {
    // 方法简写(省略 function 关键字)
    return 'hello'
  },
  ['prop_' + (() => 42)()]: 42, // 计算(动态的)属性名
}

console.log(obj.handler()) // handled
console.log(obj.greet()) // hello
console.log(obj.prop_42) // 42
js
// 字符串插值
const name = 'Lev'
const time = 'today'
console.log(`你好 ${name},${time} 过得怎么样?`)

// 多行字符串:模板字面量可以跨行,单双引号包裹的字符串不行
console.log(`第一行
第二行`)

// 带标签的模板:函数调用的语法糖
const tag = (strings, ...values) =>
  strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), '')
console.log(tag`合计 ${1 + 2} 元`) // 合计 3 元

流程控制与错误处理

块语句

  • 最基本的语句,用一对花括号 { ... } 组合多条语句,通常配合控制流语句(ifforwhile)使用。
  • var 声明的变量不是块级作用域:块内的 var x 与块外的 var x 是同一个变量,设置效果会泄漏到块外(C / Java 中等效代码则是两个变量);用 let / const 可消除这个作用域效果。
js
var x = 1
{
  var x = 2 // var 不是块级作用域:与块外的 x 是同一个变量
}
console.log(x) // 2 —— 在 C / Java 中等效代码会输出 1

let y = 1
{
  let y = 2 // let 在块内是独立的变量,消除了这个作用域效果
}
console.log(y) // 1

条件语句:if...else

  • if 在条件为真时执行语句,可选的 else 子句在条件为假时执行;else if 按顺序测试多个条件,只有第一个求值为真的分支会执行
  • 最佳实践:
    • 总是使用块语句,尤其是嵌套 if 的时候。
    • 不要把赋值(x = y)当条件——看起来像手误的 ==,易读性差。
  • 6 个假值(falsy)falseundefinednull0NaN""(空字符串);其他一切都为真——包括所有对象。
  • 陷阱:不要混淆原始布尔值与 Boolean 包装对象——new Boolean(false) 是对象,条件里恒为,但 b == true 按原始值比较为假。
js
// 6 个假值:false、undefined、null、0、NaN、""(空字符串)
const falsy = [false, undefined, null, 0, NaN, '']
console.log(falsy.filter(Boolean)) // [] —— 全部为假

// 其他一切都为真,包括所有对象(空数组、空对象也是)
if ([] && {} && '0') {
  console.log('对象、非空字符串都是真值')
}

// 陷阱:Boolean 包装对象是“对象”,条件里恒为真
const b = new Boolean(false)
console.log(b ? 'b 为真' : 'b 为假') // b 为真
console.log(b == true ? 'b == true' : 'b != true') // b != true —— 比较时按原始值 false

条件语句:switch

  • switch 求表达式的值并与 case 标签匹配(严格相等);匹配成功执行关联语句,无匹配则找 default 子句(惯例放最后,但位置不限)。
  • 每个 case 关联一个可选的 break:执行后跳出 switch省略 break 会继续贯穿(fallthrough)执行下一个 case 的语句
js
function price(fruitType) {
  switch (fruitType) {
    case 'Oranges':
      console.log('橙子是 $0.59 一磅')
      break
    case 'Bananas':
      console.log('香蕉是 $0.48 一磅')
      break // 去掉这个 break,会继续“贯穿”执行 Cherries 分支
    case 'Cherries':
      console.log('樱桃是 $3.00 一磅')
      break
    default:
      console.log(`对不起,${fruitType} 卖完了`)
  }
}

price('Bananas') // 香蕉是 $0.48 一磅
price('Durian') // 对不起,Durian 卖完了

异常处理

异常类型

  • JavaScript 可以 throw 任意值(字符串、数字、布尔、对象都行),但使用专门为此设计的异常类型通常更高效:
    • ECMAScript 异常Error 及其子类(RangeErrorReferenceErrorSyntaxErrorTypeError 等)。
    • DOMException:Web API 抛出的异常。

throw 与 try...catch

  • throw expression; 抛出异常,指明要抛出的值。
  • try...catchtry 块和 catch 块组成:try 块中抛出异常时,控制立即转移到 catch 块;没有异常则跳过 catch 块。
  • catch (e) 的标识符存储被抛出的值,只在 catch 块存续期间可用
  • 调试建议:catch 块中输出错误用 console.error() 而不是 console.log()——它会格式化为错误并加入页面的错误消息列表。

finally 块

  • finallytrycatch 之后、try...catch...finally 语句后面的代码之前执行;无论是否抛异常都会执行(即使没有 catch 块)。
  • 典型用途:让脚本在异常发生时优雅退出,释放绑定的资源(如关闭文件)。
  • ⚠️ 如果 finally 块有返回值,该值会覆盖整个流程的返回值——不管 try / catchreturn 了什么,甚至会覆盖 catch 中抛出或重新抛出的异常。

嵌套与 Error 对象

  • try...catch 可以嵌套;内部 try 块没有对应 catch 时必须有 finally,异常交由外层的 catch 处理。
  • 抛自定义异常时用 Error 构造函数,之后可通过 e.name(错误类名)和 e.message(比错误对象转字符串更简明)获取信息。
js
function getMonthName(mo) {
  const months = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
  if (months[mo - 1]) return months[mo - 1]
  throw new Error('无效的月份数值') // 用 throw 抛出异常
}

let monthName
try {
  monthName = getMonthName(13) // 可能抛出异常的函数
} catch (e) {
  monthName = '未知'
  console.error(e.message) // 调试时建议用 console.error 而非 console.log
} finally {
  console.log('无论是否抛异常,finally 都会执行') // 常用于释放资源(如关闭文件)
}

console.log(monthName) // 未知
js
function f() {
  try {
    console.log(0)
    throw new Error('bogus')
  } catch (e) {
    console.log(1)
    return true // 这个 return 会被挂起,直到 finally 块结束
  } finally {
    console.log(3)
    return false // 覆盖前面的 return true(也会覆盖 catch 中重新抛出的异常)
  }
}

console.log(f()) // 依次输出 0、1、3、false
js
function doSomethingErrorProne() {
  throw new Error('这里出了点问题') // 自定义异常用 Error 构造函数抛出
}

try {
  doSomethingErrorProne()
} catch (e) {
  console.error(e.name) // Error —— 常规的错误类名
  console.error(e.message) // 这里出了点问题 —— 比整个错误对象转字符串更简明
}

循环和迭代

  • 循环把一个动作重复很多次(重复次数甚至可能为 0),各种循环机制的差异在于如何确定循环的开始和结束
  • JavaScript 提供的循环语句:fordo...whilewhilelabelbreakcontinuefor...infor...of

for 语句

  • 语法:for (initialization; condition; afterthought) statement
  • 执行顺序:初始化 → 判断条件 → 循环体 → 更新表达式 → 回到判断条件
  • 初始化表达式通常用于初始化循环计数器,也可以声明变量;condition 整个省略时视为 true

while 与 do...while

  • while (condition) statement先判断后执行——条件检测发生在每次执行之前,条件为假时循环体一次都不执行。
  • do statement while (condition);先执行后判断——语句在检查条件之前先执行一次,因此至少执行一次
  • ⚠️ 避免无限循环:保证循环条件最终会变成假,while (true) 这类条件永远不会为假的循环永不停止。
js
// for (初始化; 条件; 更新表达式) —— 执行顺序:初始化 → 判断条件 → 循环体 → 更新 → 回到判断
for (let step = 0; step < 5; step++) {
  console.log('向东走一步') // 运行 5 次,step 值从 0 到 4
}
js
// while:条件检测发生在每次执行“之前”
let n = 0
let x = 0
while (n < 3) {
  n++
  x += n
  console.log(`n = ${n}, x = ${x}`)
}
// n = 1, x = 1
// n = 2, x = 3
// n = 3, x = 6 —— 之后 n < 3 不再为真,循环终止

// ⚠️ 避免无限循环:保证条件最终会变成假
// while (true) { console.log('你好,世界!') }
js
// do...while:语句在检查条件“之前”先执行一次
let i = 0
do {
  i += 1
  console.log(i) // 1 2 3 4 5
} while (i < 5)

// 与 while 的关键区别:先执行后判断,至少执行一次
let j = 100
do {
  console.log(`j = ${j},条件一开始就为假,也执行了一次`)
} while (j < 5)

label、break 与 continue

  • label: statement:给语句一个标识符,供 break / continue 引用;值是任何非保留字的合法标识符。
  • break
    • 不带 label:立即终止当前所在while / do...while / for / switch,控制权交回其后面的语句。
    • 带 label(break label;):终止指定的带标记语句——多层循环中可一次跳出全部。
  • continue
    • 不带 label:跳过本轮循环体的剩余部分,继续下一次循环(不是退出循环)。
    • 带 label(continue label;):结束内层本轮迭代,跳到 label 标识的循环继续下一次迭代。
  • 经典对比(10×10 双层循环在 i === 5 && j === 5 时中断):裸 break 计数 95break label 计数 55continue label 计数 95——后者与裸 break 结果相同,但多层循环下流程更明晰。
js
// 不带 label 的 continue:跳过本轮剩余代码,直接进入下一次循环
let i = 0
let n = 0
while (i < 5) {
  i++
  if (i === 3) continue // i 为 3 时跳过累加
  n += i
  console.log(n) // 1 3 7 12
}
js
// 不带 label 的 break:只跳出当前(内层)循环
let num = 0
for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    if (i === 5 && j === 5) break // 只跳出 j 循环,i 循环继续
    num++
  }
}
console.log(num) // 95

// break label:跳出 label 标识的整个(外层)循环
num = 0
outerBreak: for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    if (i === 5 && j === 5) break outerBreak // 跳出所有循环
    num++
  }
}
console.log(num) // 55

// continue label:结束内层本轮,跳到 label 标识的循环继续下一次迭代
num = 0
outerContinue: for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    if (i === 5 && j === 5) continue outerContinue
    num++
  }
}
console.log(num) // 95 —— 结果与不带 label 的 break 相同,但多层循环时流程更明晰

for...in 与 for...of

  • for (variable in object):遍历对象所有可枚举属性(键),为每个不同的属性执行一次语句。
  • ⚠️ 不要用 for...in 迭代数组:它返回的除了数字索引,还可能有自定义属性名;迭代数组应使用带数字索引的传统 for 循环、for...offorEach()
  • for (variable of object):在可迭代对象ArrayMapSetarguments 等)上循环,迭代的是
  • 一句话区分:for...in 遍历(索引/属性名),for...of 遍历(元素本身)。
js
const arr = [3, 5, 7]
arr.foo = 'hello' // 给数组添加一个自定义属性

// for...in 遍历“键”:除数字索引外,自定义属性也会被遍历到
for (const i in arr) {
  console.log(i) // 0 1 2 foo
}

// for...of 遍历“值”:只迭代可迭代对象的元素,没有 foo
for (const v of arr) {
  console.log(v) // 3 5 7
}

函数

  • 函数类似于过程——一组执行任务或计算值的语句;但要成为函数,这个过程应该接受输入并返回与输入存在某些明显关系的输出。
  • 要使用函数,必须将其定义在希望调用它的作用域内。

定义函数

  • 函数声明(函数语句):function 关键字 + 函数名 + 参数列表 + 花括号包裹的函数体;return 语句指定返回值。
  • 参数本质上按值传递:函数内给参数赋新值,不会反映到外部;但传对象 / 数组时,函数内修改其属性 / 元素对外部可见。
  • 函数表达式:函数可以由表达式创建,可以是匿名的;也可以提供名称——具名函数表达式的名字可在函数内部自指(递归)、并在调试器堆栈跟踪中识别该函数。
  • 函数表达式便于把函数作为参数传给另一个函数(如自实现的 map(f, a))。
  • 可以根据条件定义函数;也可以在运行时用 Function 构造函数从字符串创建函数(类似 eval())。
  • 当函数是一个对象的属性时,称之为方法
js
// 参数本质上是“按值”传递:函数内给参数赋新值,不影响外部
function reassign(n) {
  n = 100
}
let num = 1
reassign(num)
console.log(num) // 1

// 但传对象/数组时,函数内修改“属性/元素”对外部可见
function upgrade(theObject) {
  theObject.make = 'Toyota'
}
const mycar = { make: 'Honda', model: 'Accord', year: 1998 }
upgrade(mycar)
console.log(mycar.make) // Toyota

function replaceFirst(theArr) {
  theArr[0] = 30
}
const arr = [45]
replaceFirst(arr)
console.log(arr[0]) // 30
js
// 函数声明:整体提升,声明之前也能调用
console.log(square(5)) // 25
function square(n) {
  return n * n
}

// 函数表达式:不提升,声明前访问会抛错
try {
  cube(3)
} catch (e) {
  console.log(`${e.name}: ${e.message}`) // ReferenceError: Cannot access 'cube' before initialization
}
const cube = (x) => x * x * x

// 具名函数表达式:名字只在函数内部可用,用于自指(递归)与调试堆栈识别
const factorial = function fac(n) {
  return n < 2 ? 1 : n * fac(n - 1)
}
console.log(factorial(3)) // 6

调用函数与函数提升

  • 定义函数不会自动执行它;调用才会以给定参数真正执行。
  • 函数必须处于调用它的作用域中,但函数声明可以被提升(整体提升,可出现在调用语句之后);函数提升仅适用于函数声明,不适用于函数表达式(表达式声明前访问抛 ReferenceError)。
  • 函数本身就是对象,因此也有方法:call()apply() 可用于动态调用、指定运行时上下文。
  • 递归:函数可以调用自身(如阶乘);递归近似于循环——都重复执行代码、都需要终止条件。递归本身就使用了函数栈(先进后出);有些算法(如遍历 DOM 树)用递归远比迭代容易。
js
// 递归函数:调用自身,需要终止条件(避免无限递归)
function factorial(n) {
  return n === 0 || n === 1 ? 1 : n * factorial(n - 1)
}
console.log(factorial(5)) // 120

// 递归本身就使用了栈(函数栈):先进后出
function foo(i) {
  if (i < 0) return
  console.log(`开始:${i}`)
  foo(i - 1)
  console.log(`结束:${i}`)
}
foo(3)
// 开始:3 开始:2 开始:1 开始:0
// 结束:0 结束:1 结束:2 结束:3

函数作用域与闭包

  • 函数内定义的变量在函数之外不可访问;函数可以访问自己作用域内以及父函数有权访问的任何变量和函数。
  • 嵌套(内部)函数对其容器(外部)函数是私有的,并形成闭包:内部函数可以“继承”容器函数的参数和变量。
  • 保存变量:闭包必须保存它可见作用域中的所有参数和变量——每次调用外部函数实际上重新创建了一遍闭包;只有返回的内部函数不再被引用时内存才会释放。
  • 多层嵌套 → 作用域链:闭包递归地包含所有包含它的函数作用域;反向不成立(外层无法访问内层的变量)。
  • 命名冲突:同名时更近的作用域优先级更高(链的第一个元素是最里面的作用域)。
  • 闭包的典型应用:为内部变量提供封装——除了暴露的方法,没有任何途径能碰到内部数据(“保险柜”)。
  • ⚠️ 陷阱:闭包内定义与外部同名的变量后,就无法再引用外部作用域中的那个变量(内部遮蔽外部)。
js
// 嵌套(内部)函数形成闭包:可以“继承”容器函数的参数和变量
function outside(x) {
  function inside(y) {
    return x + y
  }
  return inside
}

const fnInside = outside(3) // 一个“把传入值加 3”的函数——x 被闭包保存了下来
console.log(fnInside(5)) // 8
console.log(outside(3)(5)) // 8

// 命名冲突:更近的作用域优先(作用域链)
function outer() {
  const x = 5
  function inner(x) {
    return x * 2 // inner 的参数 x 遮蔽了 outer 的 x
  }
  return inner
}
console.log(outer()(10)) // 20(而不是 10)
js
// 闭包封装:内部变量只能通过暴露的方法访问,像“保险柜”
const createPet = function (name) {
  let sex
  return {
    setName(newName) {
      name = newName
    },
    getName() {
      return name
    },
    setSex(newSex) {
      if (typeof newSex === 'string' && ['male', 'female'].includes(newSex.toLowerCase())) {
        sex = newSex
      }
    },
    getSex() {
      return sex
    },
  }
}

const pet = createPet('Vivie')
console.log(pet.getName()) // Vivie
pet.setName('Oliver')
pet.setSex('male')
console.log(pet.getSex()) // male
console.log(pet.getName()) // Oliver

// IIFE + 闭包:保护不希望外部修改的数据
const getCode = (function () {
  const apiCode = '0]Eal(eh&2'
  return function () {
    return apiCode
  }
})()
console.log(getCode()) // 0]Eal(eh&2

arguments 对象与函数参数

  • 函数的实参保存在类数组arguments 对象中:arguments[i] 按序访问、arguments.length 是实参个数——但它不具备数组的操作方法。
  • 用途:处理比声明更多的参数(事先不知道会传入多少参数时)。
  • 默认参数:参数默认值本是 undefinedfunction multiply(a, b = 1) 在函数头直接设定默认值,替代函数体内的 typeof b !== "undefined" 手动检查。
  • 剩余参数...theArgs 把不定数量的参数收集成真正的数组(可直接用 map 等数组方法)。
js
// arguments 是“类数组”:有索引和 length,但没有数组方法
function myConcat(separator) {
  let result = ''
  for (let i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator
  }
  return result
}

// 可以传入比声明更多的参数
console.log(myConcat('、', '红', '橙', '蓝')) // 红、橙、蓝、
console.log(myConcat(';', '大象', '长颈鹿', '狮子')) // 大象;长颈鹿;狮子;
js
// 过去:在函数体内手动检查 undefined
function multiplyOld(a, b) {
  b = typeof b !== 'undefined' ? b : 1
  return a * b
}
console.log(multiplyOld(5)) // 5

// 默认参数:直接在函数头设定
function multiply(a, b = 1) {
  return a * b
}
console.log(multiply(5)) // 5

// 剩余参数:把不定数量的参数收集成真正的数组
function multiplyAll(multiplier, ...theArgs) {
  return theArgs.map((x) => multiplier * x)
}
console.log(multiplyAll(2, 1, 2, 3)) // [ 2, 4, 6 ]

箭头函数

  • 箭头函数表达式(“胖箭头”)相比函数表达式语法更短,且没有自己的 thisargumentssupernew.target;箭头函数总是匿名的。
  • 引入的两个动机:更简洁的函数a.map((s) => s.length))和 this 的无绑定性
  • 箭头函数出现之前,每个新函数都定义自己的 this 值(构造函数中是新对象、严格模式下是 undefined、对象方法中指向该对象……),回调里经常拿不到预期的 this——过去要靠 const self = this;bind() 修复。
  • 箭头函数使用封闭执行上下文的 this,回调场景不再需要 self/bind 补丁。
js
const person = {
  name: 'Alice',
  friends: ['Bob', 'Carol'],
  printRegular() {
    this.friends.forEach(function (f) {
      // 普通函数有“自己的 this”(这里不指向 person),拿不到 name
      console.log(`${this?.name} 的朋友:${f}`)
    })
  },
  printArrow() {
    this.friends.forEach((f) => {
      // 箭头函数没有自己的 this,沿用封闭执行上下文(printArrow)的 this
      console.log(`${this.name} 的朋友:${f}`)
    })
  },
}

person.printRegular() // undefined 的朋友:Bob / Carol
person.printArrow() // Alice 的朋友:Bob / Carol

预定义函数

JavaScript 的顶级内置函数:

函数作用
eval()执行以字符串表示的 JavaScript 代码
isFinite()判断传入值是否是有限数值(必要时先转为数值)
isNaN()判断一个值是否是 NaN(内部强制转换规则需注意,推荐用 Number.isNaN()
parseFloat()解析字符串参数,返回浮点数
parseInt()解析字符串参数,返回指定基数的整数
encodeURI() / decodeURI()对 URI 进行编码 / 解码
encodeURIComponent() / decodeURIComponent()对 URI 组件进行编码 / 解码
escape() / unescape()已弃用,用 encodeURI(Component) / decodeURI(Component) 代替

索引集合

  • 按索引值排序的数据集合:数组和类数组结构(ArrayTypedArray)。
  • 数组是由名称和索引引用的值构成的有序列表;JavaScript 没有明确的数组数据类型,用预定义的 Array 对象处理。

创建数组与 length

  • 三种等效创建:new Array(...)Array(...)、字面量 [...]——字面量更短,通常是首选
  • ⚠️ 单个数字传给 Array() 会被解释为 arrayLength 而非元素:Array(42) 是长度 42 的空数组,[42] 才是单元素数组;Array(9.3)(非整数)抛 RangeErrorArray.of(9.3) 可安全创建单元素数组。
  • 在实现层面,元素就是标准对象属性、索引就是属性名;非整数“索引”会成为对象属性而不是元素,不计入 length
  • length 总是比最大索引大 1(cats[30] = ...length 31),且可写:写小值截断数组、写 0 清空、写大值拉长(产生空槽)。
js
// Array(数字) 是“长度”,不是元素——单元素数字数组必须用字面量
const a = Array(42)
console.log(a.length, a[0]) // 42 undefined
const b = [42]
console.log(b.length, b[0]) // 1 42
console.log(Array.of(9.3)) // [ 9.3 ] —— Array.of 不会把数字当长度

// length 可写:截断、清空、拉长
const cats = ['Dusty', 'Misty', 'Twiggy']
cats.length = 2
console.log(cats) // [ 'Dusty', 'Misty' ] —— Twiggy 被移除
cats.length = 0
console.log(cats) // []
cats.length = 3
console.log(cats) // [ <3 empty items> ]

// 非整数“索引”不是元素,而是对象属性,不计入 length
const arr = []
arr[3.4] = 'Oranges'
console.log(arr.length, Object.hasOwn(arr, 3.4)) // 0 true

常用数组方法

  • 增删:push(尾加)/ pop(尾删)/ shift(头删)/ unshift(头加)/ splice(任意位置删除并替换,原地修改)。
  • 取值:slice(提取片段返回新数组)/ at(支持负索引从末尾数)/ indexOf / lastIndexOf
  • 变换:concat(连接)/ join(拼成字符串)/ reverse(原地颠倒)/ flat(展平)/ sort(原地排序,可接受比较函数:负数 a 在前、正数 b 在前、0 保持原序)。
js
const arr = ['1', '2', '3']
console.log(arr.concat('a', 'b')) // 连接并返回新数组
console.log(arr.join(' - ')) // 1 - 2 - 3
arr.push('4') // 尾部添加,返回新 length
console.log(arr.pop()) // 4 —— 移出尾部并返回
console.log(arr.shift()) // 1 —— 移出头部并返回
arr.unshift('0') // 头部添加
console.log(arr) // [ '0', '2', '3' ]

const letters = ['a', 'b', 'c', 'd', 'e']
console.log(letters.slice(1, 4)) // [ 'b', 'c', 'd' ] —— 提取片段,原数组不变
console.log(letters.at(-2)) // d —— 负索引从末尾数

const nums = ['1', '2', '3', '4', '5']
console.log(nums.splice(1, 3, 'a', 'b')) // [ '2', '3', '4' ] —— 返回被删元素,原地修改
console.log(nums) // [ '1', 'a', 'b', '5' ]

console.log([1, 2, [3, 4]].flat()) // [ 1, 2, 3, 4 ] —— 展平嵌套数组
console.log(['Wind', 'Rain', 'Fire'].sort()) // [ 'Fire', 'Rain', 'Wind' ] —— 默认字典序
// sort 接受比较函数(负数 a 在前 / 正数 b 在前 / 0 保持原序):按最后一个字母排序
console.log(['Wind', 'Rain', 'Fire'].sort((a, b) => a.at(-1).localeCompare(b.at(-1)))) // [ 'Wind', 'Fire', 'Rain' ]

迭代方法

  • 接受回调的方法称为迭代方法,都接受可选的第二参数 thisArgsort() 例外——它不是迭代方法)。
  • map / flatMap(map + 深度 1 的 flat)、filterfind / findLast / findIndex / findLastIndexevery(全部满足)/ some(至少一个)、forEach
  • reduce / reduceRight:把数组元素两两递归处理成单个值;指定 initialValue 时从它开始累积。
js
const a = ['a', 10, 'b', 20, 'c', 30]
const isNumber = (v) => typeof v === 'number'

console.log(a.filter(isNumber)) // [ 10, 20, 30 ] —— 所有令回调为真的元素
console.log(a.find(isNumber)) // 10 —— 第一个令回调为真的元素
console.log(a.findLast(isNumber)) // 30 —— 最后一个
console.log(a.findIndex(isNumber)) // 1 —— 第一个的索引
console.log(a.findLastIndex(isNumber)) // 5 —— 最后一个的索引

console.log(['a', 'b', 'c'].map((x) => x.toUpperCase())) // [ 'A', 'B', 'C' ]
console.log(['a', 'b'].flatMap((x) => [x, x.toUpperCase()])) // [ 'a', 'A', 'b', 'B' ] —— map + 深度 1 的 flat

console.log([1, 2, 3].every(isNumber)) // true —— 全部满足
console.log([1, '2', 3].every(isNumber)) // false
console.log([1, '2', 3].some(isNumber)) // true —— 至少一个满足

// reduce:把数组两两递归处理成单个值
console.log([10, 20, 30].reduce((acc, cur) => acc + cur, 0)) // 60

稀疏数组与类数组对象

  • 空槽的五种产生方式:Array(5)、字面量连续逗号、越界赋值、拉长 lengthdelete 元素。
  • 行为分裂:索引访问 / for...of / 展开把空槽当 undefined迭代方法(forEachfilter 等)与 Object.keys 跳过空槽;手动赋值 undefined 的元素不会被跳过。
  • 数组可嵌套(多维数组),也可以像对象一样存储额外属性(如 exec() / match() 返回的数组带匹配信息)。
  • 类数组对象argumentsNodeList)有 length 和索引但没有数组方法;可用 Array.prototype.forEach.call(arrayLike, ...) 间接调用。
js
const arr = [1, 2, , , 5] // 稀疏数组(含空槽)

// 这些操作把空槽当作 undefined
console.log(arr[2]) // undefined
for (const i of arr) console.log(i) // 1 2 undefined undefined 5
console.log([...arr]) // [ 1, 2, undefined, undefined, 5 ]

// 迭代方法跳过空槽
arr.forEach((i) => console.log(i)) // 1 2 5
console.log(arr.filter(() => true)) // [ 1, 2, 5 ]
console.log(Object.keys(arr)) // [ '0', '1', '4' ]

// 手动赋值 undefined 的元素“不会”被跳过
const dense = ['first', 'second', undefined, 'fourth']
dense.forEach((el) => console.log(el)) // first second undefined fourth

带键的集合

Map

  • Map 是键/值对映射集合,可按插入顺序遍历;核心 API:set / get / has / delete / clear / sizefor...of 每次迭代得到 [key, value] 数组。
  • 相比 Object 用作映射的优势:键可以是任意类型(Object 的键只能是字符串或 Symbol)、有 size、可直接迭代、无原型带来的默认键干扰。
  • 选择建议:键在运行时才知道、所有键同类型所有值同类型、需要频繁增删键 → 用 Map;有针对单个元素的逻辑 → 用 Object
  • WeakMap:键必须是对象(或非注册 symbol),且不对键创建强引用——键对象被垃圾回收后对应值也随之回收;不可枚举(枚举会引入对 GC 状态的依赖)。用例:为对象存储私有数据 / 隐藏实现细节。
js
const sayings = new Map()
sayings.set('dog', 'woof')
sayings.set('cat', 'meow')
sayings.set('elephant', 'toot')

console.log(sayings.size) // 3
console.log(sayings.get('dog')) // woof
console.log(sayings.get('fox')) // undefined
console.log(sayings.has('bird')) // false

sayings.delete('dog')
for (const [key, value] of sayings) {
  console.log(`${key} goes ${value}`) // cat goes meow / elephant goes toot —— 按插入顺序
}

sayings.clear()
console.log(sayings.size) // 0

// 键值相等基于 SameValueZero:NaN 也能当键(与 === 不同),+0 与 -0 视为相等
const m = new Map()
m.set(NaN, '找到我了')
console.log(m.get(NaN)) // 找到我了

Set

  • Set一组唯一值的集合,按添加顺序遍历,值只能出现一次;核心 API:add / has / delete / size
  • 数组 ⇆ Set 转换:Array.from(set)[...set]new Set(array)——转换时重复值自动删除(常用来去重)。
  • 相比数组的优势:hasindexOf 快(哈希查找)、删除元素不需要 spliceNaN 也能被正确查找。
  • WeakSet:只能存对象(或非注册 symbol)、弱引用不可枚举;用例:标记 / 跟踪 DOM 元素而不泄漏内存。
  • Map / Set 的键值相等基于 SameValueZero 算法:与 === 基本一致,但 NaN 等于自身、+0-0 相等。
js
const mySet = new Set()
mySet.add(1)
mySet.add('some text')
mySet.add('foo')

console.log(mySet.has(1)) // true
mySet.delete('foo')
console.log(mySet.size) // 2
for (const item of mySet) console.log(item) // 1 / some text —— 按添加顺序

// 数组 ⇆ Set:转换时自动去重
const dedup = new Set([1, 2, 2, 3, 3, 3])
console.log([...dedup]) // [ 1, 2, 3 ] —— 展开语法
console.log(Array.from(dedup)) // [ 1, 2, 3 ] —— Array.from

使用对象

对象和属性

  • 对象是一系列属性的集合,属性包含名和值;值为函数的属性称为方法
  • 两种访问方式:点符号 obj.prop;方括号 obj["prop"](对象因此也叫“关联数组”)。非法标识符属性名(含空格、连字符、数字开头)只能用方括号,方括号也支持运行时动态判定的属性名。
  • 方括号中的所有键都会转换为字符串(对象键只能是 String 或 Symbol)。
  • 未赋值的属性值为 undefined(而不是 null)。
  • 枚举属性的三种原生方法:for...in(自身 + 原型链上的可枚举属性)、Object.keys(o)(自身可枚举)、Object.getOwnPropertyNames(o)(自身全部,无论是否可枚举)。
js
const myCar = {}
myCar.make = 'Ford' // 点访问
myCar['model'] = 'Mustang' // 方括号访问(对象有时被叫作“关联数组”)
myCar.year = 1969
console.log(myCar.noProperty) // undefined —— 未赋值的属性是 undefined(不是 null)

// 非法标识符属性名只能用方括号;方括号里的键都会转成字符串
const myObj = {}
myObj['date created'] = '带空格的属性名'
myObj[''] = '空字符串也可以'
myObj[Math.random()] = '动态键'
console.log(Object.keys(myObj).length) // 3

// 枚举属性的三种原生方法
console.log(Object.keys(myCar)) // [ 'make', 'model', 'year' ] —— 自身可枚举属性
console.log(Object.getOwnPropertyNames(myCar)) // 自身所有属性(无论是否可枚举)
for (const key in myCar) console.log(key) // 自身 + 原型链上的可枚举属性

创建对象的三种方式

  • 对象初始化器(字面量):{ prop: value, ... }——每次遇到该表达式都会创建新对象。
  • 构造函数 + new:函数名首字母大写是惯例;用 this 把传入值赋给对象属性;属性值可以是另一个对象;通过 Constructor.prototype.x 可为该类型所有对象增加属性。
  • Object.create():直接为新对象指定原型对象,无需定义构造函数。
  • 所有 JavaScript 对象至少继承于一个对象——被继承者称为原型(prototype)。
js
// 方式一:对象初始化器(字面量)
const myHonda = { color: 'red', wheels: 4, engine: { cylinders: 4, size: 2.2 } }
console.log(myHonda.engine.size) // 2.2

// 方式二:构造函数 + new(约定首字母大写)
function Car(make, model, year, owner) {
  this.make = make
  this.model = model
  this.year = year
  this.owner = owner
}
const rand = { name: 'Rand McKinnon', age: 33 }
const car2 = new Car('Nissan', '300ZX', 1992, rand)
console.log(car2.owner.name) // Rand McKinnon —— 属性值可以是另一个对象

// 方式三:Object.create —— 直接指定原型对象,无需构造函数
const Animal = {
  type: 'Invertebrates',
  displayType() {
    console.log(this.type)
  },
}
const fish = Object.create(Animal)
fish.type = 'Fishes'
fish.displayType() // Fishes

getter / setter、删除与比较

  • getter 获取属性值、setter 设置属性值:在字面量中用 get prop() / set prop(x) 定义(getter 无参数、setter 恰一个参数);也可以随时用 Object.defineProperty / Object.defineProperties 给已有对象补加。
  • delete 操作符可删除非继承属性。
  • 对象是引用类型:两个独立创建的同形对象 == / === 都是 false;只有比较同一个对象的引用才为 true
js
// 对象字面量中定义 getter / setter
const o = {
  a: 7,
  get b() {
    return this.a + 1
  },
  set c(x) {
    this.a = x / 2
  },
}
console.log(o.a) // 7
console.log(o.b) // 8 —— getter 像属性一样读取
o.c = 50 // setter 像属性一样赋值
console.log(o.a) // 25

// 也可以随时用 Object.defineProperties 给已有对象补加
const p = { a: 0 }
Object.defineProperties(p, {
  b: {
    get() {
      return this.a + 1
    },
  },
  c: {
    set(x) {
      this.a = x / 2
    },
  },
})
p.c = 10
console.log(p.b) // 6
js
// 对象是引用类型:两个独立创建的同形对象永不相等
const fruit = { name: 'apple' }
const fruitbear = { name: 'apple' }
console.log(fruit == fruitbear) // false
console.log(fruit === fruitbear) // false

// 只有比较同一个对象的引用时才为 true
const ref = fruit
console.log(fruit === ref) // true

// delete 可删除非继承属性
delete fruit.name
console.log(fruit) // {}

使用类

类的本质与声明

  • JavaScript 是基于原型的语言,类可以看作已有原型继承机制的一种抽象——所有语法都可转换为原型继承;类本身也是普通的 JavaScript 值。
  • 类体可包含:构造函数 constructor、实例字段、实例方法、静态字段 / 静态方法 / 静态块,以及以 # 开头的私有形式。
  • 与函数声明不同,类声明不会被提升(有 TDZ 限制,行为类似 let / const);类也有表达式形式,表达式的名字只在类体内可见。
  • 类的“调用”必须使用 new,否则 TypeError;构造函数中 this 指向新实例并自动作为 new 的结果返回(不建议手动返回对象——会顶替 this)。
js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b]
  }
  getRed() {
    return this.values[0]
  }
  get red() {
    // 访问器字段:像“实际属性”一样读写
    return this.values[0]
  }
  set red(value) {
    this.values[0] = value
  }
  static isValid(r, g, b) {
    // 静态方法定义在类本身上,不在实例上
    return [r, g, b].every((v) => v >= 0 && v <= 255)
  }
}

const red = new Color(255, 0, 0)
console.log(red.getRed()) // 255
red.red = 0
console.log(red.red) // 0

// 实例方法定义在原型(Color.prototype)上,所有实例共享同一个函数
console.log(new Color(0, 0, 0).getRed === new Color(1, 1, 1).getRed) // true

console.log(Color.isValid(255, 0, 0)) // true
console.log(Color.isValid(1000, 0, 0)) // false
console.log(new Color(0, 0, 0).isValid) // undefined —— 实例上没有静态方法

// 类的“调用”必须使用 new
try {
  Color(1, 2, 3)
} catch (e) {
  console.log(`${e.name}: ${e.message}`) // TypeError: Class constructor Color cannot be invoked without 'new'
}

实例方法、访问器与静态属性

  • 实例方法定义在原型上(如 Color.prototype),所有实例共享同一个函数(在构造函数里用 this.fn = ... 则每个实例都新建一份)。
  • 访问器字段get red() / set red(v) 让方法像“实际属性”一样读写;只有 getter 没有 setter 时字段只读(严格模式下赋值抛 TypeError,非严格模式静默忽略)。
  • 公共字段luckyNumber = ...)几乎等价于在构造函数里给 this 赋属性。
  • 静态属性定义在类本身上,实例上不存在(new Color().isValid === undefined);静态初始化块在类第一次加载时运行,且可访问静态私有元素。

私有字段

  • # 开头,必须先在类体中声明才能引用;永远不会与公共字段命名冲突。
  • 硬私有:类外访问是早期 SyntaxError(静态分析即可发现),没有任何机制能从类外碰到它——放心重构内部结构,只要暴露方法的行为不变。
  • 类方法可以访问同类其他实例的私有字段;访问不存在的私有字段抛错(不是返回 undefined),可用 #field in obj 检查。
  • 限制:同名不能声明两次、不能 delete;方法、getter/setter 也可以是私有的。
  • 封装的价值:入口处校验(如 setRed 拒绝 1000)、防止用户依赖内部数据结构(如 RGB → HSL 重构不破坏 API)。
js
class Color {
  #values // 私有字段必须先在类体中声明,以 # 开头
  constructor(r, g, b) {
    this.#values = [r, g, b]
  }
  getRed() {
    return this.#values[0]
  }
  setRed(value) {
    if (value < 0 || value > 255) throw new RangeError('无效的 R 值') // 封装:入口处校验
    this.#values[0] = value
  }
  redDifference(anotherColor) {
    if (!(#values in anotherColor)) throw new TypeError('期望 Color 实例') // in 检查私有字段是否存在
    return this.#values[0] - anotherColor.#values[0] // 可访问“同类其他实例”的私有字段
  }
}

const red = new Color(255, 0, 0)
console.log(red.getRed()) // 255

try {
  red.setRed(1000)
} catch (e) {
  console.log(`${e.name}: ${e.message}`) // RangeError: 无效的 R 值
}

const crimson = new Color(220, 20, 60)
console.log(red.redDifference(crimson)) // 35

// 类外访问 red.#values 是“早期语法错误”(SyntaxError),代码根本无法运行

扩展与继承

  • 派生类用 extends 声明,可访问父类所有公共属性;访问 this 之前必须先调用 super()(它调用父类构造函数初始化 this)。
  • 派生类继承父类全部方法(含静态方法),可覆盖(如 toString())并用 super.method() 在避免重复的前提下增强父类逻辑。
  • 派生类无权访问父类的私有字段——硬私有的作用范围严格限制在声明它的类体内。
  • 一个类至多扩展一个父类(避免菱形问题);多重继承效果可用类组合与混入(mixin)模拟。
  • 派生类实例同时也是父类实例:color instanceof Colorcolor instanceof ColorWithAlpha 都为 true
  • 何时用类:想要储存内部数据 + 暴露大量方法时(如内置的 Map / Date / Error 家族);注意可变性与深层继承树带来的维护成本。
js
class Color {
  #values
  constructor(r, g, b) {
    this.#values = [r, g, b]
  }
  get red() {
    return this.#values[0]
  }
  toString() {
    return this.#values.join(', ')
  }
  static isValid(r, g, b) {
    return [r, g, b].every((v) => v >= 0 && v <= 255)
  }
}

class ColorWithAlpha extends Color {
  #alpha
  constructor(r, g, b, a) {
    super(r, g, b) // 访问 this 之前必须先调用 super()
    this.#alpha = a
  }
  toString() {
    return `${super.toString()}, ${this.#alpha}` // super 调用父类方法:增强而不重复
  }
  static isValid(r, g, b, a) {
    return super.isValid(r, g, b) && a >= 0 && a <= 1 // 静态方法同样被继承
  }
}

const color = new ColorWithAlpha(255, 0, 0, 0.5)
console.log(color.red) // 255 —— 继承了父类的访问器
console.log(color.toString()) // 255, 0, 0, 0.5 —— 覆盖并增强了 toString
console.log(ColorWithAlpha.isValid(255, 0, 0, -1)) // false
console.log(color instanceof Color) // true —— 派生类实例同时也是父类实例
console.log(color instanceof ColorWithAlpha) // true

使用 Promise

链式调用

  • Promise 代表一个异步操作的最终完成或失败;回调函数是附加到 Promise 上的(.then()),而不是作为参数传入函数——这解决了回调地狱。
  • then() 返回一个新的 Promise:回调返回普通值则传给下一个 then;返回 Promise 则等待其兑现后把兑现值传下去。
  • ⚠️ 务必在 then 回调中 return 你启动的 Promise:忘记 return 会产生“漂浮”的 Promise——无法追踪其敲定状态,有竞态时下一个 then提前调用、读到不完整的值。
  • 经验法则:链保持扁平化,不要嵌套;catch(cb) 等同于 then(null, cb),错误处理统一放链尾。
  • async / await 让代码几乎与同步版本一样(并发语义与 Promise 链相同:await 只挂起依赖其值的部分)。
js
// 把 setTimeout 封装进 Promise:旧式回调 API 的标准包装法
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms))

// then() 返回一个“新的 Promise”,因此可以链式衔接;catch 统一收尾
wait(10, 1)
  .then((result) => result * 2) // 返回普通值:传给下一个 then
  .then((result) => wait(10, result + 1)) // 返回 Promise:等待其兑现后再继续(务必 return!)
  .then((result) => {
    console.log(`得到最终结果:${result}`) // 3
    // 在 catch 之后还能继续链式操作
    return Promise.resolve()
      .then(() => {
        throw new Error('有哪里不对了')
        console.log('执行「这个」') // 不可达 —— throw 导致拒绝
      })
      .catch(() => console.log('执行「那个」'))
      .then(() => console.log('执行「这个」,无论前面发生了什么'))
  })
  .catch((e) => console.error(e))
js
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms))

// async/await:与同步代码几乎一样的写法,错误处理回归 try/catch
async function main() {
  try {
    const a = await wait(10, 1)
    const b = await wait(10, a + 1)
    console.log(`得到最终结果:${b}`) // 2
    throw new Error('有哪里不对了')
  } catch (e) {
    console.log(`执行「那个」:${e.message}`)
  }
  console.log('执行「这个」,无论前面发生了什么')
}

main()
// await 挂起时不会停止整个程序,其他异步任务仍可运行

错误处理

  • 一遇到异常抛出,就顺着链寻找下一个 .catch()(或 onRejected)——工作方式与同步代码的 try...catch 高度对称;async / await 下直接用 try...catch
  • 嵌套可限制 catch 的作用域:内层 catch 只捕获其作用域内的失败(可选步骤失败不影响主链继续),外层错误不会被内层吞掉——细粒度错误恢复。
  • catch 之后可以继续链式操作(失败恢复后接着做)。
  • 未处理的拒绝会冒泡到调用栈顶部:浏览器派发 unhandledrejection / rejectionhandled 全局事件;Node.js 用 process.on("unhandledRejection", ...) 捕获。

组合与时序

  • 四个并发组合工具:
    • Promise.all:全部兑现才兑现,任何一个拒绝立即拒绝
    • Promise.allSettled:等所有 Promise 敲定(无论成败),逐个给出状态。
    • Promise.any:第一个兑现的获胜。
    • Promise.race:第一个敲定(无论成败)的获胜。
  • 顺序执行可用 reduce 把函数数组折叠成 Promise 链,或 for...of + await;但除非确有依赖,优先并发
  • new Promise(executor) 从零创建 Promise 只应在封装旧式回调 API 时使用(如把 setTimeout 包成 wait(ms)),避免同步/异步混叫的“Zalgo 状态”。
  • 时序保证:传给 then() 的函数永远不会被同步调用(即使 Promise 已解决)——Promise 回调进微任务队列,setTimeout 回调进任务队列,微任务先于下一个任务执行。
js
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms))
const fail = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms))

// Promise.all:并发启动,全部兑现才兑现;任何一个拒绝则立即拒绝
Promise.all([wait(10, 1), wait(20, 2), wait(5, 3)]).then(([r1, r2, r3]) => {
  console.log('all:', r1, r2, r3) // all: 1 2 3
})

// Promise.allSettled:等所有 Promise 敲定(无论成败),逐个给出状态
Promise.allSettled([wait(10, 'ok'), fail(10, 'boom')]).then((results) => {
  console.log('allSettled:', results.map((r) => r.status)) // [ 'fulfilled', 'rejected' ]
})

// Promise.race:第一个敲定的获胜
Promise.race([wait(50, '慢'), wait(10, '快')]).then((winner) => {
  console.log('race:', winner) // race: 快
})
js
// Promise 回调是“微任务”,setTimeout 回调是“任务”——微任务先于下一个任务执行
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

wait().then(() => console.log(4)) // 任务(setTimeout)之后的微任务,最后执行
Promise.resolve()
  .then(() => console.log(2)) // 微任务:当前同步代码退出后立刻执行
  .then(() => console.log(3))
console.log(1) // 同步代码最先执行

// 输出顺序:1 2 3 4 —— 传给 then() 的函数“永远不会被同步调用”,即使 Promise 已经解决

迭代器和生成器

迭代器

  • 迭代器是实现了迭代器协议的对象:next() 方法返回 { value, done }——value 是序列下一个值,donetrue 时迭代结束(此时的 value 是迭代器的返回值)。
  • 迭代一个迭代器称为消耗它——通常只能执行一次;产生终值后再调 next() 应持续返回 { done: true }
  • 与数组的关键差异:数组必须完整分配,迭代器按需分配——因此迭代器可以表示无限序列。
js
// 迭代器协议:实现 next() 方法,返回 { value, done }
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start
  let iterationCount = 0
  return {
    next() {
      if (nextIndex < end) {
        const result = { value: nextIndex, done: false }
        nextIndex += step
        iterationCount++
        return result
      }
      return { value: iterationCount, done: true } // done 时的 value 是迭代器的“返回值”
    },
  }
}

const it = makeRangeIterator(1, 10, 2)
let result = it.next()
while (!result.done) {
  console.log(result.value) // 1 3 5 7 9
  result = it.next()
}
console.log(`已迭代序列的大小:${result.value}`) // 5

// 迭代器按需分配,可以表示无限序列(数组则必须完整分配)

生成器函数与可迭代对象

  • 生成器函数function*)是自定义迭代器的强大替代:无需显式维护内部状态;初次调用不执行代码而是返回生成器,每次 next() 执行到下一个 yield
  • 可迭代对象:实现 [Symbol.iterator]() 方法的对象;String / Array / TypedArray / Map / Set 是内置可迭代对象。
  • 专用于可迭代对象的语法:for...of、展开语法、yield*、解构。
  • 一次性 vs 多次迭代:生成器的 [Symbol.iterator]() 返回自身 → 只能迭代一次;可多次迭代的对象每次调用须返回新迭代器。
js
// 生成器函数:function* + yield,无需手动维护内部状态
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0
  for (let i = start; i < end; i += step) {
    iterationCount++
    yield i
  }
  return iterationCount
}
console.log([...makeRangeIterator(1, 10, 2)]) // [ 1, 3, 5, 7, 9 ]

// 自定义可迭代对象:实现 [Symbol.iterator]() 方法
const myIterable = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3
  },
}
for (const value of myIterable) console.log(value) // 1 2 3
console.log([...myIterable]) // [ 1, 2, 3 ] —— for...of、展开、解构都适用于可迭代对象

// 生成器既是迭代器也是可迭代对象,但只能迭代一次
const gen = makeRangeIterator(0, 3)
console.log(gen[Symbol.iterator]() === gen) // true —— 返回自身
console.log([...gen]) // [ 0, 1, 2 ]
console.log([...gen]) // [] —— 已被消耗

高级生成器

  • 生成器按需计算 yield 的值,可高效表示计算昂贵的序列甚至无限序列。
  • next(x) 的参数会成为 yield 表达式的值——外部可以向生成器注入状态(传给第一个 next() 的值会被忽略)。
  • throw() 强制生成器从当前挂起的 yield 处抛出异常;return(value) 返回给定值并终结生成器。
js
// next(x) 的参数会成为 yield 表达式的值——可以与生成器“对话”
function* fibonacci() {
  let current = 0
  let next = 1
  while (true) {
    const reset = yield current
    ;[current, next] = [next, next + current]
    if (reset) {
      current = 0
      next = 1
    }
  }
}

const sequence = fibonacci()
console.log(sequence.next().value) // 0
console.log(sequence.next().value) // 1
console.log(sequence.next().value) // 1
console.log(sequence.next().value) // 2
console.log(sequence.next().value) // 3
console.log(sequence.next(true).value) // 0 —— 传入 true 重启序列(传给第一个 next 的值会被忽略)
console.log(sequence.next().value) // 1

// throw() / return() 可从外部控制生成器
const g = fibonacci()
g.next()
console.log(g.return(42)) // { value: 42, done: true } —— 返回给定值并终结生成器