JS知识回顾(上)

JavaScript

什么是 JavaScript

JavaScript 是一门用来与网页交互的脚本语言,包含以下三个组成部分:

  • ECMAScript:由 ECMA-262 定义并提供核心功能。
  • 文档对象模型(DOM):提供与网页内容交互的方法和接口
  • 浏览器对象模型(BOM):提供与浏览器交互的方法和接口。

JavaScript 是通过script元素插入到HTML页面中的。这个元素可用于把JavaScript代码嵌入到HTML 页面中,跟其他标记混合在一起,也可用于引入保存在外部文件中的 JavaScript

  • 所有script元素会依照它们在网页中出现的次序被解释。在不使用deferasync 属性的 情况下,包含在script元素中的代码必须严格按次序解释。
  • 可以使用defer 属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出 的次序执行。
  • 可以使用async 属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异 步脚本不能保证按照它们在页面中出现的次序执行。
<body>
  <h1>JavaScript Fundamentals – Part 1</h1>

  <script>
    console.log("执行脚本1");
  </script>

  <script>
    console.log("执行脚本2");
  </script>

  <script defer>
    let timer1 = setTimeout(() => {
      console.log("执行脚本3");
      clearTimeout(timer1);
    }, 300);
  </script>
  <script defer>
    console.log("执行脚本4");
  </script>

  <script async>
    let timer2 = setTimeout(() => {
      console.log("执行脚本5");
      clearTimeout(timer2);
    }, 300);
  </script>
  <script async>
    console.log("执行脚本6");
  </script>
</body>

控制台输出结果:

执行脚本1
执行脚本2
执行脚本4
执行脚本6
执行脚本3
执行脚本5

变量

变量命名规则

  • 第一个字符必须是字母下划线或者美元符号$开头,其他字符可以是字母、下划线、美元符号和数字。
  • 不能以关键字和保留关键字命名
  • 变量中的英文字符是大小写敏感的,abc 和 Abc 是两个完全不同的变量,只要有任何一个字母大小写不一样,就是两个变量

typeof 操作符

typeof 运算符用于获取数据类型,但它不能准确判断所有类型。

console.log(typeof undefined); // "undefined" ✅
console.log(typeof true); // "boolean" ✅
console.log(typeof 42); // "number" ✅
console.log(typeof "hello"); // "string" ✅
console.log(typeof Symbol()); // "symbol" ✅
console.log(typeof 10n); // "bigint" ✅
console.log(typeof null); // "object" ❌(历史遗留问题)
console.log(typeof []); // "object" ❌(应使用 Array.isArray())
console.log(typeof {}); // "object" ✅
console.log(typeof function () {}); // "function" ✅(函数是特殊对象)

typeof在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用typeof null 返回的是"object"。这是因为特殊值null被认为是一个对空对象的引用。

数据类型

在 JavaScript 中,数据类型分为两大类:$简单数据类型$和$复杂数据类型$​。

简单数据类型

JavaScript 中的基本数据类型有七种(包括 ES6 新增的 Symbol 和 ES10 新增的 BigInt):

1. **Undefined**:表示未定义的值。只有一个值:`undefined`。
  • 当声明一个变量但未初始化时,该变量的值就是undefined
  • 函数没有明确返回值时,默认返回undefined
  • 使用typeof检测未声明的变量也会返回"undefined"(但实际中应避免使用未声明的变量)。
let a;
console.log(a); // undefined
  1. Null:表示空值。只有一个值:null
  • 表示一个空对象指针
  • 在需要释放对象引用时,可以将其设置为null(例如:obj = null;),以便垃圾回收
let b = null;
console.log(typeof b); // "object" (历史遗留问题)

$为什么typeof null返回 object?$

参考链接:[有趣的 JavaScript] 为什么 typeof null 返回 object

$如何安全检测null$

基本类型通过 值本身比较,判断一个值是不是 null,用===严格等于:

const isNull = (val) => val === null;
console.log(isNull(null)); // true;
console.log(isNull(undefined)); // false
  1. Boolean:表示布尔值,有两个值:truefalse
  • 布尔值,常用于条件判断。

  • 其他类型可以通过Boolean()函数或双重非运算(!!)转换为布尔值。

  • 在条件判断中,以下值会被转换为false(falsy 值):

    false / 0(包括-0+0)/ NaN / ""(空字符串)/ null / undefined

  1. Number:表示数字,包括整数和浮点数,以及一些特殊值(如NaNInfinity)。
  • 采用 IEEE 754 标准定义的双精度 64 位二进制格式。因此,JavaScript 中所有的数字都是浮点数(尽管整数和浮点数在表示上有所不同)

  • 整数范围:-2^53 + 12^53 - 1(即Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER

  • 特殊值:

    • NaN(Not a Number):表示一个本来要返回数值的操作数未返回数值的情况(如:0/0)。NaN与任何值都不相等,包括它自身。使用isNaN()Number.isNaN()来检测。

      console.log(0 / 0); // NaN
      console.log(-0 / +0); // NaN
      console.log(NaN == NaN); // false
      console.log(isNaN(NaN)); // true
      console.log(isNaN(10)); // false,10是数值
      console.log(isNaN("10")); // false,可以转换为数值10
      console.log(isNaN("blue")); // true,不可以转换为数值
      console.log(isNaN(true)); //false,可以转换为数值1
      
    • Infinity-Infinity:表示正无穷和负无穷。可以用isFinite()函数检测一个值是否是有限的。

      console.log(5 / 0); // Infinity
      console.log(5 / -0); // -Infinity
      

$为什么0.1+0.2 !== 0.3?$

  • 原因:IEEE 754 双精度浮点数精度问题(所有语言通病)。

  • 解决方案:使用 toFixed() 或处理为整数运算:

    console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3
    

$数值转换$

有 3 个函数可以将非数值转换为数值:Number()parseInt()parseFloat()

Number()可用于除 Symbol 以外的数据类型,后两个函数主要用于将字符串转为数值。

$Number()$基于如下规则转换:

  • 布尔值,true 转换为 1,false 转换为 0。
  • 数值,直接返回。
  • null,返回 0。
  • undefined,返回 NaN。
  • 字符串,应用以下规则:
    • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。 因此,Number("1")返回 1,Number("123")返回 123,Number("011")返回 11(忽略前面的零)。
    • 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)
    • 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整 数值。
    • 如果是空字符串(不包含字符),则返回 0。
    • 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
  • Symbol,抛出错误,Cannot convert a Symbol value to a number
  • 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。
console.log(Number(true)); // 1
console.log(Number("")); // 0
console.log(Number("programmer")); // NaN
console.log(Number("123")); // 123
console.log(Number("100.25")); // 100.25
console.log(Number("0x12")); // 18
console.log(Number(a)); // NaN
console.log(Number(obj)); // 0
// console.log(Number(fooSymbol)); // 报错  Cannot convert a Symbol value to a number
console.log(Number(bigNum1)); // 9007199254740991
console.log(Number({})); // NaN

$parseInt()$更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,$parseInt()$立即 返回。这意味着空字符串也会返回$NaN$(这一点跟$Number()$不一样,它返回 0。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。$parseInt()$函数也能识别不同的整数格式(十进制、八 进制、十六进制)。换句话说,如果字符串以"0x"开头,就会被解释为十六进制整数。 $parseInt()$也接收第二个参数,用于指定底数(进制数)。如果知道要解析的值是十六进制,那么可以传入 16 作为第二个参数。

console.log(parseInt("1234blue")); //1234
console.log(parseInt("22.5")); // 22
console.log(parseInt("")); // NaN
console.log(parseInt(70)); // 70
console.log(parseInt("0xf")); // 15

console.log(parseInt("AF")); // NaN
console.log(parseInt("AF", 16)); // 175 按十六进制解析

console.log(parseInt("10", 2)); // 2 按二进制解析
console.log(parseInt("10", 8)); // 8 按八进制解析
console.log(parseInt("10", 10)); // 10 按十进制解析
console.log(parseInt("10", 16)); // 16 按十六进制解析

$parseFloat()$函数的工作方式跟$parseInt()$​ 函数类似,都是从位置 0 开始检测每个字符。同样, 它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有 效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"将转换成 22.34。

$parseFloat()$函数的另一个不同之处在于,它$始终忽略字符串开头的零$。也就是说,十六进制数值始终会返回 0。因为 $parseFloat()$​ 只解析十进制值,因此不能指定底数。

console.log(parseFloat("1234blue")); // 1234
console.log(parseFloat("0xA")); // 0
console.log(parseFloat("22.5")); // 22.5
console.log(parseFloat("22.34.5")); // 22.34
console.log(parseFloat("0908.5")); // 908.5
console.log(parseFloat("3.125e7")); // 31250000
  1. String:表示字符串。
  • 字符串是不可变的(immutable),一旦创建,就不能改变。对字符串的操作(如拼接、替换)都会返回一个新的字符串。
  • 可以用单引号(')、双引号(")或反引号(`)表示。反引号用于模板字符串(template literals),可以嵌入表达式(${expression})和多行字符串。
let currentJob = "programmer";
let introduction = "My job is a " + currentJob;
let introduction2 = `My job is a ${currentJob}`;

$模版字符串$可以支持定义$标签函数$。$标签函数$本身是一个常规函数,通过前缀到$模版字符串$来应用自定义行为,它会接收被插值记号分隔后的模板和对每个表达式求值的结果。

function sumTag(strings, ...values) {
  // Static parts: [ 'The sum of ', ' and ', ' is ', '' ]
  console.log("Static parts:", strings);
  // Dynamic values: [ 10, 15, 25 ]
  console.log("Dynamic values:", values);
  let result = undefined;
  for (const val of values) {
    result += values;
  }
  return result;
}
const num1 = 10;
const num2 = 15;
sumTag`The sum of ${num1} and ${num2} is ${num1 + num2}`;
  1. Symbol(ES6 新增):表示唯一的、不可变的值,通常用作对象属性的键。
  • 通过Symbol()函数创建,每次调用都会返回一个唯一的 symbol 值,即使传入相同的描述(description)
  • 描述参数是可选的,用于调试,但不影响 symbol 的唯一性。
  • 使用全局符号注册表$Symbol.for()$,如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在$全局符号注册表$中创建并$重用$符号。
  • 主要用途:
    • 作为对象属性的键(避免属性名冲突)
    • 定义对象的私有成员(虽然不完全私有,但不会与常规属性名冲突)
    • 内置 Symbol 值(如Symbol.iterator)用于自定义对象的内置行为
let sym1 = Symbol("foo");
let sym2 = Symbol("foo");
console.log(sym1 === sym2); // false

let appleGlobalSymbol = Symbol.for("apple"); // 创建新符号
let otherAppleGlobalSymbol = Symbol.for("apple"); // 重用已有符号
console.log(appleGlobalSymbol === otherAppleGlobalSymbol); // true

//场景1:创建唯一对象属性键,使用Symbol创建对象属性键时,可以防止对象属性被意外覆盖。
const idSymbol = Symbol("id");
const user = {
  name: "Alice",
  [idSymbol]: 123, // 避免属性名冲突
};
user.id = 1;
console.log(user.id); // 1
console.log(user[idSymbol]); // 123

//场景2:创建绝对唯一的常量
// 传统方式(可能重复)
const LOG_LEVEL = {
  DEBUG: 1,
  INFO: 2,
  WARN: 3,
  ERROR: 4,
};

// 使用 Symbol(绝对唯一)
const LOG_LEVEL = {
  DEBUG: Symbol("debug"),
  INFO: Symbol("info"),
  WARN: Symbol("warn"),
  ERROR: Symbol("error"),
};

function log(message, level) {
  if (level === LOG_LEVEL.DEBUG) {
    console.debug(message);
  }
  // ...
}
  1. BigInt(ES2020 新增):表示任意精度的整数,用于解决大整数问题。
  • 用于表示大于2^53-1的整数。在数字后面加n或调用BigInt()函数创建。
  • 不能和Number类型直接混合运算,需要先转换为同一类型。
  • 在需要处理大整数(如高精度时间戳、大整数 ID)时使用。
let big1 = 9007199254740991n;
let big2 = BigInt("9007199254740991");
console.log(big1 === big2); // true

简单数据类型的特点

  • 不可变性(Immutable):基本数据类型的值不能被改变。例如,字符串的修改操作实际上是创建了一个新的字符串。
let str = "hello";
str[0] = "H"; // 无效操作
console.log(str); // "hello"(未被改变)

let newStr = str.toUpperCase(); // 创建新字符串
console.log(newStr); // "HELLO"
  • 按值传递:当将一个基本类型的值赋给变量或作为参数传递时,会创建一个新的副本。
let a = 10;
let b = a; // 创建值副本
b = 20;

console.log(a); // 10(不受影响)
console.log(b); // 20
  • 无方法属性:基本类型本身没有方法,但可通过包装对象临时调用。当我们对基本数据类型调用方法时(如"hello".toUpperCase()),JavaScript 会自动将其临时转换为对应的包装对象,执行方法后再丢弃包装对象。
let str = "hello";
console.log(str.length); // 5(自动创建 String 包装对象)
console.log(str.toUpperCase()); // "HELLO"(临时对象调用后销毁)
  • 按值比较:两个基本类型的值相等当且仅当它们的值相同。
const num1 = 5;
const num2 = 5;
console.log(num1 === num2); // true(值相同)

复杂数据类型

在 JavaScript 中,复杂数据类型(也称为引用类型)与基本类型有本质区别。复杂数据类型在内存中以$对象$形式存在,通过引用访问,具有可变性和动态结构。$数组$、$函数$、$正则$也都是对象类型,属于复杂值。

let obj = { name: "Alice", age: 30 }; // Object
let arr = [1, 2, 3, 4, 5]; // Array
let func = function () {
  return "Hello";
}; // Function
let reg = new RegExp(/abc/); // RegExp

console.log(typeof obj); // object
console.log(typeof arr); // object
console.log(typeof func); // function
console.log(typeof func.prototype); //object
console.log(typeof reg); // object

const student = {
  name: "xiao ming",
  sex: "f",
};
// hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。
console.log(student.hasOwnProperty("name")); // true
//  toString()返回对象的字符串表示
console.log(student.toString()); // [object Object]
// valueOf()返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。
console.log(student.valueOf()); // { name: 'xiao ming', sex: 'f' }

复杂数据类型的特点

  1. 存储方式:堆内存中存储实际数据,变量保存的是内存地址引用
  2. 可变性:创建后可修改内容
  3. 比较规则:比较的是内存地址,而非内容
  4. 动态结构:可随时添加/删除属性
const obj1 = { name: "Alice" };
const obj2 = obj1; // 复制引用(非对象本身)

obj2.name = "Bob";
console.log(obj1.name); // "Bob"(原对象被修改)

const arr1 = [1, 2];
const arr2 = [1, 2];
const arr3 = arr1;

console.log(arr1 === arr2); // false(不同内存地址)
console.log(arr1 === arr3); // true(相同引用)

// 内容比较方法
console.log(JSON.stringify(arr1) === JSON.stringify(arr2)); // true

const user = { name: "John" };
user.age = 30; // 动态添加属性
delete user.name; // 动态删除属性

function updateProfile(profile) {
  profile.age = 28; // 修改会影响外部对象
  profile = { name: "Sarah" }; // 重新赋值不会影响外部
}

const myProfile = { name: "Tom" };
updateProfile(myProfile);
console.log(myProfile); // { name: "Tom", age: 28 }

let、const 和 var

这三个关键字都可以用来声明变量,它们的区别主要从$作用域$、$变量提升$、$重复声明$以及$是否可重新赋值$​ 这几个方面来阐述。

作用域

$var$声明的变量具有$函数作用域$或$全局作用域$,这意味着在函数内部用$var$声明的变量只能在函数内部访问,而在函数外部声明的变量则是全局的。但是,如果在块语句(如 if、for 等{}内)中使用 var,它不会局限于该块,而是会存在于$整个函数$或$全局作用域$​。

console.log("函数外部调用a", a); // 输出:undefined
console.log("函数外部调用window.a", window.a); // 输出:undefined
function _var() {
  var a = 3;
  console.log("函数作用域", a);
}
_var(); // 输出:3
// console.log("函数外部调用a", a); // 输出:undefined
console.log("块语句外的b", b); //输出:undefined

for (var b = 0; b < 3; b++) {
  console.log("块语句里的b", b); // 输出:0,1,2
  var _b = 5;
  console.log("块语句里的_b", _b); //输出:5,5,5
}
console.log("块语句外的_b", _b); //5

console.log("块语句外的c", c); // 输出:undefined
if (true) {
  var c = 5;
  console.log("块语句里的c", c); // 输出:5
}

$let$ 和 $const$ 声明的变量具有$块级作用域(block scope)$。$块级作用域$指的是变量仅在声明它的块(即一对花括号${}$内部)中有效。

// console.log("快语句外的i", i); //输出:Error i is not defined
for (let i = 0; i < 3; i++) {
  console.log("快语句里的i", i); // 输出:0,1,2
}

// console.log("块语句外的word", word); // 输出:Error word is not defined
if (true) {
  const word = "test";
  console.log("块语句里的word", word); //输出:test
}

$var$ 在全局作用域声明时,会成为 $window$ 的属性,而 $let$/$const$​ 不会。

//以下代码请在浏览器环境下运行
var globalVar = 1;
let globalLet = 2;
console.log(window.globalVar); // 1(浏览器环境)
console.log(window.globalLet); // undefined

变量提升

从上面的代码可以看出var在未声明前可以访问,不过其值为undefined。但是let const 是无法在声明前进行访问的。这就是varletconst最大的区别:var存在变量提升

var 声明的变量会被提升到其作用域的顶部,并初始化为 undefined。这意味着在变量声明之前就可以访问到该变量,但其值为undefined

letconst 也会被提升,但是不会被初始化。在声明之前访问会触发暂时性死区,抛出 ReferenceError。暂时性死区是指在代码块内,使用letconst命令声明变量之前,该变量处于不可用状态,即“死区”。

ES6引入暂时性死区的概念主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。

简单理解就是:

var 的「创建」和「初始化」都被提升了。

letconst的「创建」过程被提升了,但是「初始化」没有提升。

重复声明

var 允许重复声明(可能覆盖变量)。

let const 禁止重复声明(直接报错)。

在同一作用域下,let/const 禁止与 varletconst 重复声明。

var test = "test1";
var test = "test2";
console.log("重复声明", test); //output:test2

var name = "Alice";
let name = "Bob"; // SyntaxError(同一作用域)

重新赋值

varlet可重新赋值

const 声明时必须赋值,并且该值不可改变。这里的值不可改变是指:

  • 基本类型(数字、字符串等)不可重新赋值。
  • 引用类型(对象、数组)可修改内部属性(内存地址不变)。
let count = 0;
count = 1; // 允许
const PI = 3.14;
// PI = 3.14159; // TypeError: Assignment to constant variable.

const user = { name: "Alice" };
user.name = "Bob"; // 允许(修改属性)
// user = { name: "Charlie" }; // TypeError: Assignment to constant variable.

误区陷阱

函数提升与变量提升的优先级

当用 var 声明 foo 变量以及声明 foo 函数时,打印出来的 foo 不是 var 声明的变量,而是 foo 函数。

//代码1
var foo;
function foo() {}
console.log(foo); // [Function: foo]

//代码2
function foo() {}
var foo;
console.log(foo); // [Function: foo]
  1. var 的「创建」和「初始化」都被提升了。
  2. function 的「创建」「初始化」和「赋值」都被提升了。

由于 function 比 var 多一个「赋值」过程,所以代码 1 和代码 2 的输出都是函数

由此可见函数提升要比变量提升的优先级要高一些且不会被变量声明覆盖

循环中的 var vs let
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 输出 3, 3, 3
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100); // 输出 0, 1, 2
}

// var 声明(共享同一个 i)
[ 回调1 ] -> 指向全局 i (最终值=3)
[ 回调2 ] -> 指向全局 i (最终值=3)
[ 回调3 ] -> 指向全局 i (最终值=3)

// let 声明(每个回调有自己的 i)
[ 回调1 ] -> 捕获作用域 A 的 i=0
[ 回调2 ] -> 捕获作用域 B 的 i=1
[ 回调3 ] -> 捕获作用域 C 的 i=2

使用varlet定义循环变量的 for 循环每秒输出对应的 i 和 j 值,可以看到结果:

  • var 无块级作用域,循环结束后 i=3,所有回调共享同一个 i
  • let 在循环中会为每次迭代创建独立作用域,j 的值被保留。依次输出不同的 j 值

for 循环执行流程:

i=0:设置定时器(延迟 1000*0 = 0ms)

i=1:设置定时器(延迟 1000*1 = 1000ms)

i=2:设置定时器(延迟 1000*2 = 2000ms)

i=3:循环条件 i<3 不成立,循环结束

定时器回调执行时:

如果变量 i 是由 var 声明的,所有回调函数访问的是同一个变量 i(因为 var 没有块级作用域)。此时循环已结束,i 的值为 3,虽然延迟时间不同,但所有回调输出的 i 都是最终值 3。

如果变量 i 是由 let 声明的,每次循环都会创建一个新的块级作用域i 在每个作用域中是独立的。定时器回调捕获的是当前迭代的 i(每个回调有自己的 i)。

操作符

ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串数值布尔值,甚至还有对象。在应用给对象时,操作符通常会调用valueOf()和/或toString()方法来取得可以计算的值。

操作符的优先级顺序:Operator_precedence

一元操作符

一元操作符(unary operator)是指只需要一个操作数的操作符。它们可以出现在操作数之前(前缀形式)或之后(后缀形式),具体取决于操作符的类型。

递增操作符 (++)

  • 前缀递增 (++operand):先增加操作数的值,然后返回增加后的值。
  • 后缀递增 (operand++):先返回操作数的当前值,然后再增加操作数的值。
let x = 5;
console.log(++x); // 6(前缀:先加1,后返回值)
console.log(x++); // 6(后缀:先返回值,后加1)
console.log(x); // 7

let num1 = 2;
let num2 = 20;
let num3 = num1++ + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 23

num3 = ++num1 + num2;
num4 = num1 + num2;
console.log(num3); // 24
console.log(num4); // 24

递减操作符 (--)

  • 前缀递减 (--operand):先减少操作数的值,然后返回减少后的值。
  • 后缀递减 (operand--):先返回操作数的当前值,然后再减少操作数的值。
let y = 5;
console.log(--y); // 4(前缀:先减1,后返回值)
console.log(y--); // 4(后缀:先返回值,后减1)
console.log(y); // 3

let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 21

num3 = --num1 + num2;
num4 = num1 + num2;
console.log(num3); // 20
console.log(num4); // 20

一元加 (+)

将操作数转换为数字类型。如果操作数已经是数字,则不会改变。

console.log(+10); // 10
console.log(+"10"); // 10(字符串转数字)
console.log(+"abc"); // NaN(转换失败)
console.log(+true); // 1
console.log(+false); // 0

一元减 (-)

将操作数转换为数字类型,然后取负值(正数变负数,负数变正数)。

console.log(-10); // -10
console.log(-"10"); // -10
console.log(-true); // -1
console.log(-false); // -0

typeof

返回表示操作数类型的字符串。

console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"(历史遗留问题)

void

执行表达式,但始终返回 undefined。常用于避免表达式返回值。

console.log(void 0); // undefined
console.log(void (1 + 2)); // undefined
// 常用于阻止链接跳转:<a href="javascript:void(0)">点击</a>

delete

删除对象的属性或数组元素。成功删除时返回 true,否则返回 false

let obj = { a: 1, b: 2 };
console.log(delete obj.a); // true
console.log(obj); // { b: 2 }

let arr = [1, 2, 3];
console.log(delete arr[1]); // true(但数组长度不变,arr[1] 变为 empty)
console.log(arr); // [1, empty, 3]

注意事项

  1. 递增/递减操作符
    • 只能用于变量(如 let x = 5; x++),不能直接用于字面量(如 5++ 会报错)。
  2. delete 的限制
    • 无法删除使用 varletconst 声明的变量。
    • 无法删除内置对象的不可配置属性(如 Math.PI)。
  3. 类型转换
    • 一元加/减和逻辑非会隐式转换操作数的类型,使用时需注意。

位操作符

位操作符用于在二进制级别直接操作数值的位。在 JavaScript 中,所有位操作符都会先将操作数转换为 32 位有符号整数,然后执行操作,最后再转换回 JavaScript 数值(64 位浮点数)。

按位与 (&)

对两个操作数的每个对应位执行AND操作。只有当两个位都为 1 时,结果位才为 1。

console.log(5 & 3); // 1
// 5: 0101
// 3: 0011
// & : 0001 (1)

按位或 (|)

对两个操作数的每个对应位执行 OR 操作。只要有一个位为 1,结果位就为 1。

console.log(5 | 3); // 7
// 5: 0101
// 3: 0011
// | : 0111 (7)

按位异或 (^)

对两个操作数的每个对应位执行 XOR 操作。当两个位不同时,结果位为 1。

console.log(5 ^ 3); // 6
// 5: 0101
// 3: 0011
// ^ : 0110 (6)

按位非 (~)

对操作数的二进制表示按位取反(包括符号位)。简单公式:~x = -(x + 1)

console.log(~5); // -6
// 5 的二进制:00000000000000000000000000000101
// 按位取反:  11111111111111111111111111111010(即 -6)

左移 (<<)

将第一个操作数的所有位向左移动指定的位数,右边用 0 填充。 公式: x << n = x × 2ⁿ

console.log(5 << 1); // 10
// 5: 0101
// <<1: 1010 (10)

有符号右移 (>>)

将第一个操作数的所有位向右移动指定的位数,左边用符号位填充(正数补 0,负数补 1)。

公式: x >> n = floor(x ÷ 2ⁿ)

console.log(5 >> 1); // 2
// 5: 0101
// >>1: 0010 (2)

console.log(-5 >> 1); // -3
// -5: 11111111111111111111111111111011
// >>1: 11111111111111111111111111111101 (-3)

无符号右移 (>>>)

将第一个操作数的所有位向右移动指定的位数,左边总是用 0 填充。

console.log(5 >>> 1); // 2
// 5: 0101
// >>>1: 0010 (2)

console.log(-5 >>> 1); // 2147483645
// -5: 11111111111111111111111111111011
// >>>1: 01111111111111111111111111111101 (2147483645)

布尔操作符

逻辑非 (!)

将操作数转换为布尔值,然后取反(truefalsefalsetrue)。

console.log(!true); // false
console.log(!false); // true
console.log(!0); // true (0 转换为 false,取反为 true)
console.log(!1); // false (1 转换为 true,取反为 false)
console.log(!""); // true (空字符串转换为 false,取反为 true)
console.log(!"hello"); // false (非空字符串转换为 true,取反为 false)
console.log(!null); // true
console.log(!NaN); // true
console.log(!undefined); // true
console.log(!{}); // false
console.log(!{ name: "zzz" }); // false

双重非 (!!)

将操作数转换为布尔值,不取反(即得到原值的布尔等价)。

js 有五个假值(falsy values):0空字符串""undefinednullNaN。这些值的布尔值都为false

console.log(!!true); // true
console.log(!!false); // false
console.log(!!0); // false
console.log(!!1); // true
console.log(!!""); // false
console.log(!!"hello"); // true
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!NaN); // false
console.log(!!{}); // true
console.log(!!{ name: "zzz" }); // true

逻辑与 (&&)

  • 如果第一个操作数为 假值,返回第一个操作数
  • 如果第一个操作数为 真值,返回第二个操作数
// 第一个操作数为假值,直接返回它
console.log(false && true); // false
console.log(0 && "hello"); // 0
console.log("" && "world"); // ""
console.log(null && 42); // null

// 第一个操作数为真值,返回第二个操作数
console.log(true && false); // false
console.log(1 && 0); // 0
console.log("hello" && ""); // ""
console.log("a" && "b"); // "b"

// 只有当 user 存在时才访问属性
const user = { name: "John" };
const userName = user && user.name; // "John"

// 安全地调用函数
const callback = null;
callback && callback(); // 不会执行,因为 callback 是假值

// 设置默认值(已被 ?? 和 || 替代)
const config = {};
const port = config.port && 3000; // undefined(注意:可能不是期望的结果)

逻辑或 (||)

  • 如果第一个操作数为 真值,返回第一个操作数
  • 如果第一个操作数为 假值,返回第二个操作数
// 第一个操作数为真值,直接返回它
console.log(true || false); // true
console.log(1 || 0); // 1
console.log("hello" || ""); // "hello"
console.log("a" || "b"); // "a"

// 第一个操作数为假值,返回第二个操作数
console.log(false || true); // true
console.log(0 || "hello"); // "hello"
console.log("" || "world"); // "world"
console.log(null || 42); // 42

// 设置默认值
const userName = inputName || "Anonymous";
const pageSize = config.pageSize || 10;

// 函数参数默认值(ES6之前的方式)
function greet(name) {
  name = name || "Guest";
  console.log(`Hello, ${name}!`);
}

// 选择第一个真值
const firstValidValue = null || 0 || "" || "Hello" || 42; // "Hello"

空值合并操作符 (??)

只有当左侧为 nullundefined 时才返回右侧操作数。

console.log(null ?? "default"); // "default"
console.log(undefined ?? "default"); // "default"
console.log(0 ?? "default"); // 0
console.log("" ?? "default"); // ""
console.log(false ?? "default"); // false

// 与 || 的区别
console.log(0 || "default"); // "default" - 可能不是期望的结果
console.log(0 ?? "default"); // 0 - 保留假值但非空的值

乘性操作符

乘法操作符 (*)

执行两个数值的乘法运算。

console.log(5 * 3); // 15
console.log(-2 * 4); // -8
console.log(2.5 * 2); // 5

// 字符串转换为数字
console.log("5" * "3"); // 15
console.log("2.5" * 2); // 5

// 布尔值转换
console.log(true * 3); // 1 * 3 = 3
console.log(false * 5); // 0 * 5 = 0

// 特殊值
console.log(5 * null); // 0 (null → 0)
console.log(5 * undefined); // NaN (undefined → NaN)
console.log(5 * "hello"); // NaN (无法转换为数字)

// 数组转换
console.log([2] * [3]); // 6 (数组转换为数字)
console.log([] * 5); // 0 (空数组 → 0)

除法操作符 (/)

执行两个数值的除法运算。

console.log(10 / 2); // 5
console.log(8 / 3); // 2.6666666666666665
console.log(-9 / 3); // -3

console.log(5 / 0); // Infinity
console.log(-5 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(Infinity / Infinity); // NaN
console.log(10 / Infinity); // 0

console.log("10" / "2"); // 5
console.log("10" / 2); // 5
console.log(true / false); // Infinity (1 / 0)
console.log(null / 2); // 0 (0 / 2)
console.log(undefined / 2); // NaN

取模操作符 (%)

返回除法操作的余数。

console.log(10 % 3); // 1 (10 ÷ 3 = 3 余 1)
console.log(8 % 4); // 0 (8 ÷ 4 = 2 余 0)
console.log(7 % 2); // 1 (判断奇偶性)

console.log(5 % 0); // NaN
console.log(0 % 5); // 0
console.log(Infinity % 2); // NaN
console.log(5 % Infinity); // 5

console.log("10" % "3"); // 1
console.log("10.5" % 3); // 1.5
console.log(true % 2); // 1 (1 % 2)
console.log(false % 2); // 0 (0 % 2)

console.log(10 % 3); // 1
console.log(-10 % 3); // -1
console.log(10 % -3); // 1
console.log(-10 % -3); // -1

**指数操作符 (**)**

console.log(2 ** 3); // 8 (2³)
console.log(3 ** 2); // 9 (3²)
console.log(4 ** 0.5); // 2 (平方根)
console.log(2 ** -2); // 0.25 (1/4)

console.log("2" ** "3"); // 8
console.log(2 ** "3"); // 8
console.log(true ** 3); // 1 (1³)
console.log(false ** 3); // 0 (0³)

console.log(2 ** 3 === Math.pow(2, 3)); // true
console.log(5 ** 2 === Math.pow(5, 2)); // true

加性操作符

加法操作符 (+)

加法操作符有两种主要行为:数值相加字符串连接

console.log(5 + 3); // 8
console.log(2.5 + 1.5); // 4
console.log(-2 + 4); // 2

// 只要有字符串,就进行连接
console.log(1 + "2"); // "12"
console.log("3" + 4); // "34"
console.log("5" + null); // "5null"
console.log("6" + undefined); // "6undefined"
console.log("7" + true); // "7true"

// 布尔值转换为数字
console.log(true + true); // 2 (1 + 1)
console.log(true + false); // 1 (1 + 0)
console.log(false + false); // 0 (0 + 0)

// 混合类型
console.log(1 + true); // 2 (1 + 1)
console.log(1 + false); // 1 (1 + 0)

console.log(1 + null); // 1 (null → 0)
console.log(1 + undefined); // NaN (undefined → NaN)
console.log(1 + NaN); // NaN

// 数组和对象
console.log(1 + []); // "1" (空数组 → 空字符串)
console.log(1 + [2]); // "12" (数组 → "2")
console.log(1 + [2, 3]); // "12,3" (数组 → "2,3")
console.log(1 + {}); // "1[object Object]"

减法操作符 (-)

减法操作符始终执行数值减法,并进行类型转换。

console.log(5 - 3); // 2
console.log(10 - 2.5); // 7.5
console.log(-2 - 4); // -6

// 字符串转换为数字
console.log("10" - "3"); // 7
console.log("10" - 3); // 7
console.log("10.5" - 2); // 8.5

// 布尔值转换
console.log(true - false); // 1 (1 - 0)
console.log(false - true); // -1 (0 - 1)

// 特殊值
console.log(5 - null); // 5 (null → 0)
console.log(5 - undefined); // NaN
console.log(5 - "hello"); // NaN

let a = 5;
let b = "3";
let c = 2;

console.log(a + b - c); // 53 - 2 = 51
// 步骤: 5 + "3" = "53", 然后 "53" - 2 = 51

console.log(a - b + c); // 5 - 3 + 2 = 4
// 步骤: 5 - "3" = 2, 然后 2 + 2 = 4

关系操作符

关系操作符用于比较两个值,并返回一个布尔值(truefalse)。这些操作符在条件判断、循环控制和排序等场景中非常关键。

小于 (<) 和 大于 (>)

比较两个值的大小关系。

console.log(5 < 10); // true
console.log(5 > 10); // false
console.log(10 < 10); // false
console.log(10 > 10); // false

// 字符串比较(按字典顺序)
console.log("apple" < "banana"); // true
console.log("10" < "2"); // true (因为 "1" < "2")
console.log("10" < 2); // false ("10" 转换为数字 10)

// 布尔值转换
console.log(true < false); // false (1 < 0)
console.log(true > 0); // true (1 > 0)

// 特殊值
console.log(null < 1); // true (null → 0)
console.log(undefined < 0); // false (undefined → NaN)
console.log(NaN < 0); // false
console.log(NaN > 0); // false

小于等于 (<=) 和 大于等于 (>=)

比较两个值的大小或相等关系。

console.log(5 <= 10); // true
console.log(5 >= 10); // false
console.log(10 <= 10); // true
console.log(10 >= 10); // true

console.log(5 <= "5"); // true
console.log("10" >= 10); // true
console.log(null <= 0); // true (null → 0)
console.log(undefined >= 0); // false (undefined → NaN)

相等 (==) 和 不相等 (!=)

比较值是否相等,会进行类型转换(类型强制转换)。

console.log(5 == 5); // true
console.log(5 == "5"); // true (字符串转换为数字)
console.log(true == 1); // true (true → 1)
console.log(false == 0); // true (false → 0)
console.log(null == undefined); // true

console.log(5 != 10); // true
console.log(5 != "5"); // false
console.log(true != 1); // false
console.log(null != undefined); // false

console.log(null == 0); // false (null 只与 undefined 和自身相等)
console.log(undefined == 0); // false
console.log(NaN == NaN); // false (NaN 与任何值都不相等,包括自身)
console.log("" == 0); // true (空字符串 → 0)
console.log(" " == 0); // true (空格字符串 → 0)
console.log([] == 0); // true (空数组 → 0)
console.log([] == ""); // true (空数组 → 空字符串)

严格相等 (===) 和 严格不相等 (!==)

比较值和类型是否都相等(不进行类型转换)。

console.log(5 === 5); // true
console.log(5 === "5"); // false (类型不同)
console.log(true === 1); // false (类型不同)
console.log(null === undefined); // false (类型不同)

console.log(5 !== 10); // true
console.log(5 !== "5"); // true (类型不同)
console.log(true !== 1); // true (类型不同)
console.log(null !== undefined); // true

// 避免意外的类型转换
console.log(0 == false); // true (可能不是期望的结果)
console.log(0 === false); // false (明确区分)

console.log("" == 0); // true (可能不是期望的结果)
console.log("" === 0); // false (明确区分)

console.log(null == undefined); // true
console.log(null === undefined); // false

条件操作符(三元操作符)

条件操作符是 JavaScript 中唯一需要三个操作数的操作符,通常被称为三元操作符。它的语法是:

condition ? expression1 : expression2;

// 基本示例
const age = 20;
const status = age >= 18 ? "成年人" : "未成年人";
console.log(status); // "成年人"

const isRaining = true;
const action = isRaining ? "带伞" : "不用带伞";
console.log(action); // "带伞"

语句

if 语句

if 语句用于根据条件执行不同的代码块。

if (condition) {
  // 当条件为真时执行的代码
} else if (anotherCondition) {
  // 当第一个条件为假,且此条件为真时执行
} else {
  // 当所有条件都为假时执行
}

let age = 18;

if (age < 18) {
  console.log("未成年");
} else if (age >= 18 && age < 60) {
  console.log("成年人");
} else {
  console.log("老年人");
}

do-while 语句

do while 循环先执行一次代码块,然后检查条件,如果条件为真则继续循环。

do {
  // 循环体(至少执行一次)
} while (condition);

// 基础示例
let number = 0;

do {
  console.log("数字: " + number);
  number++;
} while (number < 3);
// 输出:数字: 0, 数字: 1, 数字: 2

// 即使条件不成立,也会执行一次
let x = 10;

do {
  console.log("这行代码会执行"); // 输出:这行代码会执行
  x++;
} while (x < 5);

while 语句

while 循环在条件为真时重复执行代码块。

while (condition) {
  // 循环体
}

// 基础示例
let count = 0;

while (count < 5) {
  console.log("计数: " + count);
  count++;
}
// 输出:计数: 0, 计数: 1, 计数: 2, 计数: 3, 计数: 4

// 遍历数组
let fruits = ["苹果", "香蕉", "橙子"];
let i = 0;

while (i < fruits.length) {
  console.log(fruits[i]);
  i++;
}

// 危险!这会创建无限循环
// while (true) {
//     console.log("永远执行");
// }

for 语句

for 循环是最常用的循环结构,适合已知循环次数的情况。

for (初始化; 条件; 更新) {
  // 循环体
}

// 基础示例
for (let i = 0; i < 5; i++) {
  console.log("索引: " + i);
}
// 输出:索引: 0, 索引: 1, 索引: 2, 索引: 3, 索引: 4

// 遍历数组
let colors = ["红色", "绿色", "蓝色"];
for (let i = 0; i < colors.length; i++) {
  console.log(colors[i]);
}
// 输出:红色, 绿色, 蓝色

// 递减循环
for (let i = 10; i > 0; i--) {
  console.log(i);
}
// 输出:10, 9, 8, ..., 1

// 多个变量
for (let i = 0, j = 10; i < j; i++, j--) {
  console.log(`i=${i}, j=${j}`);
}

// 省略部分表达式
let k = 0;
for (; k < 3; ) {
  console.log(k);
  k++;
}

for-in 语句

for-in 循环用于遍历对象可枚举属性

for (key in object) {
  // 使用 object[key] 访问属性值
}

// 遍历对象
let person = {
  name: "张三",
  age: 25,
  city: "北京",
};

for (let key in person) {
  console.log(key + ": " + person[key]);
}
// 输出:
// name: 张三
// age: 25
// city: 北京

// 遍历数组(不推荐)
let fruits = ["苹果", "香蕉", "橙子"];
for (let index in fruits) {
  console.log(index + ": " + fruits[index]);
}
// 输出:0: 苹果, 1: 香蕉, 2: 橙子

for-of 语句

for-of 循环用于遍历可迭代对象(数组、字符串、Map、Set 等),直接获取值。

for (variable of iterable) {
  // 使用 variable
}

// 遍历数组
let numbers = [10, 20, 30];
for (let number of numbers) {
  console.log(number);
}
// 输出:10, 20, 30

// 遍历字符串
let text = "Hello";
for (let char of text) {
  console.log(char);
}
// 输出:H, e, l, l, o

// 遍历 Map
let map = new Map();
map.set("name", "李四");
map.set("age", 30);

for (let [key, value] of map) {
  console.log(key + ": " + value);
}
// 输出:name: 李四, age: 30

// 遍历 Set
let set = new Set([1, 2, 3, 3, 2]);
for (let item of set) {
  console.log(item);
}
// 输出:1, 2, 3

break 和 continue 语句

breakcontinue语句为执行循环代码提供了更严格的控制手段。

  • break语句用于立即退出循环,强制执行循环后的下一条语句
  • continue语句也用于立即退出循环,但会再次从循环顶部 开始执行。
// break - 完全退出循环
for (let i = 0; i < 10; i++) {
  if (i === 5) {
    break; // 当 i=5 时退出循环
  }
  console.log(i); // 输出:0, 1, 2, 3, 4
}

// continue - 跳过当前迭代
for (let i = 0; i < 5; i++) {
  if (i === 2) {
    continue; // 跳过 i=2 的情况
  }
  console.log(i); // 输出:0, 1, 3, 4
}

标签语句

标签语句用于给语句加标签。可以在后面通过breakcontinue 语句引用。标签语句的典型应用场景是嵌套循环

// 使用标签控制外层循环
outerLoop: for (let i = 0; i < 3; i++) {
  innerLoop: for (let j = 0; j < 3; j++) {
    if (i === 1 && j === 1) {
      break outerLoop; // 直接退出外层循环
    }
    console.log(`i=${i}, j=${j}`);
  }
}
// 输出:
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0

with 语句

with 语句用于扩展语句的作用域链,允许您在一个对象的作用域内执行代码块,从而简化对对象属性的访问。

with (expression) {
  statement;
}

let person = {
  name: "张三",
  age: 25,
  job: "工程师",
};

// 不使用 with
console.log(person.name);
console.log(person.age);
console.log(person.job);

// 使用 with
with (person) {
  console.log(name); // 相当于 person.name
  console.log(age); // 相当于 person.age
  console.log(job); // 相当于 person.job
}

switch 语句

switch 语句用于基于不同条件执行不同代码块,可以替代多个 if...else if 语句。

switch (expression) {
  case value1:
    // 当expression等于value1时执行
    break;
  case value2:
    // 当expression等于value2时执行
    break;
  default:
  // 当没有匹配的case时执行
}

严格模式

严格模式(Strict Mode)是 JavaScript 中的一种模式,它允许你在严格的条件下运行 JavaScript 代码,从而帮助开发者编写更安全、更优质的代码。严格模式通过抛出错误来消除一些静默错误,修复了一些导致 JavaScript 引擎难以优化的缺陷,并且禁用了某些可能在未来版本中定义的语法。

let hasDriversLicense = false;
const passTest = true;

if (passTest) hasDriverLicense = true;
if (hasDriversLicense) console.log("I can drive :D");

这段代码中,非严格模式下不会报错,严格模式下会抛出错误。

"use strict";

let hasDriversLicense = false;
const passTest = true;

if (passTest) hasDriverLicense = true;
if (hasDriversLicense) console.log("I can drive :D");

运行之后会提示错误:hasDriverLicense is not defined。

函数

函数是一段可重复使用的代码块,用于执行特定任务或计算值。函数可以被当作变量一样使用:可以赋值给变量,可以作为参数传递,可以作为另一个函数的返回值,也可以拥有属性和方法。

function functionName(arg0, arg1,...,argN) {
  statements
}


定义方式

函数声明(Function Declaration)

函数声明会被提升(hoisted),意味着你可以在声明之前调用它。

// 函数声明(会提升)
function calculateArea(width, height) {
  return width * height;
}
console.log(calculateArea(5, 3)); // 15

const age1 = calcAge1(1999);
function calcAge1(birthYear) {
  return new Date().getFullYear() - birthYear;
}

函数表达式(Function Expression)

函数表达式将函数赋值给一个变量。它可以是匿名的,也可以有函数名。函数表达式不会被提升,因此必须在定义之后才能调用。

// 函数表达式
const calculateArea = function (width, height) {
  return width * height;
};

// 命名函数表达式(推荐用于调试)
const factorial = function fact(n) {
  if (n <= 1) return 1;
  return n * fact(n - 1); // 可以在内部使用函数名
};
console.log(factorial(5)); // 120

箭头函数

箭头函数是 ES6 中引入的,使用箭头=>定义函数。它们更简洁,并且不绑定自己的thisargumentssupernew.target

// 基本箭头函数
const calculateArea = (width, height) => {
  return width * height;
};

// 简写形式
const square = (x) => x * x; // 单参数可省略括号
const add = (a, b) => a + b; // 单表达式可省略return和花括号
const greet = () => console.log("Hello!"); // 无参数需要空括号

函数构造函数(不推荐)

使用 Function 构造函数可以动态创建函数,但这种方式不推荐,因为会有安全性和性能问题。

// 很少使用,主要用于动态创建函数
const multiply = new Function("a", "b", "return a * b");
console.log(multiply(4, 5)); // 20

函数调用

直接调用

直接使用函数名(或变量名)后跟括号来调用函数。

function sayHello() {
  console.log("Hello");
}
sayHello();

作为方法调用

当函数作为对象的属性时,我们称它为方法。通过对象来调用方法。

const obj = {
  method: function () {
    console.log("Hello from method");
  },
};
obj.method();

使用 call 和 apply 调用

callapply方法允许你调用一个函数,并指定函数内部的this值,以及传递参数。

function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

const person = { name: "Alice" };

// 使用call,参数逐个传递
greet.call(person, "Hello", "!");

// 使用apply,参数以数组形式传递
greet.apply(person, ["Hello", "!"]);

构造函数调用

使用new关键字来调用函数作为构造函数,创建一个新对象。

function Person(name) {
  this.name = name;
}

const person1 = new Person("Bob");

函数参数

形参和实参

形参是函数定义时列出的变量,实参是函数调用时传递的值。

默认参数

ES6 允许为函数参数设置默认值。

function multiply(a, b = 1) {
  return a * b;
}

剩余参数

剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

function sum(...numbers) {
  return numbers.reduce((acc, current) => acc + current, 0);
}

参数解构

可以使用解构赋值来从参数对象中提取数据。

function fullName({ firstName, lastName }) {
  return firstName + " " + lastName;
}

const person = { firstName: "John", lastName: "Doe" };
fullName(person);

变量、作用域与内存

原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:原始值引用值

  • 原始值:是存储在栈内存中的简单数据段,直接按值访问。变量中保存的就是数据本身
  • 引用值:是存储在堆内存中的对象,变量中保存的只是一个指向该对象的内存地址(引用)

动态属性

原始值不能有属性,尽管尝试给原始值添加属性不会报错。

let nameStr = "zzz";
nameStr.name = "zzz2";
console.log(nameStr.name); // undefined

引用值可以随时添加、修改和删除其属性和方法。

let person = new Object();
person.name = "zzz";
console.log(person.name); // zzz

注意,原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会 创建一个 Object 类型的实例,但其行为类似原始值。

let name1 = "zzz";
let name2 = new String("zzz");
name1.name = "zzz2";
name2.name = "zzz2";
console.log(name1.name); // undefined
console.log(name2.name); // zzz2
console.log(typeof name1); // string
console.log(typeof name2); // object

复制值

原始值是按值复制,创建新副本。复制的变量与原来变量可以独立使用,互不干扰。

let num1 = 5;
let num2 = num1;
num1 = 6;
console.log(num1, num2); // 6 5

引用值是按引用复制,复制的是地址。复制的变量与原来变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。

let obj1 = new Object();
let obj2 = obj1;
obj1.name = "zzz";
console.log(obj2.name); // zzz

传递参数

JavaScript 中所有函数的参数都是按值传递的。

  • 对于原始值:传递的是值的副本,函数内部修改参数不会影响外部变量。
  • 对于引用值:传递的是地址的副本。这意味着:
    • 如果通过这个地址修改了对象的属性,外部的对象会受到影响(因为指向同一个对象)。
    • 但如果重新赋值了这个参数(让它指向一个新对象),则不会影响外部的原始变量。
// 示例 1:原始值传递
function changePrimitive(num) {
  num = 100; // 修改的是局部变量 num 的副本
}
let myNum = 50;
changePrimitive(myNum);
console.log(myNum); // 50 (未受影响)

// 示例 2:引用值传递 - 修改属性
function changeProperty(obj) {
  obj.name = "Modified"; // 通过地址副本修改了堆内存中的对象
}
let myObj = { name: "Original" };
changeProperty(myObj);
console.log(myObj.name); // "Modified" (受影响)

// 示例 3:引用值传递 - 重新赋值
function reassignReference(obj) {
  obj = { name: "New Object" }; // 这里 obj 指向了一个新对象,与外部 myObj 断开了联系
}
let myObj2 = { name: "Original" };
reassignReference(myObj2);
console.log(myObj2.name); // "Original" (未受影响,因为函数内部只是改变了局部变量 obj 的指向)

确定类型

  • typeof:用于确定值的基本类型,对原始值特别有用
    • 在用于检测函数时也会返回"function"。但无法区分不同类型的对象(数组、日期、普通对象等都返回 "object"
    • typeof null 返回 "object" 是著名的历史遗留 bug
  • instanceof:用于检查对象的继承关系,确定对象是由哪个构造函数创建的
    • 检查构造函数的 prototype 属性是否出现在对象的原型链上
    • 只对对象有效,对原始值返回 false

执行上下文

执行上下文是 JavaScript 代码被解析和执行时所在环境的抽象概念。每当代码运行时,它都会在一个执行上下文中执行。

JavaScript 中有三种执行上下文:

  1. 全局执行上下文
    • 默认的、最外层的执行上下文
    • 在浏览器中是 window 对象,在 Node.js 中是 global 对象
    • 一个程序只有一个全局执行上下文
  2. 函数执行上下文
    • 每次函数被调用时都会创建一个新的函数执行上下文
    • 每个函数都有自己的执行上下文
  3. Eval 函数执行上下文(不常用,通常不建议使用)

执行上下文栈(调用栈)

执行上下文栈(也称为调用栈)用于管理代码执行期间创建的所有执行上下文。

function first() {
  console.log("Inside first function");
  second();
  console.log("Again inside first function");
}

function second() {
  console.log("Inside second function");
  third();
}

function third() {
  console.log("Inside third function");
}

first();
console.log("Inside Global Execution Context");

调用栈的变化过程:

1. 栈空
2. 全局执行上下文入栈
3. first() 被调用 -> first 执行上下文入栈
4. second() 被调用 -> second 执行上下文入栈
5. third() 被调用 -> third 执行上下文入栈
6. third() 执行完毕 -> third 执行上下文出栈
7. second() 执行完毕 -> second 执行上下文出栈
8. first() 执行完毕 -> first 执行上下文出栈
9. 全局代码执行完毕 -> 全局执行上下文出栈

创建阶段

执行上下文的创建发生在代码执行之前,包括三个重要步骤:

  1. 创建变量对象:用于存储变量、函数声明和函数参数。
    • 对于函数上下文,变量对象也被称为活动对象(Activation Object,AO)。
  2. 建立作用域链:由当前执行上下文的变量对象和所有父级执行上下文的变量对象组成。
  3. 确定 this
console.log(a); // undefined (变量提升)
var a = 10;

function foo(b) {
  console.log(b); // 20
  console.log(a); // undefined (不是全局的10)
  var a = 30;
}
foo(20);

作用域链增强

作用域链增强是指在代码执行过程中,除了常规的词法作用域之外,某些语句或操作会临时在作用域链的前端添加额外的变量对象,从而改变变量的查找顺序。

  • catch 语句

    try-catch 语句中,catch 块会将错误对象添加到作用域链的前端。

    var errorMessage = "global error";
    
    function testCatch() {
      var errorMessage = "function error";
    
      try {
        throw new Error("caught error");
      } catch (error) {
        // catch 块内,error 被添加到作用域链前端
        console.log(error.message); // "caught error" (从 catch 参数获取)
        console.log(errorMessage); // "function error" (不是 "global error")
    
        var catchVar = "inside catch";
        let blockVar = "block scoped";
      }
    
      console.log(catchVar); // "inside catch" (var 是函数作用域)
      // console.log(blockVar); // 错误: blockVar is not defined (let 是块级作用域)
      // console.log(error);    // 错误: error is not defined (catch 参数只在 catch 块内有效)
    }
    
    testCatch();
    
  • with语句(不推荐使用)

作用域

作用域定义了变量和函数的可访问范围。JavaScript 采用词法作用域(静态作用域),意味着作用域在代码编写时就已经确定。

全局作用域

在代码的任何地方都可以访问的变量和函数。

var globalVariable = "我是全局变量";

function globalFunction() {
  console.log(globalVariable); // 可以访问
}

console.log(globalVariable); // 可以访问
globalFunction(); // 可以调用

函数作用域

在函数内部声明的变量只能在函数内部访问。

function testFunctionScope() {
  var functionScopedVar = "我在函数内部";

  if (true) {
    var stillInFunction = "我还在函数内部";
  }

  console.log(functionScopedVar); // 可以访问
  console.log(stillInFunction); // 可以访问
}

// console.log(functionScopedVar); // 错误:functionScopedVar is not defined

块级作用域

使用 letconst 声明的变量具有块级作用域。

function testBlockScope() {
  if (true) {
    var varVariable = "我用 var 声明";
    let letVariable = "我用 let 声明";
    const constVariable = "我用 const 声明";

    console.log(varVariable); // 可以访问
    console.log(letVariable); // 可以访问
    console.log(constVariable); // 可以访问
  }

  console.log(varVariable); // 可以访问(函数作用域)
  // console.log(letVariable);   // 错误:letVariable is not defined
  // console.log(constVariable); // 错误:constVariable is not defined
}

作用域链

当访问一个变量时,JavaScript 引擎会沿着作用域链从当前作用域开始查找,直到找到该变量或到达全局作用域。

var globalVar = "global";

function outer() {
  var outerVar = "outer";

  function inner() {
    var innerVar = "inner";

    console.log(innerVar); // 在当前作用域找到 "inner"
    console.log(outerVar); // 在父作用域找到 "outer"
    console.log(globalVar); // 在全局作用域找到 "global"
    // console.log(notDefined); // 错误:在所有作用域都找不到
  }

  inner();
}

outer();

作用域链的可视化表示:

inner 函数的作用域
  ├── inner 的变量:innerVar
  │
  └── outer 函数的作用域
        ├── outer 的变量:outerVar
        │
        └── 全局作用域
              ├── 全局变量:globalVar
              └── 函数:outer

词法作用域 vs 动态作用域

  • 词法作用域:作用域在函数定义时确定,与调用位置无关。

    var value = "global";
    
    function foo() {
      console.log(value);
    }
    
    function bar() {
      var value = "local";
      foo(); // 输出 "global",不是 "local"
    }
    
    bar();
    
  • 动态作用域(其他语言如 Bash):作用域在函数调用时确定,与调用位置相关。

执行上下文与作用域关系

关联:

  • 每个执行上下文都有一个关联的作用域链
  • 作用域链是在执行上下文创建时建立的
  • 作用域决定了变量的可访问性,而执行上下文提供了执行环境

差异

作用域‌ 在代码编译阶段确定,而 ‌执行上下文‌ 在代码执行阶段动态创建,只在函数调用期间存在。

内存

  1. 分配内存:当声明变量、函数、对象时,系统自动分配内存
  2. 使用内存:对内存进行读写操作
  3. 释放内存:当内存不再需要时,将其释放

JavaScript 通过自动内存管理实现内存分配闲置资源回收

基本思路很简单:确定哪个变量不会再 使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

JavaScript 最常用的垃圾回收策略是标记清除,另一种是引用计数

标记清除

该算法分为两个阶段:

  • 标记阶段:从根对象开始,递归遍历所有可达对象,并标记它们
  • 清除阶段:遍历整个堆内存,回收未被标记的对象

标记清除

引用计数

基本思路是每个值都有一个引用计数器,当有新的引用指向该值时,计数器加 1;当引用被删除时,计数器减 1。当计数器为 0 时,值被回收。

// 引用计数示例
let obj1 = { name: "obj1" }; // 引用计数: 1
let obj2 = obj1; // 引用计数: 2

obj1 = null; // 引用计数: 1
obj2 = null; // 引用计数: 0 → 可回收

然而,引用计数算法存在一个严重的问题——循环引用。当两个或多个对象相互引用时,即使它们都不再被外部引用,它们的引用计数也不会为 0,导致内存泄漏

// 循环引用导致内存泄漏
function createCycle() {
  let objA = { name: "A" };
  let objB = { name: "B" };

  objA.ref = objB; // objA 引用 objB
  objB.ref = objA; // objB 引用 objA

  return "cycle created";
}

createCycle();
// objA 和 objB 的引用计数永远不为0,无法回收(在引用计数算法中)

内存泄漏

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失。

优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性,局部变量在超出作用域后会被自动解除引用。

解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。

JavaScript 中的内存泄漏大部分是由不合理的引用导致的。

  • 意外声明全局变量

    // 泄漏示例 1:意外的全局变量
    function createLeak() {
      leakedData = new Array(1000000).fill("*"); // 没有 var/let/const
    }
    
    createLeak();
    // leakedData 成为全局变量,永远不会被回收
    
  • 被遗忘的定时器和回调

    // 泄漏示例 2:未清理的定时器
    class DataProcessor {
      constructor() {
        this.data = new Array(10000).fill("data");
        this.intervalId = setInterval(() => {
          this.processData();
        }, 1000);
      }
    
      processData() {
        // 处理数据
      }
    
      // 忘记提供清理方法
      // destroy() {
      //     clearInterval(this.intervalId);
      // }
    }
    
    const processor = new DataProcessor();
    // 即使不再需要 processor,定时器仍然持有引用
    
  • DOM 引用泄漏

    // 泄漏示例 3:DOM 引用
    const elementsCache = new Map();
    
    function cacheElement(id) {
      const element = document.getElementById(id);
      if (element) {
        elementsCache.set(id, element);
      }
    }
    
    // 即使从 DOM 中移除元素,elementsCache 仍然持有引用
    function removeElement(id) {
      const element = document.getElementById(id);
      if (element) {
        element.remove();
        // 忘记从缓存中清除:elementsCache.delete(id);
      }
    }
    
  • 闭包泄漏

    // 泄漏示例 4:闭包持有大对象
    function createClosureLeak() {
      const largeData = new Array(1000000).fill("large data");
    
      return function () {
        // 即使不使用 largeData,闭包仍然持有对其词法环境的引用
        console.log("doing something");
      };
    }
    
    const leakedClosure = createClosureLeak();
    // largeData 无法被回收,因为闭包保持着对创建时环境的引用
    

静态分配与对象池

要提升 JavaScript 性能,还要考虑如何减少浏览器执行垃圾回收的次数。

可以使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。 应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。 由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。

// 对象池减少垃圾回收压力
class ObjectPool {
  constructor(createFn, resetFn, initialSize = 10) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.pool = [];
    this.active = new Set();

    // 预创建对象
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(createFn());
    }
  }

  acquire() {
    let obj;
    if (this.pool.length > 0) {
      obj = this.pool.pop();
    } else {
      obj = this.createFn();
    }

    this.active.add(obj);
    return obj;
  }

  release(obj) {
    if (this.active.has(obj)) {
      this.resetFn(obj);
      this.active.delete(obj);
      this.pool.push(obj);
    }
  }
}

// 使用对象池
const vectorPool = new ObjectPool(
  () => ({ x: 0, y: 0, z: 0 }), // 创建函数
  (vec) => {
    vec.x = 0;
    vec.y = 0;
    vec.z = 0;
  } // 重置函数
);

// 在游戏循环中使用
function gameLoop() {
  // 重用对象而不是创建新对象
  const position = vectorPool.acquire();
  position.x = Math.random();
  position.y = Math.random();

  // 使用 position...

  // 使用完毕后释放
  vectorPool.release(position);
}

如果对象池是使用数组来实现,可以在初始化时静态分配创建一个大小够用的数组,从而避免动态分配操作。

基本引用类型

Date

Date 类型用于处理日期和时间。JavaScript 中的 Date 类型是基于 Unix 时间戳 的,即自 1970 年 1 月 1 日(UTC)起经过的毫秒数。

创建 Date 对象

有四种主要方式创建一个新的 Date 对象:

// 1.当前日期和时间
let now = new Date();
console.log(now);
// 2.传入毫秒数
let date1 = new Date(1761268892354);
console.log(date1); // 转换为对应日期
// 3.传入日期字符串
let date2 = new Date("2025-10-25T06:30:00Z"); // ISO 8601 格式,Z 表示 UTC
let date3 = new Date("2025-10-25T06:30:00+08:00"); // ISO 8601 格式,表示东八区时间
let date4 = new Date("October 24, 2025 14:30:00"); // 解析因浏览器/地区而异,可能不可靠
console.log(date2);
console.log(date3);
console.log(date4);
// 4.传入日期组件(年,月,日,时,分,秒,毫秒)
// 注意:月份是从0开始计数的(0=一月,1=二月,... , 11 = 十二月)
let specificDate = new Date(2025, 9, 24);
console.log(specificDate);

特别提醒:使用日期字符串解析时,如果没有指定时区,浏览器会将其解释为本地时间。

静态方法

Date 构造函数本身有一些有用的方法:

// Date.now() - 返回当前的时间戳(毫秒),无需创建 Date 对象
let timestamp = Date.now();
console.log(timestamp); // 例如: 1698388200000

// Date.parse() - 解析一个日期字符串并返回时间戳(毫秒)
let parsedTime = Date.parse("2025-10-24");
console.log(parsedTime); // 1761264000000

// Date.UTC() - 接受和构造函数相同的组件参数,但返回 UTC 时间的时间戳
let utcTimestamp = Date.UTC(2025, 9, 24); // 月份同样是 0-11
console.log(utcTimestamp);

实例方法(获取信息)

一旦有一个 Date 对象,可以获取其各个部分。方法通常有 本地时间UTC 时间 两个版本。

let date = new Date(2025, 9, 24, 9, 40, 35);
// 获取年,月,日,星期
console.log(date.getFullYear()); // 2025 (使用 getFullYear, 不要用 getYear)
console.log(date.getMonth()); // 9 (注意:月份是 0-11)
console.log(date.getDate()); // 24 (一个月中的第几天)
console.log(date.getDay()); // 5 (星期几,0=星期日, 6=星期六)
// 获取时、分、秒、毫秒
console.log(date.getHours()); // 9
console.log(date.getMinutes()); // 40
console.log(date.getSeconds()); // 35
console.log(date.getMilliseconds()); // 0
// 获取时间戳
console.log(date.getTime()); // 1761270035000
// UTC版本的方法(返回基于UTC的时间)
console.log(date.getUTCHours()); // 1(北京时间+8,所以UTC时间是9 - 8 = 1

实例方法(设置信息)

修改一个 Date 对象的值

let date = new Date();
// 设置年、月、日等
date.setFullYear(2025);
date.setMonth(9);
date.setDate(24);

date.setHours(9);
date.setMinutes(59);
date.setSeconds(20);
console.log(date.toLocaleDateString()); //2025/10/24

一个重要的特性:如果设置的值超出了合理范围,Date 对象会自动“溢出”并调整日期。

date = new Date(2025, 1, 29);
console.log(date); // 自动调整为2025-02-28

格式化方法

将 Date 对象转换为字符串。

let date = new Date();
console.log(date.toString()); // Fri Oct 24 2025 10:14:32 GMT+0800 (中国标准时间)
console.log(date.toDateString()); // Fri Oct 24 2025
console.log(date.toTimeString()); // 10:14:32 GMT+0800 (中国标准时间)

console.log(date.toLocaleDateString()); // 2025/10/24
console.log(date.toLocaleTimeString()); // 10:14:32
console.log(date.toLocaleString()); // 2025/10/24 10:14:32

// 用于网络传输或存储的标准格式
console.log(date.toISOString()); //2025-10-24T02:14:32.478Z
console.log(date.toUTCString()); // Fri, 24 Oct 2025 02:14:32 GMT
console.log(date.toJSON()); // 2025-10-24T02:14:32.478Z

日期计算与比较

由于 Date 对象底层是数字(时间戳),因此可以直接进行比较和数学运算

let start = new Date(2025, 0, 1); //2025年1月1日
let end = new Date(2025, 11, 31); // 2025年12月31日

// 比较日期
console.log(start < end); // true
// 计算日期差(毫秒)
let differenceInMs = end - start;
console.log(differenceInMs); // 两个日期之间相差的毫秒数
// 将毫秒转换为天
let differenceInDays = differenceInMs / (1000 * 60 * 60 * 24);
console.log(differenceInDays); // ~364天(取决于具体年份)

// 创建一个未来的日期(例如,10天后)
let today = new Date();
let tenDaysLater = new Date(today.getTime() + 10 * 24 * 60 * 60 * 1000);
console.log(tenDaysLater); // 2025-11-03T02:29:41.154Z

总结与最佳实践

  1. 月份陷阱:永远记住,JavaScript 中的月份是从 011
  2. 时区问题:在处理跨时区应用时,要明确你是在使用本地时间 (getHours) 还是 UTC 时间 (getUTCHours)。存储和传输时,推荐使用 toISOString() 生成的 UTC 时间。
  3. 性能:如果只需要当前时间戳,使用 Date.now()new Date().getTime() 更高效。
  4. 库的考虑:对于非常复杂的日期操作(如时区转换、复杂的格式化、节假日计算),建议使用成熟的库,如 Moment.js(较重)或 date-fns(轻量,函数式)。

RegExp(正则表达式)

RegExp(正则表达式)是 JavaScript 中用于模式匹配文本搜索/替换的强大工具。

创建正则表达式

可以通过字面量或者构造函数创建正则表达式,两者区别就是,前者编译时创建,后者运行时创建。

// 使用两个斜杠包围,可选的标志在后面
let pattern1 = /abc/;
let pattern2 = /abc/gi; // 带标志 g 和 i

// 使用 RegExp 构造函数
let pattern1 = new RegExp("abc");
let pattern2 = new RegExp("abc", "gi"); // 模式字符串,标志字符串

表示匹配模式的标记(Flags)

  • $g$:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。

    let globalPattern = /a/g;
    let str = "abcabc";
    console.log(str.match(globalPattern)); // ["a","a"]
    
  • $i$:不区分大小写,表示在查找匹配时忽略 pattern 和字符串的大小写。

    let caseInsensitive = /hello/i;
    console.log(caseInsensitive.test("Hello")); // true
    console.log(caseInsensitive.test("HELLO")); // true
    
  • $m$:多行模式,表示查找到一行文本末尾时会继续查找。

    let multiLine = /^test/m;
    let text = "first line\ntest line";
    let text2 = "first line test line";
    let text3 = `first line
                    test line`;
    console.log(multiLine.test(text)); // true
    console.log(multiLine.test(text2)); // false
    console.log(multiLine.test(text3)); // fasle
    
  • $y$:粘附模式,表示只查找从lastIndex开始及之后的字符串。

    let sticky = /a/y;
    sticky.lastIndex = 1;
    console.log(sticky.test("ba")); // true
    console.log(sticky.test("ab")); // false(从位置1开始不是a)
    
  • $u$:Unicode 模式,启用 Unicode 匹配。

    let unicodePattern = /\u{1F60A}/u;
    console.log(unicodePattern.test("😊")); //true
    
  • $s$:dotAll 模式,表示元字符,匹配任何字符(包括\n 或\r)

    let dotAll = /test.something/s;
    console.log(dotAll.test("test\nsomething")); // true
    

模式语法

正则表达式的模式由普通字符和特殊字符(元字符)组成。以下是一些常用的元字符:

  • 字符类

    • [abc]:匹配方括号内的任意一个字符。
    • [^abc]:匹配不在方括号内的任意字符。
    • [a-z]:匹配任意小写字母。
    • \d:匹配数字,等价于[0-9]
    • \D:匹配非数字。
    • \w:匹配字母、数字或下划线,等价于[A-Za-z0-9_]
    • \W:匹配非字母、数字、下划线。
    • \s:匹配任意空白字符(包括空格、制表符、换行符等)。
    • \S:匹配非空白字符。
    // 简单字符类
    let pattern1 = /[abc]/; // 匹配 a, b 或 c
    console.log("cdefga".match(pattern1)); // ["c"]
    
    let pattern2 = /[^abc]/; // 匹配除了 a, b, c 的任意字符
    console.log("cdefga".match(pattern2)); // ["d"]
    
    let pattern3 = /[a-z]/; // 匹配任意小写字母
    console.log("I am a kid".match(pattern3)); // ["a"]
    
    let pattern4 = /[A-Z]/; // 匹配任意大写字母
    console.log("I am a kid".match(pattern4)); // ["I"]
    
    let pattern5 = /[0-9]/; // 匹配数字
    console.log("我有1个苹果".match(pattern5)); // ["1"]
    
    // 预定义字符类
    let digit = /\d/; // 数字,等同于 [0-9]
    console.log(digit.test("abcdefg")); // false
    
    let notDigit = /\D/; // 非数字,等同于 [^0-9]
    console.log(notDigit.test("....")); // true
    
    let wordChar = /\w/; // 单词字符,等同于 [a-zA-Z0-9_]
    console.log(wordChar.test("1234569")); // true
    
    let notWordChar = /\W/; // 非单词字符
    console.log(notWordChar.test("...")); // true
    
    let whitespace = /\s/; // 空白字符(空格、制表符、换行等)
    console.log(whitespace.test("\n123")); // true
    
    let notWhitespace = /\S/; // 非空白字符
    console.log(notWhitespace.test(" ")); // fasle
    
  • 量词

    • *:匹配前一个元素 0 次或多次。
    • +:匹配前一个元素 1 次或多次。
    • ?:匹配前一个元素 0 次或 1 次。
    • {n}:匹配前一个元素恰好 n 次。
    • {n,}:匹配前一个元素至少 n 次。
    • {n,m}:匹配前一个元素至少 n 次,至多 m 次。
    // 基本量词
    let zeroOrMore = /a*/; // 0个或多个a
    let oneOrMore = /a+/; // 1个或多个a
    let zeroOrOne = /a?/; // 0个或1个a
    const str = "aaa";
    const str2 = "bbab";
    console.log(str.match(zeroOrMore)); // ["aaa"]
    console.log(str2.match(oneOrMore)); // ["a"]
    
    // 精确量词
    let exactlyThree = /a{3}/; // 正好3个a
    let threeOrMore = /a{3,}/; // 3个或更多a
    let betweenThreeAndFive = /a{3,5}/; // 3到5个a
    console.log(str.match(exactlyThree)); // ["aaa"]
    
    // 贪婪 vs 非贪婪
    let greedy = /a+/; // 匹配尽可能多的a
    let lazy = /a+?/; // 匹配尽可能少的a
    console.log(str.match(greedy)); // ["aaa"]
    console.log(str2.match(lazy)); // ["a"]
    
  • 位置匹配

    • ^:匹配字符串的开始(在多行模式下匹配行首)。
    • $:匹配字符串的结束(在多行模式下匹配行尾)。
    • \b:匹配单词边界。
    • \B:匹配非单词边界。
    let start = /^abc/; // 以abc开头
    let end = /abc$/; // 以abc结尾
    let wordBoundary = /\bword\b/; // 完整的单词"word"
    let notWordBoundary = /\Bword\B/; // word不能出现在单词边界
    
  • 分组和捕获

    • (pattern):捕获匹配的子字符串。
    • (?:pattern):非捕获分组,不捕获匹配的子字符串。
    • (?<name>pattern):命名捕获组,将匹配的子字符串捕获到名为name的组中。
    //捕获组
    let pattern = /(\d{4})-(\d{2})-(\d{2})/;
    let date = "2025-10-24";
    let match = pattern.exec(date);
    console.log(match[0]); // 2025-10-24 完整匹配
    console.log(match[1]); // 2025
    console.log(match[2]); // 10
    console.log(match[3]); // 24
    
    //命名捕获组
    pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
    let match2 = pattern.exec(date);
    console.log(match2.groups.year); // 2025
    console.log(match2.groups.month); // 10
    console.log(match2.groups.day); // 24
    
    //非捕获组
    // 使用 (?:) 表示不捕获的分组
    pattern = /(?:Mr|Mrs|Miss)\.(\w+)/;
    let string = "Hello,Mrs.Smith";
    let match3 = pattern.exec(string);
    console.log(match3);
    console.log(match3[1]); // Smith
    
  • 断言

    • x(?=y):正向肯定查找,匹配 x 仅当后面跟着 y。

      //正向前瞻 - 后面跟着特定模式
      let positiveLookahead = /abc(?=def)/; //匹配后面跟着def的abc
      console.log(positiveLookahead.test("abcdef")); // true
      console.log(positiveLookahead.test("abczxy")); // false
      
    • x(?!y):正向否定查找,匹配 x 仅当后面不跟着 y。

      //负向前瞻 - 后面不跟着特定模式
      let negativeLookahead = /abc(?!def)/; //匹配后面不跟着def的abc
      console.log(negativeLookahead.test("abcdef")); // false
      console.log(negativeLookahead.test("abczxy")); // true
      
    • (?<=y)x:反向肯定查找,匹配 x 仅当前面是 y。

      //正向后顾 - 前面是特定模式
      let positiveLookbehind = /(?<=abc)def/;
      console.log(positiveLookbehind.test("abcdef")); // true
      console.log(positiveLookbehind.test("xyzdef")); // false
      
    • (?<!y)x:反向否定查找,匹配 x 仅当前面不是 y。

      // 负向后顾 - 前面不是特定模式
      let negativeLookbehind = /(?<!abc)def/; // 匹配前面不是abc的def
      console.log(negativeLookbehind.test("abcdef")); // false
      console.log(negativeLookbehind.test("xyzdef")); // true
      

实例方法

  • test() - 测试是否匹配
  • exec() - 执行搜索,返回详细信息
  • toString() - 返回正则表达式字符串
  • [Symbol.match]() - 返回匹配项
let pattern = /test/gi;
// test()  - 测试是否匹配
console.log(pattern.test("This is a test")); // true

// exec() - 执行搜索,返回详细信息
pattern.lastIndex = 0; // 重置开始位置
let result = pattern.exec("test is test");
console.log(result[0]); // test
console.log(result.index); // 0
console.log(result.input); // test is test

//toString() - 返回正则表达式字符串
console.log(pattern.toString()); // /test/gi

// [Symbol.match]() - 被String.prototype.match调用
console.log("test".match(pattern)); // ["test"]

注意:如果同一个正则表达式对象多次调用 testexec,它会从 lastIndex 开始匹配。上述代码中pattern执行test之后,lastIndex属性已经指向了字符串的末尾,所以再次调用时从末尾开始匹配,找不到匹配项,返回 null。可以通过将lastIndex重置为 0 避免问题。

实例属性

let pattern = /test/gim;

console.log(pattern.source); // "test" - 模式字符串
console.log(pattern.flags); // "gim" - 标志字符串
console.log(pattern.global); // true - 是否有g标志
console.log(pattern.ignoreCase); // false - 是否有i标志
console.log(pattern.multiline); // true - 是否有m标志
console.log(pattern.dotAll); // false - 是否有s标志
console.log(pattern.unicode); // false - 是否有u标志
console.log(pattern.sticky); // false - 是否有y标志
console.log(pattern.lastIndex); // 0 - 下次匹配开始位置

字符串中使用正则表达式

let str = "Hello world,hello JavaScript";

// match() - 查找匹配
console.log(str.match(/hello/gi)); // ["Hello", "hello"]

// replace() - 替换匹配内容
console.log(str.replace(/hello/gi, "Hi")); // "Hi world, Hi JavaScript"

// search() - 查找匹配位置
console.log(str.search(/world/)); // 6

// split() - 用正则表达式分割字符串
console.log("a,b c;d".split(/[, ;]/)); // ["a", "b", "c", "d"]

原始值包装类型

JavaScript 中有 3 种特殊的引用类型,允许对字符串、数字、布尔值这些原始类型调用方法,用于在必要时将原始值转换为对象:

  • String - 对应字符串原始值
  • Number - 对应数字原始值
  • Boolean - 对应布尔原始值
// 原始值
let str = "hello"; // 字符串原始值
let num = 123; // 数字原始值
let bool = true; // 布尔原始值

// 对应的包装类型对象
let strObj = new String("hello"); // String 对象
let numObj = new Number(123); // Number 对象
let boolObj = new Boolean(true); // Boolean 对象

自动装箱(Auto-Boxing)

当对原始值调用方法时,JavaScript 会自动创建对应的包装对象:

let str = "hello world";

// 原始值调用方法时,JavaScript 内部会:
// 1. 创建 String 包装对象:new String(str)
// 2. 调用方法:String对象.substring(0, 5)
// 3. 销毁包装对象,返回结果
let result = str.substring(0, 5);

console.log(result); // "hello"
console.log(typeof str); // "string" - 仍然是原始值

其自动装箱过程可以用代码演示为:

let text = "hello world";
//当调用 text.substring() 时,实际上发生了:
let temp = new String(text); // 1.创建临时包装对象
let result = temp.substring(0, 5); // 2.调用方法
temp = null; // 3.销毁临时对象

console.log(result); // "hello"

自动拆箱(Auto-Unboxing)

当需要原始值时,包装对象会自动转换为原始值:

let strObj = new String("hello");
let numObj = new Number(100);
let boolObj = new Boolean(true);

// 自动拆箱为原始值
console.log(strObj + " world"); // "hello world"
console.log(numObj + 50); // 150
console.log(boolObj && false); // false

// 但类型检查仍然是对象
console.log(typeof strObj); // "object"
console.log(typeof new String("test")); // "object"

String 包装类型

创建方式:

// 字面量(推荐)
let str1 = "hello";

// 构造函数(不推荐)
let str2 = new String("hello");

console.log(str1 === "hello"); // true
console.log(str2 === "hello"); // false - 因为 str2 是对象
console.log(str2.valueOf() === "hello"); // true

String 对象的方法:

let text = "Hello World";

// 字符访问
console.log(text.charAt(1)); // "e"
console.log(text[1]); // "e" (ES6)

// 查找方法
console.log(text.indexOf("o")); // 4
console.log(text.lastIndexOf("o")); // 7
console.log(text.includes("World")); // true

// 大小写转换
console.log(text.toUpperCase()); // "HELLO WORLD"
console.log(text.toLowerCase()); // "hello world"

// 截取方法
console.log(text.substring(0, 5)); // "Hello"
console.log(text.slice(0, 5)); // "Hello"
console.log(text.substr(0, 5)); // "Hello"

// 分割和连接
console.log(text.split(" ")); // ["Hello", "World"]
console.log("a,b,c".split(",")); // ["a", "b", "c"]

// 替换
console.log(text.replace("World", "JavaScript")); // "Hello JavaScript"

// 去除字符串首尾的所有空白字符。这包括空格、制表符(tab)、换行符
console.log("  hello  ".trim()); // "hello"

//只去除字符串开头的空白字符:
console.log("  hello  ".trimStart()); // "hello  "
console.log("  hello  ".trimLeft()); // "hello  " (trimLeft 是 trimStart 的别名)

//只去除字符串末尾的空白字符:
console.log("  hello  ".trimEnd()); // "  hello"
console.log("  hello  ".trimRight()); // "  hello" (trimRight 是 trimEnd 的别名)

Number 包装类型

创建方式:

// 字面量
let num1 = 123.456;

// 构造函数
let num2 = new Number(123.456);

Number 对象的方法:

let num = 123.45678;

// 转换为字符串
console.log(num.toString()); // "123.45678"
console.log(num.toFixed(2)); // "123.46"
console.log(num.toExponential(2)); // "1.23e+2"
console.log(num.toPrecision(5)); // "123.46"

// 数值格式化
let price = 1234.5;
console.log(
  price.toLocaleString("zh-CN", {
    style: "currency",
    currency: "CNY",
  })
); // "¥1,234.50"

Number 的静态属性和方法:

// 静态属性
console.log(Number.MAX_VALUE); // 最大数值
console.log(Number.MIN_VALUE); // 最小正值
console.log(Number.MAX_SAFE_INTEGER); // 最大安全整数
console.log(Number.MIN_SAFE_INTEGER); // 最小安全整数

// 静态方法
console.log(Number.parseInt("123px")); // 123
console.log(Number.parseFloat("123.45")); // 123.45
console.log(Number.isNaN(NaN)); // true
console.log(Number.isFinite(123)); // true
console.log(Number.isInteger(123.0)); // true

Boolean 包装类型

创建方式:

// 字面量
let bool1 = true;

// 构造函数(不推荐)
let bool2 = new Boolean(true);

注意事项:

let falseObj = new Boolean(false);
let falseValue = false;

console.log(falseObj && true); // true (对象总是真值)
console.log(falseValue && true); // false

console.log(typeof falseObj); // "object"
console.log(typeof falseValue); // "boolean"

// 比较
console.log(falseObj == falseValue); // true (值相等)
console.log(falseObj === falseValue); // false (类型不同)

原始值 vs 包装对象

let primitive = "hello";
let wrapper = new String("hello");

console.log("=== 类型比较 ===");
console.log(typeof primitive); // "string"
console.log(typeof wrapper); // "object"

console.log("=== 值比较 ===");
console.log(primitive == "hello"); // true
console.log(wrapper == "hello"); // true (自动拆箱)
console.log(primitive === "hello"); // true
console.log(wrapper === "hello"); // false

console.log("=== 方法调用 ===");
console.log(primitive.toUpperCase()); // "HELLO" - 自动装箱
console.log(wrapper.toUpperCase()); // "HELLO"

单例内置对象

单例内置对象是 JavaScript 中非常重要的概念,它们在全局作用域中作为单个实例存在,不需要显式创建即可使用。

单例内置对象具有以下特点:

  • 全局唯一:在整个应用程序中只有一个实例
  • 无需实例化:直接使用,不需要 new 关键字
  • 内置功能:提供特定领域的实用功能
  • 全局可访问:在任何地方都可以直接调用

Global

在 JavaScript 中,Global 对象是一个抽象概念,在不同环境中有不同的实现:

// 在浏览器中,Global 对象是 window
console.log(window === this); // 在全局作用域中为 true

// 在 Node.js 中,Global 对象是 global
console.log(global);

// ES2020 引入的 globalThis,统一了不同环境的全局对象
console.log(globalThis);

Global对象的属性和方法如下:

// 全局值属性
console.log(undefined); // 全局的 undefined
console.log(Infinity); // 无穷大
console.log(NaN); // 非数字

// 全局函数
isNaN(123); // 检查是否为 NaN
isFinite(123); // 检查是否为有限数
parseInt("123"); // 字符串转整数
parseFloat("123.45"); // 字符串转浮点数
eval("2 + 2"); // 执行字符串代码(慎用!)

// URI 编码方法
let uri = "https://example.com/测试路径";
console.log(encodeURI(uri)); // 编码整个 URI
console.log(encodeURIComponent(uri)); // 编码 URI 组件
console.log(decodeURI(encoded)); // 解码 URI

Math

Math 对象提供了数学常数和数学函数。

  • 数学常熟

    console.log(Math.PI); // 3.141592653589793
    console.log(Math.E); // 2.718281828459045
    console.log(Math.LN2); // 0.693 - 2的自然对数
    console.log(Math.LN10); // 2.302 - 10的自然对数
    console.log(Math.LOG2E); // 1.442 - 以2为底e的对数
    console.log(Math.LOG10E); // 0.434 - 以10为底e的对数
    console.log(Math.SQRT2); // 1.414 - 2的平方根
    console.log(Math.SQRT1_2); // 0.707 - 1/2的平方根
    
  • 计算方法

    // 基本运算
    console.log(Math.abs(-5)); // 5 - 绝对值
    console.log(Math.sqrt(16)); // 4 - 平方根
    console.log(Math.cbrt(27)); // 3 - 立方根
    console.log(Math.pow(2, 3)); // 8 - 幂运算
    console.log(Math.exp(1)); // 2.718 - e的指数
    
    // 对数运算
    console.log(Math.log(Math.E)); // 1 - 自然对数
    console.log(Math.log10(100)); // 2 - 以10为底的对数
    console.log(Math.log2(8)); // 3 - 以2为底的对数
    
  • 舍入方法

    let num = 3.75;
    
    console.log(Math.round(num)); // 4 - 四舍五入
    console.log(Math.floor(num)); // 3 - 向下取整
    console.log(Math.ceil(num)); // 4 - 向上取整
    console.log(Math.trunc(num)); // 3 - 去除小数部分
    
    // 处理负数
    console.log(Math.floor(-3.75)); // -4
    console.log(Math.trunc(-3.75)); // -3
    
  • 三角方法

    let angle = Math.PI / 4; // 45度
    
    console.log(Math.sin(angle)); // 0.707 - 正弦
    console.log(Math.cos(angle)); // 0.707 - 余弦
    console.log(Math.tan(angle)); // 1 - 正切
    
    // 反三角函数
    console.log(Math.asin(0.5)); // 0.523 - 反正弦
    console.log(Math.acos(0.5)); // 1.047 - 反余弦
    console.log(Math.atan(1)); // 0.785 - 反正切
    console.log(Math.atan2(1, 1)); // 0.785 - 两点间角度
    
  • 最大/最小值

    console.log(Math.max(1, 3, 2)); // 3
    console.log(Math.min(1, 3, 2)); // 1
    
    // 处理数组
    let numbers = [1, 3, 2, 5, 4];
    console.log(Math.max(...numbers)); // 5 - 使用扩展运算符
    console.log(Math.min.apply(null, numbers)); // 1 - 使用 apply
    
  • 随机数

    // 生成 [0, 1) 之间的随机数
    console.log(Math.random());
    
    // 实用随机数函数
    function getRandomInt(min, max) {
      return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    
    function getRandomFloat(min, max) {
      return Math.random() * (max - min) + min;
    }
    
    // 生成随机颜色
    function getRandomColor() {
      return `#${Math.floor(Math.random() * 0xffffff)
        .toString(16)
        .padStart(6, "0")}`;
    }
    

JSON

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,它属于 JavaScript 的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。

  • JSON.stringify()

    const obj = {
      name: "张三",
      age: 25,
      hobbies: ["阅读", "运动"],
      address: {
        city: "北京",
        street: "朝阳路",
      },
      date: new Date(),
      score: undefined, // 会被忽略
      func: function () {}, // 会被忽略
    };
    
    // 基本序列化
    console.log(JSON.stringify(obj));
    
    // 带缩进的格式化输出
    console.log(JSON.stringify(obj, null, 2));
    
    // 使用替换函数
    function replacer(key, value) {
      if (typeof value === "string") {
        return value.toUpperCase();
      }
      return value;
    }
    console.log(JSON.stringify(obj, replacer, 2));
    
    // 只序列化指定属性
    console.log(JSON.stringify(obj, ["name", "age"], 2));
    
  • JSON.parse()

    const jsonString = '{"name":"张三","age":25,"hobbies":["阅读","运动"]}';
    
    // 基本解析
    const parsed = JSON.parse(jsonString);
    console.log(parsed.name); // "张三"
    
    // 使用 reviver 函数
    const revived = JSON.parse(jsonString, function (key, value) {
      if (key === "age") {
        return value + 1; // 年龄加1
      }
      return value;
    });
    console.log(revived.age); // 26
    
  • 错误处理

    function safeParse(jsonString) {
      try {
        return JSON.parse(jsonString);
      } catch (error) {
        console.error("JSON 解析错误:", error.message);
        return null;
      }
    }
    
    // 有效的 JSON
    console.log(safeParse('{"name": "John"}')); // {name: "John"}
    
    // 无效的 JSON
    console.log(safeParse('{name: "John"}')); // null, 输出错误信息
    

console 对象

// 不同级别的日志
console.log("普通信息");
console.info("提示信息");
console.warn("警告信息");
console.error("错误信息");
console.debug("调试信息");

// 分组日志
console.group("用户信息");
console.log("姓名: 张三");
console.log("年龄: 25");
console.groupEnd();

// 表格显示
console.table([
  { name: "张三", age: 25 },
  { name: "李四", age: 30 },
]);

// 性能测量
console.time("操作计时");
// 执行某些操作
console.timeEnd("操作计时");

Intl 对象(国际化)

// 日期格式化
const date = new Date();
console.log(new Intl.DateTimeFormat("zh-CN").format(date));
console.log(new Intl.DateTimeFormat("en-US").format(date));

// 数字格式化
const number = 1234567.89;
console.log(new Intl.NumberFormat("zh-CN").format(number)); // 1,234,567.89
console.log(new Intl.NumberFormat("de-DE").format(number)); // 1.234.567,89

// 货币格式化
const price = 1234.56;
console.log(
  new Intl.NumberFormat("zh-CN", {
    style: "currency",
    currency: "CNY",
  }).format(price)
); // ¥1,234.56

集合引用类型

集合引用类型是 JavaScript 中用于存储和操作数据集合的重要工具,主要包括 ArrayMapSetWeakMapWeakSet 等。

Array

ECMAScript 数组是一组有序数据,数组中每个元素可以是任意类型的数据。ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。

创建数组

有几种方式可以创建数组:

// 1. 数组字面量
let fruit = ["apple", "banana", "orange"];
let emptyArray = [];

// 2. Array构造函数
let colors = new Array("red", "blue", "green");
let numbers = new Array(5); // 长度为5的空数组
let names = new Array("Mark");
console.log(colors); // [ 'red', 'blue', 'green' ]
console.log(numbers); // [ <5 empty items> ]
console.log(names); // [ 'Mark' ]

// 3. Array.of()
let arr1 = Array.of(1, 2, 3); // [1,2,3]
let arr2 = Array.of(5); // [5] - 与new Array(5)不同
console.log(arr1); // [ 1, 2, 3 ]
console.log(arr2); // [ 5 ]

// 4.Array.from() - 从类数组对象或可迭代对象创建
let fromString = Array.from("hello");
let fromSet = Array.from(new Set([1, 2, 3]));
console.log(fromString); // [ 'h', 'e', 'l', 'l', 'o' ]
console.log(fromSet); // [ 1, 2, 3 ]

使用构造函数时,如果只传入一个数值,则会创建一个长度为指定长度的空数组。

Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:from()of()

from()用于将类数组结构(任何可迭代的结构,或者有一个 length 属性 和可索引元素的结构)转换为数组实例,而of()用于将一组参数转换为数组实例。

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位。ES6 新增的方法和迭代器普遍将这 些空位当成存在的元素,只不过值为undefined

//数组空位
const options = [1, , , , 5];
for (const option of options) {
  console.log(option === undefined); // false true true true false
}

ES6 之前的方法会忽略这个空位,但具体的行为也会因方法而异

const options = [1, , , , 5];
//map()会跳过空位置
console.log(options.map(() => 6)); //[ 6, <3 empty items>, 6 ]
//join()视空位置为字符串
console.log(options.join("-")); // 1----5

由于行为不一致和存在性能隐患,如果确实需要空位,则可以显式地用undefined值代替。

数组索引

要取得或设置数组的值,需要使用中括号并提供相应值的数字索引

let colors = ["red", "blue", "green"];
colors[2] = "black"; // 修改第三项
colors[3] = "brown"; //添加第四项
console.log(colors); // [ 'red', 'blue', 'black', 'brown' ]

在设置数组的值时,如果设置超过数组最大索引的索引,则数组长度会自动扩展到该索引值+1,这中间的所有元素在访问时会返回 undefined:

let colors = ["red", "blue", "green"];
colors[99] = "black";
console.log(colors.length); // 100
console.log(colors[3]); // undefined

除此之外,修改数组length属性,可以从数组末尾删除或添加元素。

let colors = ["red", "blue", "green"];
colors.length = 2;
console.log(colors[2]); // undefined

检测数组

一个经典的 ECMAScript 问题是判断一个对象是不是数组。在只有一个网页(只有一个全局作用域)的情况下,使用instanceof操作符即可。

使用instanceof的问题是假定只有一个全局执行上下文。如果网页有多个框架,可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。此时,可以使用 ECMAScript 提供的Array.isArray(),这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。

let arr = [1, 2, 3];

console.log(Array.isArray(arr)); // true
console.log(arr instanceof Array); // true
console.log(Array.isArray({})); // false

迭代器方法

在 ES6 中,Array 的原型上暴露了 3 个用于检索数组内容的方法:keys()values()entries()

  • keys() :返回数组索引的迭代器
  • values():返回数组元素的迭代器
  • entries():返回索引/值对的迭代器
const fruits = ["apple", "banana", "cherry"];

// 通过 for...of 循环遍历,并结合解构赋值:
for (const [index, value] of fruits.entries()) {
  console.log(`Index ${index} has value: ${value}`);
}

复制和填充方法

ES6 新增了两个方法:批量复制方法copyWithin()和填充数组方法fill(),这两个方法指定的范围都是前闭后开

$copyWithin(target, start, end)$:在数组内部批量复制元素覆盖指定位置。

参数含义:

  • target: 目标起始索引
  • start: 源起始索引(可选)
  • end: 源结束索引(可选)
let arr1 = [1, 2, 3, 4, 5];

// 将索引3到末尾的元素复制到索引0开始的位置
arr1.copyWithin(0, 3);
console.log(arr1); // [4, 5, 3, 4, 5]

let arr2 = [1, 2, 3, 4, 5];

// 将索引3到4之前的元素复制到索引0开始的位置
arr2.copyWithin(0, 3, 4);
console.log(arr2); // [4, 2, 3, 4, 5]

//使用负数引用
let arr3 = [1, 2, 3, 4, 5];

// 将倒数第2个到倒数第1个之前的元素复制到索引0
arr3.copyWithin(0, -2, -1);
console.log(arr3); // [4, 2, 3, 4, 5]

边界情况处理:

  • 索引超出范围会被静默忽略
  • 索引反向(start > end)则不会执行任何操作
  • 目标位置空间不足时,只复制能容纳的元素

$fill(value,start,end)$:将数组中的一些或所有元素替换为固定值。

参数含义:

  • value: 填充值
  • start: 起始索引(可选)
  • end: 结束索引(可选)
// 用固定值填充整个数组
let arr1 = [1, 2, 3, 4];
arr1.fill(0);
console.log(arr1); // [ 0, 0, 0, 0 ]

// 初始化指定长度的数组
let arr2 = new Array(3).fill("a");
console.log(arr2); // [ 'a', 'a', 'a' ]

let arr3 = ["a", "b", "c", "d"];
// 从索引1开始填充到索引3之前(即索引1和2)
arr3.fill(7, 1, 3);
console.log(arr3); // [ 'a', 7, 7, 'd' ]

//使用负数索引(从数组末尾开始计算)
let arr4 = [1, 2, 3, 4, 5];
arr4.fill(0, -3, -1); // 相当于从索引2填充到索引4
console.log(arr4); // [ 1, 2, 0, 0, 5 ]

当填充值为对象时,所有元素将指向同一个对象的引用

let arr5 = new Array(3).fill({ name: "John" });
arr5[0].name = "Jane";
console.log(arr5[1].name); // "Jane" (所有元素都被修改了)

如果需要填充不同的对象,建议使用Array.from()

let arr6 = Array.from({ length: 3 }, () => ({ name: "John" }));
arr6[0].name = "Jane";
console.log(arr6[1].name); // "John" (所有元素都是不同的引用)

转换方法

所有对象都有toLocaleString()toString()valueOf()方法。其中,valueOf()返回的还是数组本身。而toString()就是对数组的每个值都会调用其toString()方法,以得到最终的字符串,同理,toLocaleString()就是对数组的每个值都会调用其toLocaleString()方法。

let color1 = {
  toLocaleString() {
    return "orangered";
  },
  toString() {
    return "red";
  },
};
let color2 = {
  toLocaleString() {
    return "skyblue";
  },
  toString() {
    return "blue";
  },
};
let colors = [color1, color2];
console.log(colors.toString()); // "red,blue"
console.log(colors.toLocaleString()); // "orangered,skyblue"

如果想使用不同的分隔符,可以使用join()方法。join()方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。如果不给join()传入任何参数,或者传入undefined,则仍然使用逗号作为分隔符。

let colors = ["red", "blue", "green"];
console.log(colors.join("-")); // red-blue-green
console.log(colors.join("||")); // red||blue||green

注意:如果数组中某一项是null或者undefined,则在join()toLocaleString()toString()valueOf()返回的结果中会以空字符串表示。

栈和队列方法

是一种后进先出的结构,也就是最近添加的项先被删除。数据项的插入(称为推入,push)和删除(称为弹出,pop)只在栈的一个地方发生,即栈顶。ECMAScript 数组提供了push()pop()方法,以实现类似栈的行为。

  • push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。
  • pop()方法用于删除数组的最后一项,同时减少数组的length值,返回被删除的值。
let colors = new Array();
let count = colors.push("red", "green");
console.log(count); // 2
count = colors.push("black");
console.log(count); // 3

let item = colors.pop();
console.log(item); // black
console.log(colors.length); // 2

队列先进先出形式限制访问,队列列表末尾添加数据,但从列表开头获取数据。数组提供了shift()方法从数组开头取得数据。它会从数组的第一项并返回它,然后数组长度减 1。

let colors = new Array();
let count = colors.push("red", "greed");
console.log(count); // 2
count = colors.push("black");
console.log(count); // 3

let item = colors.shift(); // 取得第一项
console.log(item); // red
console.log(colors.length); // 2

ECMAScript 也为数组提供了unshift()方法。unshift()就是执行跟shift()相反的操作:接收任意数量的参数,并将它们添加到数组开头,返回数组的最新长度。

let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green");
console.log(count); // 2

count = colors.unshift("black");
console.log(count); // 3

let item = colors.pop();
console.log(item); // green
console.log(colors.length); // 2

排序方法

数组有两个方法可以用来对元素重新排序:reverse()sort()

  • reverse()方法就是将数组元素反向排列。
  • sort()默认情况下会按照升序排列,它会在每一项上调用String()转换为字符串(即使元素是数值),比较字符串决定顺序。除此之外,sort()方法可以接收一个比较函数,用于判断哪个值排在前面。比较函数接收两个参数ab,并且需要返回一个负数、零或正数,来表示a应该排在b之前、相同还是之后。
let numbers = [1, 5, 10, 2, 8];

// 默认排序(按字符串)
console.log(numbers.sort()); // [1, 10, 2, 5, 8]

// 数值排序
console.log(numbers.sort((a, b) => a - b)); // [1, 2, 5, 8, 10]
console.log(numbers.sort((a, b) => b - a)); // [10, 8, 5, 2, 1]

// 对象数组排序
let users = [
  { name: "John", age: 30 },
  { name: "Jane", age: 25 },
  { name: "Bob", age: 35 },
];
console.log(users.sort((a, b) => a.age - b.age));

// 反转
console.log(numbers.reverse()); // [10, 8, 5, 2, 1]

操作方法

  • concat():用于连接两个或多个数组,不改变现有数据,返回连接后的新副本。在合并数组时,如果将被合并数组的属性[Symbol.isConcatSpreadable],数组将作为单个元素进行合并。

    let colors = ["red", "green", "blue"];
    let newColors = ["black", "brown"];
    let moreNewColors = ["pink", "cyan"];
    moreNewColors[Symbol.isConcatSpreadable] = false;
    
    let colors2 = colors.concat(newColors);
    let colors3 = colors.concat(moreNewColors);
    console.log(colors2); // [ 'red', 'green', 'blue', 'black', 'brown' ]
    console.log(colors3); // [ 'red', 'green', 'blue',  ["pink", "cyan"]]
    
  • slice():截取数组,创建一个包含原有数组中一个或多个元素的新数组。接收一个或两个参数[start,end)。如果只有一个参数,则slice()会返回该索引到数组末尾的所有元素。如果有两个参数,则slice()返回从开始索引到结束索引对应的所有元素,其中不包含结束索引对应的元素。

    let colors = ["red", "green", "blue", "pink", "brown"];
    let colors2 = colors.slice(1);
    let colors3 = colors.slice(1, 3);
    let colors4 = colors.slice(-3);
    console.log(colors2); // [ 'green', 'blue', 'pink', 'brown' ]
    console.log(colors3); // [ 'green', 'blue' ]
    console.log(colors4); // [ 'blue', 'pink', 'brown' ]
    
  • splice():最强大的数组方法,它有 3 种不同的方式。

    • 删除:需要给splice()传 2 个参数,要删除的第一个元素的位置和要删除的元素数量。
    • 插入:需要给splice()传 3 个参数,开始位置、0(要删除的元素数量)和要插入的元素。
    • 替换splice()在删除元素的同时可以在指定位置插入新元素,同样要传入 3 个参数,开始位置、要删除元素的数量和要插入的任意多个元素。
    let arr = [1, 2, 3, 4, 5];
    
    // 删除
    let removed = arr.splice(0, 2); // 从索引0开始删除2个元素
    console.log(arr); // [3, 4, 5]
    console.log(removed); // [1, 2]
    
    // 插入
    arr.splice(1, 0, "a", "b"); // 从索引1开始插入,不删除
    console.log(arr); // [3, 'a', 'b', 4, 5]
    
    // 替换
    arr.splice(2, 1, "c"); // 从索引2开始删除1个元素,插入'c'
    console.log(arr); // [3, 'a', 'c', 4, 5]
    

搜索和位置方法

ECMAScript 提供两类搜索数组的方法:按严格相等搜索按断言函数搜索

ECMAScript 提供了 3 个严格相等的搜索方法:indexOf()lastIndexOf()includes()。这些方法都接收两个参数:要查找的元素和一个可选的起始搜索位置。indexOf()includes()方法从数组前头(第一项)开始向后搜索,而lastIndexOf()从数组末尾(最后一项)开始向前搜索。

indexOf()lastIndexOf()都返回要查找的元素在数组中的位置,如果没找到则返回-1。includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。在比较第一个参数跟数组每一 项时,会使用全等(===)比较,也就是说两项必须严格相等。

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];

// 严格相等搜索
console.log(numbers.indexOf(3)); // 2
console.log(numbers.lastIndexOf(3)); // 6
console.log(numbers.includes(3)); // true

断言函数接收 3 个参数:元素、索引和数组本身。其中元素是数组中当前搜索的元素,索引是当前元素的索引,而数组就是正在搜索的数组。

find()findIndex()方法使用了断言函数。这两个方法都从数组的最小索引开始。find()返回第一个匹配的元素,findIndex()返回第一个匹配元素的索引。这两个方法也都接收第二个可选的参数, 用于指定断言函数内部 this 的值。

findLast()findLastIndex()与上面 2 个方法类似,区别就是它们是从后向前查找第一个满足条件的。

// 断言函数搜索
let users = [
  { id: 1, name: "John" },
  { id: 2, name: "Jane" },
  { id: 3, name: "Bob" },
];

console.log(users.find((user) => user.id === 2)); // {id: 2, name: 'Jane'}
console.log(users.findIndex((user) => user.name === "Bob")); // 2

// 寻找索引(ES2023)
console.log(users.findLast((user) => user.id < 3)); // {id: 2, name: 'Jane'}
console.log(users.findLastIndex((user) => user.id < 3)); // 1

迭代方法

ECMAScript 为数组定义了 5 个迭代方法。每个方法接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)。传给每个方法的函数接收 3 个参数:数组元素元素索引数组本身

  • filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。
  • forEach():对数组每一项都运行传入的函数,没有返回值。
  • map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
  • every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。
  • some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。
let numbers = [1, 2, 3, 4, 5];

// forEach - 遍历执行
numbers.forEach((item, index, array) => {
  console.log(`索引 ${index}: ${item}`);
});

// map - 映射新数组
let doubled = numbers.map((num) => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter - 过滤数组
let evens = numbers.filter((num) => num % 2 === 0);
console.log(evens); // [2, 4]

// every / some - 条件检查
console.log(numbers.every((num) => num > 0)); // true
console.log(numbers.some((num) => num > 3)); // true

归并方法

ECMAScript 为数组提供了两个归并方法:reduce()reduceRight()。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。reduce()方法从数组第一项开始遍历到最后一项。而reduceRight()从最后一项开始遍历至第一项。

这两个方法都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。 传给reduce()reduceRight()的函数接收 4 个参数:上一个归并值当前项当前项的索引数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始。

let numbers = [1, 2, 3, 4, 5];
// reduce - 累加器
let sum = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // 15

// reduceRight - 从右向左
let reversed = numbers.reduceRight((acc, curr) => [...acc, curr], []);
console.log(reversed); // [5, 4, 3, 2, 1]

// reduce 的复杂应用
let stats = numbers.reduce(
  (acc, curr) => {
    acc.sum += curr;
    acc.product *= curr;
    return acc;
  },
  { sum: 0, product: 1 }
);

console.log(stats); // {sum: 15, product: 120}

定型数组(Typed Arrays)

定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。

// 创建缓冲区
let buffer = new ArrayBuffer(16); // 16字节
let buffer2 = buffer.slice(4, 12);
console.log(buffer2.byteLength); //8

// 创建视图
let int32View = new Int32Array(buffer);
let float64View = new Float64Array(buffer);

// 操作数据
int32View[0] = 42;
console.log(int32View[0]); // 42

// 不同的数值类型
let int8 = new Int8Array([1, 2, 3]);
let uint8 = new Uint8Array([1, 2, 3]);
let float32 = new Float32Array([1.1, 2.2, 3.3]);

Map

作为 ECMAScript 6 的新增特性,Map 是一种新的集合类型,实现真正的键/值存储机制。

  • Map的键可以是任意类型
  • Map是可遍历的,有size属性
  • Map的键是强引用的,即使对象不再被其他地方使用,Map中仍然保留着该对象的引用,从而阻止垃圾回收

基本操作

let map = new Map();

// 添加键值对
map.set("name", "John");
map.set(1, "number one");
map.set({}, "object key");

// 获取值
console.log(map.get("name")); // 'John'
console.log(map.get(1)); // 'number one'

// 检查存在
console.log(map.has("name")); // true

// 删除
map.delete(1);

// 大小
console.log(map.size); // 2

// 清空
map.clear();

使用 new 关键字和 Map 构造函数可以创建一个空映射,初始化之后,可以使用set()方法再添加键/值对。另外,可以使用get()has()进行查询,可以通过 size 属性获取映射中的键/值对的数量,还可以使用delete()clear()删除值

顺序与迭代

Map 保持插入顺序,并提供多种迭代方式:

let map = new Map([
  ["name", "John"],
  ["age", 30],
  ["city", "New York"],
]);

// 遍历键
for (let key of map.keys()) {
  console.log(key);
}

// 遍历值
for (let value of map.values()) {
  console.log(value);
}

// 遍历键值对
for (let [key, value] of map.entries()) {
  console.log(`${key}: ${value}`);
}

// forEach 方法
map.forEach((value, key) => {
  console.log(`${key} = ${value}`);
});

// 转换为数组
console.log([...map]); // [['name', 'John'], ['age', 30], ['city', 'New York']]
console.log(Array.from(map)); // 同上

Map 与 Object 的区别

// 1. 键的类型
let map = new Map();
let obj = {};

map.set(1, "number");
obj[1] = "number"; // 键会被转换为字符串 "1"

map.set({}, "object");
obj[{}] = "object"; // 键会被转换为字符串 "[object Object]"

// 2. 顺序保证
map.set("a", 1).set("b", 2).set("c", 3);
console.log([...map.keys()]); // ['a', 'b', 'c'] - 插入顺序

// 3. 性能
// Map 在频繁增删键值对的场景下性能更好
// Object 在频繁查找的场景下可能有优化

WeakMap

在 JavaScript 中,WeakMap 是一种键值对的集合,其中键必须是对象,而值可以是任意类型。WeakMap 与普通的 Map 有几个关键的区别:

  1. 键必须是对象:不能使用基本类型(如数字、字符串、符号等)作为键。
  2. 弱引用:WeakMap 对键是弱引用的。这意味着,如果没有其他引用指向该键(对象),则该键值对会被垃圾回收机制回收。
  3. 不可枚举:WeakMap 没有方法可以获取所有键或所有值,因此无法遍历 WeakMap。也没有 size 属性。

由于这些特性,WeakMap 常用于存储与对象关联的元数据,而不会阻止这些对象被垃圾回收。这对于管理内存和避免内存泄漏非常有用。

let obj1 = { name: "obj1" };
let obj2 = { name: "obj2" };

let weakMap = new WeakMap([
  [obj1, "value1"],
  [obj2, "value2"],
]);

weakMap = new WeakMap();
let obj1 = {};
let obj2 = {};

// 键必须是对象
weakMap.set(obj1, "value1");
weakMap.set(obj2, "value2");

console.log(weakMap.get(obj1)); // 'value1'

// 没有 size 属性,不能遍历
// console.log(weakMap.size); // undefined

// 当对象被垃圾回收时,对应的键值对会自动删除
obj1 = null; // obj1 可以被垃圾回收

WeakMap 只有以下四个方法:

  1. set(key, value):向 WeakMap 中添加一个键值对。返回这个 WeakMap 对象,因此可以使用链式调用。
  2. get(key):返回与键关联的值,如果键不存在则返回 undefined。
  3. has(key):返回一个布尔值,表示 WeakMap 中是否存在该键。
  4. delete(key):删除与键关联的值。删除成功返回 true,如果键不存在则返回 false。

注意:WeakMap 没有clear方法,也没有size属性和遍历方法(如keys(), values(), entries())。

应用场景

1. 私有属性模拟
const privateData = new WeakMap();

class Person {
  constructor(name, age) {
    // 将实例与私有数据关联
    privateData.set(this, {
      name: name,
      age: age,
    });
  }

  getName() {
    return privateData.get(this).name;
  }

  getAge() {
    return privateData.get(this).age;
  }
}

const person = new Person("Alice", 30);
console.log(person.getName()); // "Alice"
// 无法直接访问 privateData 中的信息
2.DOM 元素元数据存储
const elementData = new WeakMap();

const button = document.createElement("button");

// 给这个节点关联一些元数据
elementData.set(button, { disabled: true });

// 当 button 从 DOM 中移除并被垃圾回收时,对应的元数据也会被自动清理

Set

Set用于存储唯一值,集合中的值不重复Set在很多方面都像是加强的Map,因为它们的大多数 API 和行为都是共有的。

基本 API

使用new关键字和Set构造函数可以创建一个空集合,初始化之后,可以使用add()增加值,使用has()查询,通过size取得元素数量,以及使用delete()clear()删除元素。

let set = new Set();
// 添加值
set.add(1);
set.add(2);
set.add(2); // 重复值会被忽略
set.add("hello");
set.add({}); // 对象引用不同,可以添加
console.log(set.size); // 4
// 检查存在
console.log(set.has(1)); // true
// 删除
set.delete(1);
// 清空
set.clear();

顺序与迭代

Set会维护值插入时的顺序,支持按顺序迭代。

let set = new Set([1, 2, 3, 4, 5]);

// 遍历值
for (let value of set) {
  console.log(value);
}

// forEach
set.forEach((value) => {
  console.log(value);
});

// 转换为数组
console.log([...set]); // [1, 2, 3, 4, 5]
console.log(Array.from(set)); // 同上

应用场景

利用 Set 存储的值不重复的特性,可以用于以下场景:

// 1. 数组去重
let numbers = [1, 2, 2, 3, 4, 4, 5];
let unique = [...new Set(numbers)];
console.log(unique); // [ 1, 2, 3, 4, 5 ]

// 2. 集合运算
let setA = new Set([1, 2, 3]);
let setB = new Set([2, 3, 4]);

// 并集
let union = new Set([...setA, ...setB]);
console.log([...union]); // [ 1, 2, 3, 4 ]

//交集
let intersection = new Set([...setA].filter((x) => setB.has(x)));
console.log([...intersection]); // [ 2, 3 ]

//差集
let difference = new Set([...setA].filter((x) => !setB.has(x)));
console.log([...difference]); // [ 1 ]

WeakSet

WeakSetSet的“兄弟”类型,它具有以下特性:

  • 仅存储对象:成员必须为对象(如数组、字符串等原始值无法添加),且通过弱引用方式持有对象,不会阻止垃圾回收。
  • 自动回收:当对象无其他引用时,JavaScript 垃圾回收器可自动回收其内存
  • 不可迭代:不支持 for...of 循环等遍历方式,仅可通过 has、delete 等方法操作
let weakSet = new WeakSet();
let obj1 = {};
let obj2 = {};

weakSet.add(obj1);
weakSet.add(obj2);

console.log(weakSet.has(obj1)); // true

weakSet.delete(obj1);

// 同样没有 size,不能遍历

应用场景

// 1. 私有属性模拟
const privateData = new WeakMap();

class Person {
  constructor(name) {
    privateData.set(this, { name });
  }

  getName() {
    return privateData.get(this).name;
  }
}

// 2. DOM 元素关联数据
let element = document.getElementById("myElement");
let elementData = new WeakMap();
elementData.set(element, { clicks: 0 });

// 当 DOM 元素被移除时,关联数据自动清理

迭代与扩展

ECMAScript 6 新增的迭代器和扩展操作符对集合引用类型特别有用。这些新特性让集合类型之间相互操作、复制和修改变得异常方便。

迭代

迭代是指按照顺序访问集合中的每个元素的过程。在 JavaScript 中,迭代通常通过迭代器(Iterator)可迭代协议(Iterable Protocol)来实现。

可迭代协议

一个对象要成为可迭代的,它必须实现@@iterator方法,这意味着该对象(或者它原型链上的某个对象)必须有一个键为Symbol.iterator的属性,该属性是一个无参数的函数,返回一个迭代器对象。

let arr = ["foo"];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 'foo', done: false }
console.log(iter.next()); // { value: undefined, done: true }

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next()方法在可迭代对象中遍历数据。每次成功调用next(),都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。若不调用next(),则无法知道迭代器的当前位置。

next()方法返回的迭代器对象IteratorResult包含两个属性:donevaluedone是一个布尔值,表示是否还可以再次调用next()取得下一个值;value 包含可迭代对象的下一个值(done 为 false),或者 undefined(done 为 true)。done: true 状态称为“耗尽”。

任何实现 Iterator 接口的对象都可以作为迭代器使用。

// 自定义可迭代对象
const myIterable = {
  data: [1, 2, 3, 4, 5],
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  },
};

for (const item of myIterable) {
  console.log(item); // 1, 2, 3, 4, 5
}

提前终止迭代器

可选的return()方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:

  • for-of 循环通过breakcontinuereturn throw 提前退出;
  • 解构操作并未消费所有值。

假设我们有一个生成数字 1 到 5 的迭代器,我们希望在提前终止时执行一些清理工作(例如,关闭文件、释放资源等)。

const myIterable = {
  data: 1,
  [Symbol.iterator]() {
    let count = this.data;
    return {
      next() {
        if (count >= 5) {
          return { value: undefined, done: true };
        }
        return { value: count++, done: false };
      },

      // 可选的 return 方法,用于提前终止
      return(value) {
        console.log("迭代器被提前终止");
        return { value, done: true };
      },

      // 可选的 throw 方法,用于处理错误
      throw(error) {
        console.log("迭代器抛出错误:", error);
        return { value: undefined, done: true };
      },
    };
  },
};

// 情况1:使用for...of循环,提前break
for (const value of myIterable) {
  console.log(value);
  if (value === 3) {
    break; // 提前终止,会触发return方法
  }
}

//情况2:使用for...of循环,continue
for (const value of myIterable) {
  console.log(value);
  if (value === 3) {
    continue; // 跳过一次迭代, 不会触发return 方法
  }
}

//情况3:使用解构赋值,只取前两个值
const [a, b] = myIterable;
console.log(a, b); // 取前两个值,然后会提前终止

//情况4:手动调用return方法
console.log("\n手动调用return:");
const iter = myIterable[Symbol.iterator]();
console.log(iter.next()); // { value: 1, done: false }
iter.return(); // 迭代器被提前终止
console.log(iter.next()); // { value: 2, done: false }

//清空5:使用 throw() 方法处理错误
try {
  for (const value of myIterable) {
    console.log(value);
    if (value === 3) {
      throw new Error("模拟错误"); // 迭代器被提前终止
    }
  }
} catch (error) {
  console.log("捕获到错误:", error.message); // 捕获到错误: 模拟错误
}

生成器

生成器是 ES6 引入的一种特殊函数,它可以通过function*来定义,并且使用yield关键字来暂停和恢复函数的执行。生成器函数返回一个生成器对象,该对象符合可迭代协议迭代器协议

基本概念

  1. 定义生成器函数:使用function*声明。
  2. yield 关键字:在生成器函数内部使用,可以暂停函数的执行,并返回一个值。下次调用生成器的next()方法时,函数会从暂停的地方继续执行。
  3. 生成器对象:生成器函数被调用时不会立即执行,而是返回一个生成器对象。这个对象具有next()return()throw()方法。
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = simpleGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

生成器的方法

  1. next():恢复生成器的执行,返回一个对象,包含valuedone属性。
  2. return(value):终止生成器的执行,返回给定的value并设置生成器为完成状态。
  3. throw(error):在生成器内部抛出一个错误,可以被生成器内部的 try-catch 捕获。

使用 yield 实现输入和输出

在生成器中,yield 关键字扮演着两个角色:

  1. 输出:将值从生成器内部传递到外部
  2. 输入:从外部接收值并传递到生成器内部
function* generatorWithParams() {
  const x = yield "Please provide x";
  const y = yield "Please provide y";
  yield x + y;
}

const gen = generatorWithParams();
console.log(gen.next()); // { value: 'Please provide x', done: false }
console.log(gen.next(10)); // { value: 'Please provide y', done: false }
console.log(gen.next(20)); // { value: 30, done: false }
console.log(gen.next()); // { value: undefined, done: true }
  1. 第一次调用next()时,生成器内部状态:
// 执行到第一个 yield,暂停在这里:
const x = yield 'Please provide x';
// ↑ 暂停在这个位置,等待下一次 next() 调用

此时:

  • 生成器输出:'Please provide x'
  • 生成器暂停,等待输入
  • x 变量还没有被赋值
  1. 第二次调用next(10)
// 恢复执行,10 成为第一个 yield 的返回值
const x = 10; // ← yield 表达式返回 10
// 继续执行到第二个 yield
const y = yield 'Please provide y';
// ↑ 再次暂停,等待输入

此时:

  • 第一个 yield 表达式返回 10,赋值给 x
  • 生成器输出:'Please provide y'
  • x 现在有值了:10
  • y 变量还没有被赋值
  1. 第二次调用next(20)
// 恢复执行,20 成为第二个 yield 的返回值
const y = 20; // ← yield 表达式返回 20
// 继续执行到第三个 yield
yield x + y; // 10 + 20 = 30
// ↑ 暂停,输出 30

此时:

  • 第二个 yield 表达式返回 20,赋值给 y
  • 生成器输出:x + y = 30
  • x = 10, y = 20,两个变量都有值了
  1. 第四次调用 next()
// 恢复执行,但后面没有更多 yield
// 函数自然结束,返回 undefined

生成器与迭代协议

生成器对象既是可迭代对象,也是迭代器。因此,可以直接用于for...of循环。

function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

for (const num of range(1, 5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

产生可迭代对象

使用yield*表达式可以在生成器中委托另一个生成器或可迭代对象。

function* generator1() {
  yield 1;
  yield 2;
}

function* generator2() {
  yield* generator1();
  yield* [3, 4];
  yield 5;
}

const gen = generator2();
console.log([...gen]); // [1, 2, 3, 4, 5]

使用 yield*实现递归算法

yield*最有用的地方是实现递归操作,此时生成器可以产生自身。

function* nTimes(n) {
  if (n > 0) {
    yield* nTimes(n - 1);
    yield n - 1;
  }
}
for (const x of nTimes(3)) {
  console.log(x);
}

错误处理

在生成器内部可以使用 try-catch 来捕获错误,包括通过throw方法从外部抛入的错误。

function* errorHandlingGenerator() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log("内部捕获错误:", e);
  }
}

const gen = errorHandlingGenerator();
gen.next(); // 执行到第一个yield
gen.throw(new Error("出错了!")); // 在生成器内部捕获错误并打印

提前终止

生成器可以通过return()方法提前终止。

function* generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.return(10)); // { value: 10, done: true },生成器终止
console.log(gen.next()); // { value: undefined, done: true }

扩展

扩展操作符在对可迭代对象执行浅复制时特别有用, 只需简单的语法就可以复制整个对象:

let arr1 = [1, 2, 3];
let arr2 = [...arr1];
console.log(arr1); // [ 1, 2, 3 ]
console.log(arr2); // [ 1, 2, 3 ]
console.log(arr1 === arr2); // false

浅复制意味着只会复制对象引用:

let arr1 = [{}];
let arr2 = [...arr1];
arr1[0].foo = "bar";

console.log(arr2[0]); // { foo: 'bar' }

对象、类与面向对象编程

对象

对象是 JavaScript 中最基本、最重要的数据结构。对象是属性的集合,每个属性都是一个键值对(key-value pair)。

// 对象的本质
let person = {
  name: "张三", // 属性:键="name", 值="张三"
  age: 30, // 属性:键="age", 值=30
  isStudent: false, // 属性:键="isStudent", 值=false
};

对象的特性

  • 动态性:可以随时添加、修改、删除属性
  • 引用类型:对象是通过引用传递的
  • 原型继承:每个对象都有一个原型链

对象的创建方式

  • 对象字面量(最常用)

    // 空对象
    let emptyObj = {};
    
    // 带属性的对象
    let person = {
      name: "张三",
      age: 30,
      // 方法
      sayHello: function () {
        return `你好,我是${this.name}`;
      },
      // ES6 方法简写
      introduce() {
        return `我叫${this.name},今年${this.age}岁`;
      },
    };
    
  • Object 构造函数

    // 空对象
    let obj1 = new Object();
    let obj2 = new Object(); // 括号可选
    
    // 带初始值
    let person = new Object();
    person.name = "张三";
    person.age = 30;
    
  • Object.create()

    // 创建没有原型的对象
    let obj1 = Object.create(null);
    console.log(obj1.toString); // undefined
    
    // 以现有对象为原型
    let prototype = {
      greet() {
        console.log("Hello!");
      },
    };
    let obj2 = Object.create(prototype);
    obj2.name = "张三";
    obj2.greet(); // "Hello!"
    
  • 构造函数模式

    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.sayHello = function () {
        return `你好,我是${this.name}`;
      };
    }
    
    let person1 = new Person("张三", 30);
    let person2 = new Person("李四", 25);
    
  • 类语法(ES6)

    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    
      sayHello() {
        return `你好,我是${this.name}`;
      }
    
      // 静态方法
      static createAnonymous() {
        return new Person("匿名", 0);
      }
    }
    
    let person = new Person("张三", 30);
    let anonymous = Person.createAnonymous();
    

属性的操作和管理

  • 属性访问

    let person = {
      name: "张三",
      age: 30,
      "first-name": "张", // 特殊属性名
    };
    
    // 点表示法
    console.log(person.name); // "张三"
    
    // 方括号表示法
    console.log(person["name"]); // "张三"
    console.log(person["first-name"]); // "张"(必须用方括号)
    
    // 动态属性名
    let property = "name";
    console.log(person[property]); // "张三"
    
  • 属性添加和修改

    let person = {};
    
    // 添加属性
    person.name = "张三";
    person["age"] = 30;
    
    // 修改属性
    person.age = 31;
    person["name"] = "李四";
    
    // 添加方法
    person.sayHello = function () {
      return `你好,我是${this.name}`;
    };
    
  • 属性删除

    let person = {
      name: "张三",
      age: 30,
      temp: "临时数据",
    };
    
    delete person.temp;
    console.log(person.temp); // undefined
    console.log("temp" in person); // false
    
    // 注意:delete 不能删除继承的属性
    
  • 属性检测

    let person = { name: "张三", age: 30 };
    
    // in 操作符(检查自身和原型链)
    console.log("name" in person); // true
    console.log("toString" in person); // true(继承的)
    
    // hasOwnProperty()(只检查自身属性)
    console.log(person.hasOwnProperty("name")); // true
    console.log(person.hasOwnProperty("toString")); // false
    
    // 直接判断(可能误判)
    console.log(person.name !== undefined); // true
    console.log(person.nickname !== undefined); // false
    

Object 的静态方法

  • Object.assign() - 浅复制一个或多个源对象的所有可枚举属性到目标对象

    let target = { a: 1, b: 2 };
    let source1 = { b: 3, c: 4 };
    let source2 = { c: 5, d: 6, e: { name: "zzz" } };
    
    Object.assign(target, source1, source2);
    source2.e.name = "aaa";
    console.log(target); // { a: 1, b: 3, c: 5, d: 6,e:{name:'aaa'} }
    
  • Object.keys() - 返回一个包含所有自身可枚举属性名称的数组

    let person = { name: "张三", age: 30 };
    console.log(Object.keys(person)); // ["name", "age"]
    
  • Object.values() - 返回一个包含所有自身可枚举属性值的数组

    let person = { name: "张三", age: 30 };
    console.log(Object.values(person)); // ["张三", 30]
    
  • Object.entries() - 返回一个包含所有自身可枚举属性键值对的数组

    console.log(Object.entries(person)); // [["name", "张三"], ["age", 30]]
    
  • Object.fromEntries() - 将键值对列表转换为对象

    let entries = [
      ["name", "李四"],
      ["age", 25],
    ];
    let obj = Object.fromEntries(entries);
    console.log(obj); // { name: "李四", age: 25 }
    
  • Object.create() - 使用指定的原型对象和属性创建一个新对象

    let parent = {
      sayHello: function () {
        console.log("Hello");
      },
    };
    
    let child = Object.create(parent, {
      name: { value: "小明", writable: true, enumerable: true },
      age: { value: 10 },
    });
    
    child.sayHello(); // "Hello"
    
  • Object.defineProperty() - 定义或修改对象的属性

    let obj = {};
    Object.defineProperty(obj, "name", {
      value: "张三",
      writable: false, // 不可写
      enumerable: true, // 可枚举
      configurable: false, // 不可配置
    });
    
    obj.name = "李四"; // 严格模式下会报错,非严格模式下静默失败
    console.log(obj.name); // "张三"
    
  • Object.defineProperties() - 一次定义多个属性

    let obj = {};
    Object.defineProperties(obj, {
      name: {
        value: "张三",
        writable: true,
      },
      age: {
        value: 30,
        writable: false,
      },
    });
    
  • Object.getOwnPropertyDescriptor() - 获取指定属性的属性描述符

    let descriptor = Object.getOwnPropertyDescriptor(obj, "name");
    console.log(descriptor);
    // { value: "张三", writable: true, enumerable: false, configurable: false }
    
  • Object.getOwnPropertyDescriptors - 获取所有属性的属性描述符

    let descriptors = Object.getOwnPropertyDescriptors(obj);
    console.log(descriptors);
    // {name: {value: '张三',writable: true,enumerable: false,configurable: false},
    //  age: {value: 30,writable: false,enumerable: false,configurable: false}}
    
  • Object.getOwnPropertyNames() - 返回所有自身属性的名称(包括不可枚举属性)

    let obj = { a: 1, b: 2 };
    Object.defineProperty(obj, "c", { value: 3, enumerable: false });
    console.log(Object.getOwnPropertyNames(obj)); // ["a", "b", "c"]
    
  • Object.getPrototypeOf() - 返回指定对象的原型

    let parent = {};
    let child = Object.create(parent);
    console.log(Object.getPrototypeOf(child) === parent); // true
    
  • Object.setPrototypeOf() - 设置对象的原型

    let parent = {
      greet: function () {
        console.log("Hello");
      },
    };
    let child = {};
    Object.setPrototypeOf(child, parent);
    child.greet(); // "Hello"
    

Object 的实例方法

  • hasOwnProperty() - 检查属性是否是自身属性(非继承)

    let obj = { a: 1 };
    console.log(obj.hasOwnProperty("a")); // true
    console.log(obj.hasOwnProperty("toString")); // false,因为toString是继承的
    
  • isPrototypeOf() - 检查一个对象是否在另一个对象的原型链上

    let parent = {};
    let child = Object.create(parent);
    console.log(parent.isPrototypeOf(child)); // true
    
  • propertyIsEnumerable() - 检查属性是否可枚举

    let obj = { a: 1 };
    Object.defineProperty(obj, "b", { value: 2, enumerable: false });
    console.log(obj.propertyIsEnumerable("a")); // true
    console.log(obj.propertyIsEnumerable("b")); // false
    
  • valueOf() - 返回对象的原始值

    let obj = {
      a: 1,
      valueOf: function () {
        return this.a;
      },
    };
    console.log(obj.valueOf()); // 1
    
  • toString() - 返回对象的字符串表示

    let obj = { a: 1 };
    console.log(obj.toString()); // "[object Object]"
    
    // 可以重写toString方法
    let person = {
      name: "张三",
      toString: function () {
        return `Person { name: "${this.name}" }`;
      },
    };
    console.log(person.toString()); // 'Person { name: "张三" }'
    

对象的迭代

  • 使用for...in循环

    for...in 循环会遍历对象自身和原型链上可枚举属性(不含 Symbol 属性)。

    let obj = { a: 1, b: 2, c: 3 };
    for (let key in obj) {
      console.log(key, obj[key]);
    }
    // 输出: a 1, b 2, c 3
    
  • 使用 Object.keys() 配合 for 循环

    let obj = { a: 1, b: 2, c: 3 };
    Object.keys(obj).forEach((key) => {
      console.log(key, obj[key]);
    });
    
  • 使用for...of循环(需要对象是可迭代的,普通对象默认不可迭代

    普通对象不是可迭代的,但可以使用Object.keys()Object.values()Object.entries()来获得可迭代的数组。

    let obj = { a: 1, b: 2, c: 3 };
    for (let [key, value] of Object.entries(obj)) {
      console.log(key, value);
    }
    

对象的不变性

JavaScript 提供了几种方式来设置对象的不同程度不可变性。

  • 禁止扩展 - Object.preventExtensions()

    Object.preventExtensions()让一个对象变的不可扩展,也就是永远不能再添加新的属性。

    let obj = { a: 1 };
    Object.preventExtensions(obj);
    obj.b = 2; // 非严格模式下静默失败,严格模式下报错
    console.log(obj.b); // undefined
    
  • 密封 - Object.seal()

    密封对象不可扩展,且所有自身属性都不可配置(不能删除,不能修改属性描述符,但可以修改属性值)。

    let obj = { a: 1 };
    Object.seal(obj);
    delete obj.a; // 非严格模式下静默失败,严格模式下报错
    console.log(obj.a); // 1
    
  • 冻结 - Object.freeze()

    冻结对象不可扩展、密封,且所有数据属性不可写(如果属性是对象,其内容仍可修改,除非也冻结)。

    let obj = { a: 1, b: { c: 2 } };
    Object.freeze(obj);
    obj.a = 2; // 非严格模式下静默失败,严格模式下报错
    console.log(obj.a); // 1
    
    // 注意:冻结是浅冻结,深层对象不受影响
    obj.b.c = 3; // 修改成功
    console.log(obj.b.c); // 3
    
    // 深冻结函数
    function deepFreeze(obj) {
      Object.freeze(obj);
      for (let key in obj) {
        if (
          obj.hasOwnProperty(key) &&
          typeof obj[key] === "object" &&
          obj[key] !== null
        ) {
          deepFreeze(obj[key]);
        }
      }
    }
    deepFreeze(obj);
    obj.b.c = 5;
    console.log(obj.b.c); // 3
    

对象与 JSON

对象序列化为 JSON 字符串
let person = {
  name: "张三",
  age: 30,
  toJSON: function () {
    // 可以定义toJSON方法来自定义序列化
    return {
      name: this.name,
      age: this.age,
      isAdult: this.age >= 18,
    };
  },
};

console.log(JSON.stringify(person)); // '{"name":"张三","age":30,"isAdult":true}'
从 JSON 字符串解析为对象
let jsonString = '{"name":"张三","age":30}';
let obj = JSON.parse(jsonString);
console.log(obj.name); // "张三"

对象的新特性(ES6 及以上)

属性简写
let name = "张三";
let age = 30;
let person = { name, age }; // 等同于 { name: name, age: age }
方法简写
let person = {
  name: "张三",
  sayName() {
    // 等同于 sayName: function() { ... }
    console.log(this.name);
  },
};
计算属性名
let propName = "firstName";
let person = {
  [propName]: "张三",
  ["last" + "Name"]: "李",
};
console.log(person.firstName); // "张三"
console.log(person.lastName); // "李"
展开运算符(...)用于对象
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, c: 3, d: 4 }; // { a: 1, b: 2, c: 3, d: 4 }
let obj3 = { ...obj1, a: 3 }; // { a: 3, b: 2 },覆盖属性a
对象解构
let person = { name: "张三", age: 30, job: "工程师" };

// 基本解构
let { name, age } = person;
console.log(name, age); // "张三" 30

// 重命名
let { name: personName, age: personAge } = person;
console.log(personName, personAge); // "张三" 30

// 默认值
let { name, age, country = "中国" } = person;
console.log(country); // "中国"

// 函数参数解构
function printPerson({ name, age }) {
  console.log(`${name} is ${age} years old.`);
}
printPerson(person); // "张三 is 30 years old."

对象的应用模式

工厂模式

工厂模式通过一个函数来创建对象,这个函数返回一个新的对象。工厂模式可以解决创建多个相似对象的问题,但没有解决对象识别问题(即怎样知道一个对象的类型)。

function createPerson(name, age) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.sayName = function () {
    console.log(this.name);
  };
  return o;
}

let person1 = createPerson("Alice", 30);
let person2 = createPerson("Bob", 25);
构造函数模式

构造函数模式使用new操作符来创建对象。构造函数名通常首字母大写。使用构造函数的优点是可以通过instanceof识别对象类型。缺点在于每个方法都要在每个实例上重新创建一遍。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  };
}
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);
原型模式

原型模式使用函数的prototype属性来定义共享的属性和方法。所有实例共享同一组属性和方法。缺点是所有实例在默认情况下都取得相同的属性值,并且对于引用类型的属性,一个实例修改会影响到所有实例。

function Person() {}

Person.prototype.name = "Alice";
Person.prototype.age = 30;
Person.prototype.friends = ["Bob", "Tom"];
Person.prototype.sayName = function () {
  console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();

person1.friends.push("Jerry");
console.log(person2.friends); // ["Bob", "Tom", "Jerry"] 共享了引用类型
组合使用构造函数模式和原型模式

这是最常用的模式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。这样每个实例都有自己的一份实例属性的副本,同时又共享着方法的引用,最大限度地节省了内存。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ["Bob", "Tom"];
}

Person.prototype.sayName = function () {
  console.log(this.name);
};

var person1 = new Person("Alice", 30);
var person2 = new Person("Bob", 25);

person1.friends.push("Jerry");
console.log(person1.friends); // ["Bob", "Tom", "Jerry"]
console.log(person2.friends); // ["Bob", "Tom"] 互不影响
动态原型模式

动态原型模式将原型方法的定义封装在构造函数中,通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。这样既保留了组合模式的优点,又使得代码更加整洁。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ["Bob", "Tom"];

  // 如果方法不存在,则添加到原型中
  if (typeof this.sayName !== "function") {
    Person.prototype.sayName = function () {
      console.log(this.name);
    };
  }
}

var person1 = new Person("Alice", 30);
person1.sayName(); // Alice
寄生构造函数模式

寄生构造函数模式类似于工厂模式,但是使用new操作符。该模式可以在不修改原有构造函数的情况下,为对象添加额外的功能。注意:该模式返回的对象与构造函数原型之间没有关系,不能依赖instanceof来确定对象类型。

function SpecialArray() {
  var values = new Array();
  values.push.apply(values, arguments);
  values.toPipedString = function () {
    return this.join("|");
  };
  return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString()); // "red|blue|green"
console.log(colors instanceof SpecialArray); // false
稳妥构造函数模式

稳妥对象指的是没有公共属性,而且其方法也不引用this的对象。稳妥构造函数模式适用于一些安全的环境中(禁止使用thisnew)或者防止数据被其他应用程序改动。该模式创建的对象中,除了通过方法访问数据,没有其他方式可以访问到原始数据。

function Person(name, age) {
  var o = new Object();
  // 可以在这里定义私有变量和函数
  o.sayName = function () {
    console.log(name);
  };
  return o;
}

var person1 = Person("Alice", 30);
person1.sayName(); // Alice
// 除了sayName方法,没有其他方法可以访问name
类模式(ES6)

ES6 引入了的概念,通过class关键字可以定义类模式本质上是构造函数原型模式的语法糖。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.friends = ["Bob", "Tom"];
  }

  sayName() {
    console.log(this.name);
  }
}

const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);

person1.sayName(); // Alice
console.log(person1 instanceof Person); // true

继承

实现继承ECMAScript唯一支持的继承方式,而这主要是通过原型链来实现的。JavaScript原型是对象继承属性和方法的机制,通过prototype属性实现共享,‌ 原型链则基于proto隐式原型形成查找路径,最终终止于 null,这是 JavaScript 实现继承的核心 ‌。‌‌

原型对象

在 JavaScript 中,每个函数都有一个特殊的属性叫做prototype(只有函数有,对象没有)。当我们使用new操作符来创建一个函数的实例时,这个实例的内部会有一个指针(__proto__)指向函数的prototype属性所指向的对象。这个对象就是我们所说的“原型对象”

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHello = function () {
  console.log("Hello");
};
const person = new Person("Mark", 13);
person.sayHello();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.constructor === Person); // true

所有 Person 实例共享 sayHello 方法。

  • 隐式原型(proto)‌:每个对象都有__proto__属性,指向其构造函数的prototype。实例通过__proto__访问原型链上的属性和方法。‌‌
  • 构造函数的prototype,即原型对象,有一个constructor属性,指回构造函数

构造函数、原型、实例的关系总结

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function () {
  return `${this.name} 发出声音`;
};

let cat = new Animal("猫咪");

// 关系验证
console.log(cat.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.constructor === Animal); // true
console.log(cat instanceof Animal); // true
console.log(Animal.prototype.isPrototypeOf(cat)); // true
  • 每个函数(构造函数)都有一个原型对象(prototype)
  • 每个原型对象都有一个constructor属性,指向函数本身
  • 每个实例都有一个内部属性[[Prototype]](__proto__),指向构造函数的原型对象

原型链

当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着这个对象的原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(即null)。这个过程就是原型链的工作原理。

‌ 简而言之:属性查找时,若对象自身不存在,则沿__proto__向上查找,直至Object.prototype(链顶)或null(链尾)

let animal = {
  eats: true,
  walk() {
    console.log("动物走路");
  },
};

let rabbit = {
  jumps: true,
  __proto__: animal,
};

let longEar = {
  earLength: 10,
  __proto__: rabbit,
};

// 原型链:longEar → rabbit → animal → Object.prototype → null
console.log(longEar.eats); // true(从 animal 继承)
console.log(longEar.jumps); // true(从 rabbit 继承)
longEar.walk(); // "动物走路"(从 animal 继承)

原型的操作方法

let obj = {};
let arr = [];
// 不推荐的方式(已弃用,但很多环境扔支持)
console.log(obj.__proto__); // Object.prototype
console.log(arr.__proto__); // Array.prototype

//推荐的方式
console.log(Object.getPrototypeOf(obj)); // Object.prototype
console.log(Object.getPrototypeOf(arr)); // Array.prototype

// 设置原型
let animal = { eats: true };
let rabbit = { jumps: true };
// rabbit.__proto__ = animal
Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.eats); // true

// 创建时指定原型
let dog = Object.create(animal, {
  barks: { value: true },
});
console.log(dog.eats); // true
console.log(dog.barks); // true

原型链检测

let animal = { eats: true };
let rabbit = Object.create(animal);
let longEar = Object.create(rabbit);

// 检查原型关系
console.log(animal.isPrototypeOf(rabbit)); // true
console.log(animal.isPrototypeOf(longEar)); // true
console.log(rabbit.isPrototypeOf(longEar)); // true

// instanceof 操作符
function Animal() {}
let cat = new Animal();
console.log(cat instanceof Animal); // true
console.log(cat instanceof Object); // true

属性屏蔽

当对象和原型有同名属性时,对象自身的属性会"遮蔽"原型属性:

let animal = {
  eats: true,
  walk() {
    console.log("动物走路");
  },
};
let rabbit = Object.create(animal);
rabbit.eats = false; // 创建自身属性,遮蔽原型属性

console.log(rabbit.eats); // false
console.log(Object.getPrototypeOf(rabbit).eats); // true(原型属性)

//删除自身属性后,可以重新访问原型属性
delete rabbit.eats;
console.log(rabbit.eats); // true(原型属性)

方法重写

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  return "动物声音";
};
function Dog(name) {
  Animal.call(this, name); // 调用父类构造函数
}
//设置原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

function Cat(name) {
  Animal.call(this, name); // 调用父类构造函数
}
//设置原型链
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

//重写方法
Dog.prototype.speak = function () {
  return "汪汪!";
};
let dog = new Dog("旺财");
console.log(dog.speak()); // "汪汪!"

Cat.prototype.speak = function () {
  return "喵~";
};
let cat = new Cat("咪咪");
console.log(cat.speak()); // "喵~"

原型继承的实现方式

构造函数继承 + 原型链
  • 使用构造函数来定义实例属性,通过将子类的原型设置为父类的一个实例来实现原型方法的继承。
  • 步骤:
    1. 在子类构造函数中调用父类构造函数(使用callapply)来继承实例属性。
    2. 将子类的原型设置为父类的一个实例(通常通过Object.create(父类.prototype))来继承原型方法。
    3. 修正子类原型的 constructor 指向子类构造函数。
function Animal(name) {
  this.name = name;
  this.eats = true;
}

Animal.prototype.walk = function () {
  return `${this.name} 在走路`;
};

function Dog(name, breed) {
  Animal.call(this, name); // 继承实例属性
  this.breed = breed;
}

// 继承原型方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 添加子类方法
Dog.prototype.bark = function () {
  return "汪汪!";
};

let myDog = new Dog("旺财", "金毛");
console.log(myDog.walk()); // "旺财 在走路"
console.log(myDog.bark()); // "汪汪!"
Object.create()实现纯净继承
  • 不使用构造函数,而是通过Object.create()直接创建一个以指定对象为原型的新对象,然后可以扩展自己的属性和方法。
  • 步骤:
    1. 使用Object.create(父对象)创建一个新对象,该对象继承父对象的属性和方法。
    2. 可以扩展新对象的属性和方法。
    3. 如果需要,可以重写父对象的方法。
// 基础对象
let animal = {
  init(name) {
    this.name = name;
    return this;
  },
  walk() {
    return `${this.name} 在走路`;
  },
};

// 创建子对象
let dog = Object.create(animal);
dog.bark = function () {
  return "汪汪!";
};
dog.walk = function () {
  return `${animal.walk.call(this)},摇着尾巴`;
};

let myDog = Object.create(dog).init("旺财");
ES6 Class(语法糖)
class Animal {
  constructor(name) {
    this.name = name;
    this.eats = true;
  }

  walk() {
    return `${this.name} 在走路`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }

  bark() {
    return "汪汪!";
  }

  // 重写父类方法
  walk() {
    return `${super.walk()},摇着尾巴`;
  }
}

let myDog = new Dog("旺财", "金毛");
console.log(myDog.walk()); // "旺财 在走路,摇着尾巴"
console.log(myDog.bark()); // "汪汪!"

Class(类)是 ES6 引入的基于原型继承语法糖,它提供了更清晰、更接近传统面向对象语言的语法来创建对象和处理继承。

基本概念

Class 是创建对象的模板,它封装了数据和行为,可以基于它创建多个具有相同结构和行为的对象实例。

// 传统的构造函数方式
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHello = function () {
  return `Hello, I'm ${this.name}`;
};

// ES6 Class 方式(语法糖)
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    return `Hello, I'm ${this.name}`;
  }
}

// 两种方式创建的对象行为完全相同
const person1 = new Person("John", 25);
console.log(person1.sayHello()); // "Hello, I'm John"

与函数定义不同的地方还有:

  • 函数声明可以提升,但类定义不能

    console.log(FuctionExpression); //  FuctionExpression() {}
    function FuctionExpression() {}
    
    console.log(ClassExpression); // Cannot access 'ClassExpression' before initialization
    class ClassExpression {}
    
  • 函数受函数作用域限制,而类受块作用域限制

类的构成

类可以包含构造函数方法实例方法获取函数设置函数静态类方法

构造函数方法

构造函数在创建类实例时自动调用,用于初始化实例属性。

class Person {
  // 构造函数方法
  constructor(name, age) {
    // 初始化实例属性
    this.name = name;
    this.age = age;
  }
}

const person = new Person("张三", 30);
console.log(person.name); // "张三"
实例方法

实例方法属于类的原型,所有实例共享这些方法。

class Calculator {
  // 实例方法
  add(a, b) {
    const result = a + b;
    return result;
  }
}

const calc = new Calculator();
console.log(calc.add(5, 3)); // 8
获取函数

Getter 用于以属性访问的方式获取计算值。

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  // Getter - 像属性一样访问
  get area() {
    console.log("计算面积");
    return this.width * this.height;
  }

  get perimeter() {
    console.log("计算周长");
    return 2 * (this.width + this.height);
  }
}

const rect = new Rectangle(10, 5);
console.log(rect.area); // 50(调用 get area())
console.log(rect.perimeter); // 30(调用 get perimeter())
设置函数

Setter 用于以属性赋值的方式设置值,通常包含验证逻辑。

class User {
  constructor(username, email) {
    this._username = username;
    this._email = email;
    this._age = 0;
  }

  // Setter for username
  set username(value) {
    if (value.length < 3) {
      throw new Error("用户名至少需要3个字符");
    }
    if (!/^[a-zA-Z0-9_]+$/.test(value)) {
      throw new Error("用户名只能包含字母、数字和下划线");
    }
    this._username = value;
    console.log(`用户名已更新为: ${value}`);
  }

  // Getter for username
  get username() {
    return this._username;
  }

  // Setter for email with validation
  set email(value) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      throw new Error("无效的邮箱格式");
    }
    this._email = value;
    console.log(`邮箱已更新为: ${value}`);
  }

  get email() {
    return this._email;
  }

  // Setter for age with validation
  set age(value) {
    if (value < 0 || value > 150) {
      throw new Error("年龄必须在0-150之间");
    }
    this._age = value;
    console.log(`年龄已更新为: ${value}`);
  }

  get age() {
    return this._age;
  }

  // 只读属性(只有getter,没有setter)
  get userInfo() {
    return `${this._username} (${this._email}) - ${this._age}岁`;
  }
}

const user = new User("john_doe", "john@example.com");
user.username = "jane_smith"; // 调用 setter
user.email = "jane@example.com"; // 调用 setter
user.age = 25; // 调用 setter

console.log(user.userInfo); // "jane_smith (jane@example.com) - 25岁"

// 尝试设置无效值
try {
  user.username = "ab"; // 抛出错误:用户名至少需要3个字符
} catch (error) {
  console.error(error.message);
}

// user.userInfo = "新信息";    // 错误:没有setter,无法设置
静态类方法

静态方法属于类本身,而不是类的实例。

class MathUtils {
  // 静态属性
  static PI = 3.141592653589793;

  // 几何计算
  static calculateCircleArea(radius) {
    return this.PI * radius * radius;
  }
}
// 使用静态方法和属性(不需要实例化)
console.log(MathUtils.PI); // 3.141592653589793
console.log(MathUtils.calculateCircleArea(5)); // 78.53981633974483

类构造函数

constructor关键字用于在类定义快内部创建类的构造函数。方法名constructor会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数。

类构造函数构造函数的主要区别是,调用类构造函数必须使用new操作符,而普通构造函数可以不使用new调用,那么就会以全局的this(通常是window)作为内部对象。

调用类构造函数如果忘了使用new会抛出错误:

// 把window作为this来构建实例
Person();
sayHello(); // 你好

Animal(); // Class constructor Animal cannot be invoked without 'new'

Class 继承

使用 extends 关键字实现继承。子类可以继承父类的属性和方法,并可以重写父类的方法或添加新的方法

派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数实例方法静态方法内部。

class Animal {
  constructor(name) {
    this.name = name;
    this.eats = true;
    this.sleeps = true;
  }

  eat() {
    return `${this.name} 在吃东西`;
  }

  sleep() {
    return `${this.name} 在睡觉`;
  }

  makeSound() {
    return "动物发出声音";
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 必须调用父类构造函数
    this.breed = breed;
    this.loyal = true;
  }

  // 重写父类方法
  makeSound() {
    return "汪汪!";
  }

  // 子类特有方法
  fetch() {
    return `${this.name} 在接飞盘`;
  }

  // 调用父类方法
  eat() {
    return `${super.eat()},吃得很快`;
  }
}

const dog = new Dog("旺财", "金毛");

console.log(dog.eat()); // "旺财 在吃东西,吃得很快"
console.log(dog.makeSound()); // "汪汪!"
console.log(dog.fetch()); // "旺财 在接飞盘"

私有字段和方法(ES2022)

ES2022(ES13)为 JavaScript 类(Class)正式引入了私有字段(Private Fields)私有方法(Private Methods)。它们通过在字段名或方法名前添加一个井号(#)来定义,只能在类的内部访问,外部直接访问会抛出错误

class Person {
  #name; // 私有字段
  age; // 公共字段

  constructor(name, age) {
    this.#name = name; // 初始化私有字段
    this.age = age;
  }

  // 私有方法
  #logIntroduction() {
    console.log(`I am ${this.#name}, ${this.age} years old.`);
  }

  // 公共方法,可以内部调用私有方法
  publicMethod() {
    this.#logIntroduction();
  }
}

const person = new Person("Jack", 30);
console.log(person.age); // 30 (公共字段,可以访问)
person.publicMethod(); // "I am Jack, 30 years old." (通过公共方法间接调用私有方法)

// console.log(person.#name); // SyntaxError: 外部无法直接访问私有字段
// person.#logIntroduction(); // SyntaxError: 外部无法直接调用私有方法
检查私有字段是否存在

使用 in 操作符可以检查一个对象是否拥有某个特定的私有字段。这在某些需要动态检查的场景下非常有用。

class Car {
  #color;

  hasColor() {
    // 检查此实例是否拥有私有字段 #color
    return #color in this;
  }
}

const car = new Car();
console.log(car.hasColor()); // true
静态私有字段与方法

私有特性同样可以应用于静态成员,这些成员属于类本身,而不是实例

class Config {
  static #apiKey; // 静态私有字段
  static #cache = new Map(); // 静态私有字段

  // 静态私有方法
  static #validateKey(key) {
    return key && key.length > 10;
  }

  // 静态公共方法,可以访问静态私有成员
  static setApiKey(key) {
    if (this.#validateKey(key)) {
      this.#apiKey = key;
    }
  }
}
// Config.#apiKey; // SyntaxError: 外部无法访问

类静态初始化块

ES2022 还引入了类静态初始化块(Static Initialization Blocks),它允许你在类定义时执行一些静态成员的初始化逻辑,这对于复杂的静态私有字段初始化非常有用

class DatabaseConnection {
  static #config;
  static #connection;

  //静态初始化块
  static {
    try {
      this.#config = this.#loadConfigFromFile();
    } catch (error) {
      this.#config = { url: "default_url" };
      console.error("Failed to initialize database:", error);
    }
  }

  static #loadConfigFromFile() {
    console.log("加载设置...");
    return { url: "xxxx_url" };
  }
}

代理与反射

在 JavaScript 中,代理(Proxy)反射(Reflect)是 ES6 引入的两个强大的特性,它们用于拦截和自定义对象的基本操作。

代理

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

// 基本语法
const proxy = new Proxy(target, handler);
  • target:要代理的目标对象
  • handler:包含陷阱(trap)函数的对象,用于定义代理行为

使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的”基本操作的拦截器“。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接 或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如,get() 捕获器会接收到目标对象要查询的属性代理对象三个参数。

下面对一个对象进行代理,在读取属性时,如果没有该属性则返回“Property not found”,在设置属性时进行拦截检测,如果设置 age 属性值为非 number 类型则会报错。

const target = {
  name: "Alice",
  age: 30,
};
const handler = {
  get: function (obj, prop) {
    return prop in obj ? obj[prop] : "Property not found";
  },
  set: function (obj, prop, value) {
    if (prop === "age" && typeof value !== "number") {
      throw new Error("Age must be a number");
    }
    obj[prop] = value;
    return true;
  },
};
const proxy = new Proxy(target, handler);

try {
  // console.log(proxy.name);
  // console.log(proxy.unknown); // Property not found
  // proxy.age = 25;
  proxy.age = "25"; // 报错: Age must be a number
} catch (error) {
  console.log("报错:", error.message);
}

捕获器不变式

使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。根据 ECMAScript 规范,每个 捕获的方法都知道目标对象上下文捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式” (trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的 值时,会抛出 TypeError:

const target = {};
Object.defineProperty(target, "name", {
  configurable: false,
  writable: false,
  value: "Alice",
});
const handler = {
  get() {
    return "Mark";
  },
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // TypeError

可撤销代理

有时候可能需要中断代理对象目标对象之间的联系。对于使用 new Proxy()创建的普通代理来 说,这种联系会在代理对象的生命周期内一直持续存在。

Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象目标对象的关联。撤销代理的 操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的结果都一样。撤销代理之后 再调用代理会抛出 TypeError。

撤销函数和代理对象是在实例化时同时生成的:

const target = {
  name: "Alice",
};
const handler = {
  get(obj, prop) {
    return obj[prop];
  },
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.name); // Alice
console.log(target.name); // Alice

//撤销代理
revoke();
console.log(proxy.name); // TypeError: Cannot perform 'get' on a proxy that has been revoked

反射

Reflect 是一个内置对象,它提供了拦截 JavaScript 操作的方法。这些方法与Proxy的陷阱方法一一对应。Reflect方法提供了一种简单的方式来执行默认行为,而不需要手动实现。

const obj = {
  name: "Bob",
  age: 25,
};

// 使用 Reflect 的方法
console.log(Reflect.get(obj, "name")); // "Bob"
console.log(Reflect.has(obj, "age")); // true
console.log(Reflect.ownKeys(obj)); // ["name", "age"]

Reflect 的主要方法

  1. Reflect.get(target, property, receiver):获取对象属性的值。
  2. Reflect.set(target, property, value, receiver):设置对象属性的值。
  3. Reflect.has(target, property):判断对象是否具有某个属性,相当于 in 操作符。
  4. Reflect.deleteProperty(target, property):删除对象的属性,相当于 delete 操作符。
  5. Reflect.apply(target, thisArg, args):调用一个函数,并传入指定的 this 值和参数数组。
  6. Reflect.construct(target, args, newTarget):相当于 new 操作符,用于创建一个实例。
const obj = {};

// 设置属性
Reflect.set(obj, "name", "Charlie");

// 获取属性
console.log(Reflect.get(obj, "name")); // "Charlie"

// 检查属性存在
console.log(Reflect.has(obj, "name")); // true

// 删除属性
Reflect.deleteProperty(obj, "name");

// 获取自身属性键
console.log(Reflect.ownKeys(obj)); // []

// 定义属性
Reflect.defineProperty(obj, "age", {
  value: 30,
  writable: true,
  enumerable: true,
});

// 获取属性描述符
console.log(Reflect.getOwnPropertyDescriptor(obj, "age"));

// 设置原型
const proto = { greeting: "Hello" };
Reflect.setPrototypeOf(obj, proto);
console.log(Reflect.getPrototypeOf(obj)); // { greeting: "Hello" }

代理与反射的结合使用

代理和反射通常一起使用,因为在代理的陷阱中,经常需要使用反射方法来执行默认行为。这样,可以在自定义行为的同时,确保不破坏对象的原有行为。

数据验证
const validator = {
  set(target, property, value) {
    // 验证规则
    const rules = {
      age: (val) => typeof val === "number" && val >= 0 && val <= 150,
      email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
      name: (val) => typeof val === "string" && val.length > 0,
    };

    if (rules[property] && !rules[property](value)) {
      throw new Error(`Invalid value for ${property}: ${value}`);
    }

    return Reflect.set(target, property, value);
  },
};

const person = new Proxy({}, validator);

person.name = "Alice"; // 有效
person.age = 25; // 有效
// person.email = "invalid-email"; // Error: Invalid value for email: invalid-email
自动观察者模式
function createObservable(target, callback) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const oldValue = obj[prop];
      const result = Reflect.set(obj, prop, value);
      if (oldValue !== value) {
        callback(prop, oldValue, value);
      }
      return result;
    },
  });
}
const data = { count: 0, name: "Test" };
const observable = createObservable(data, (prop, oldValue, newValue) => {
  console.log(`Property ${prop} changed from ${oldValue} to ${newValue}`);
});

observable.count = 0;
observable.count = 1; // Property count changed from 0 to 1
observable.name = "Alice"; // Property name changed from Test to Alice
负索引数组
function createNegativeIndexArray(array) {
  return new Proxy(array, {
    get(target, prop, receiver) {
      const index = Number(prop);
      if (index < 0) {
        prop = String(target.length + index);
      }
      return Reflect.get(target, prop, receiver);
    },
  });
}
const arr = createNegativeIndexArray(["a", "b", "c", "d"]);
console.log(arr[-1]); // d
console.log(arr[-2]); // c
console.log(arr[1]); // b
私有属性
const handler = {
  get(target, property) {
    if (property.startsWith("_")) {
      throw new Error(`Access to private property '${property}' is denied`);
    }
    return Reflect.get(target);
  },
  set(target, property, value) {
    if (property.startsWith("_")) {
      throw new Error(`Cannot set private property '${property}'`);
    }
    return Reflect.set(target, property, value);
  },
  has(target, property) {
    if (property.startsWith("_")) {
      return false;
    }
    return Reflect.has(target, property);
  },
  ownKeys(target) {
    return Reflect.ownKeys(target).filter((key) => !key.startsWith("_"));
  },
};
const obj = new Proxy(
  {
    name: "Public",
    _secret: "Private data",
  },
  handler
);

console.log(obj.name); // "Public"
// console.log(obj._secret); // Error: Access to private property '_secret' is denied
console.log("_secret" in obj); // false
console.log(Object.keys(obj)); // ["name"]

总结

代理和反射为 JavaScript 提供了强大的元编程能力:

  • 代理(Proxy) 允许你拦截并自定义对象的基本操作
  • 反射(Reflect) 提供了一组与代理陷阱对应的方法,用于执行默认行为
  • 两者结合使用可以实现高级功能,如数据验证、观察者模式、访问控制等

这些特性使得 JavaScript 更加灵活,能够实现更复杂的设计模式和架构模式。

函数

函数声明与函数表达式

函数声明会提升到当前作用域的顶部,因此可以在声明之前调用。

// 函数声明
function functionName(parameters) {
  // 函数体
}

// 示例
console.log(square(5)); // 25,因为函数声明提升

function square(n) {
  return n * n;
}

函数表达式不会提升,必须在定义之后才能调用。

// 函数表达式
const square = function (n) {
  return n * n;
};

console.log(square(5)); // 25

// 如果提前调用会报错
// console.log(square2(5)); // TypeError: square2 is not a function
var square2 = function (n) {
  return n * n;
};

注意:使用var声明函数表达式,变量会提升但赋值不会提升,因此提前调用是undefined,调用undefined会报错。

箭头函数

箭头函数是 ES6 引入的,语法更简洁,但也有很多场合不适用。箭头函数不能使用argumentssupernew.target,也不能用作构造函数。此外,箭头函数也没有prototype属性。

// 箭头函数
const square = (n) => {
  return n * n;
};

// 如果函数体只有一条返回语句,可以省略大括号和return
const square = (n) => n * n;

// 无参数时,使用空括号
const greet = () => console.log("Hello");

// 多个参数时,用括号括起来
const multiply = (a, b) => a * b;

// 返回对象时,用括号包裹对象字面量,避免与函数体冲突
const createObj = () => ({ key: "value" });

箭头函数没有自己的 this,它会捕获其所在上下文的 this 值。例如,在事件处理函数中,箭头函数可以避免传统函数中 this 指向问题。

函数名

因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着 一个函数可以有多个名称,如下所示:

function sum(num1, num2) {
  return num1 + num2;
}
console.log(sum(10, 10)); // 20

let anotherSum = sum;
console.log(anotherSum(10, 10)); // 20

sum = null;
console.log(anotherSum(10, 10)); // 20

ECMAScript 6 的所有函数对象都会暴露一个只读的name属性,其中包含关于函数的信息。多数情 况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称, 也会如实显示成空字符串。如果它是使用Function构造函数创建的,则会标识成"anonymous"

function foo() {}
let bar = function () {};
let baz = () => {};
console.log(foo.name); // foo
console.log(bar.name); // bar
console.log(baz.name); // baz
console.log((() => {}).name); // (空字符串)
console.log(new Function().name); // anonymous

理解参数

ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错。

事实上,在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。

arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的 元素(第一个参数是arguments[0],第二个参数是arguments[1])。而要确定传进来多少个参数, 可以访问arguments.length 属性。

function sayHi(name, message) {
  console.log("Hello " + name + ", " + message);
}
function sayHi2() {
  console.log("Hello " + arguments[0] + ", " + arguments[1]);
}
sayHi("Alice", "see you tomorrow"); // Hello Alice, see you tomorrow
sayHi2("Alice", "see you tomorrow"); // Hello Alice, see you tomorrow

如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用arguments关键字访问,而只 能通过定义的命名参数访问。 虽然箭头函数中没有 arguments 对象,但可以在包装函数中把它提供给箭头函数:

function foo() {
  let bar = () => {
    console.log(arguments[0]); // 5
  };
  bar();
}
foo(5);

除此之外,ES6 允许为函数参数设置默认值。

function greet(name = "Guest") {
  console.log(`Hello, ${name}!`);
}

greet(); // Hello, Guest!
greet("Alice"); // Hello, Alice!

// 默认参数也可以是表达式
function multiply(a, b = 2 * a) {
  return a * b;
}

console.log(multiply(5)); // 50, 因为b=2*5=10

// 默认参数也可以是一个函数调用
function getDefault() {
  return 10;
}

function add(a, b = getDefault()) {
  return a + b;
}

console.log(add(5)); // 15

没有重载

ECMAScript 函数不能像传统编程那样重载。ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。

如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的。

function addSomeNumber(num) {
  return num + 100;
}
function addSomeNumber(num) {
  return num + 200;
}
let result = addSomeNumber(100); // 300

参数扩展与收集

扩展操作符(...)可以将一个可迭代对象(如数组)展开为多个元素。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。

在函数调用中使用扩展操作符

function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

// 也可以与其他参数混用
console.log(sum(0, ...numbers)); // 3,注意这里0是a,然后数组展开为1,2,3,所以b和c是1和2,但实际是0,1,2 -> 0+1+2=3
// 注意:上面的例子中,0是第一个参数,然后数组展开为1,2,3,所以实际上参数是0,1,2,所以结果是3。但我们的数组是[1,2,3],所以如果我们要用扩展操作符传递整个数组,那么函数就会按顺序使用数组的每个元素作为参数。

在函数参数中使用扩展操作符(剩余参数)

剩余参数(rest parameters)可以将多个参数收集到一个数组中。

function sum(...numbers) {
  return numbers.reduce((acc, curr) => acc + curr, 0);
}

console.log(sum(1, 2, 3, 4)); // 10

// 也可以与其他参数一起使用
function multiply(multiplier, ...numbers) {
  return numbers.map((n) => n * multiplier);
}

console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]

函数内部

在 ECMAScript 5 中,函数内部存在两个特殊的对象:argumentsthisECMAScript 6又新增 了new.target 属性。

arguments

arguments虽然主要用于包含函数参数,但arguments对象其实还有一个callee属性,是一个指向arguments对象所在函数的指针。来看下面这个经典的阶乘函数:

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

阶乘函数一般定义成递归调用的,就像上面这个例子一样。只要给函数一个名称,而且这个名称不 会变,这样定义就没有问题。但是,这个函数要正确执行就必须保证函数名是 factorial,从而导致 了紧密耦合。使用arguments.callee就可以让函数逻辑与函数名解耦

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
let trueFactorial = factorial;
factorial = function () {
  return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0

this

this 是函数执行时的上下文对象,它的值取决于函数的调用方式。

调用方式决定 this 的值
  1. 普通函数调用:在非严格模式下,this 指向全局对象(浏览器中为 window);在严格模式下,this 为 undefined。
  2. 方法调用:当函数作为对象的方法调用时,this 指向该对象。
  3. 构造函数调用:使用 new 操作符调用函数时,this 指向新创建的对象。
  4. 使用 call、apply、bind 调用:这些方法可以显式设置 this 的值。
// 普通函数调用
function normalFunc() {
  console.log(this); // 非严格模式:全局对象;严格模式:undefined
}
normalFunc();

// 方法调用
var obj = {
  method: function () {
    console.log(this); // obj
  },
};
obj.method();

// 构造函数调用
function Constructor() {
  this.property = "value";
  console.log(this); // 新创建的对象实例
}
var instance = new Constructor();

// 使用call和apply
function func() {
  console.log(this);
}
var customThis = { name: "custom" };
func.call(customThis); // customThis
func.apply(customThis); // customThis

// 使用bind
var boundFunc = func.bind(customThis);
boundFunc(); // customThis

caller

ECMAScript 5 也会给函数对象上添加一个属性:caller。这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);
}
outer();

以上代码会显示 outer()函数的源代码。这是因为 ourter()调用了 inner(),inner.caller 指向 outer()。如果要降低耦合度,则可以通过arguments.callee.caller来引用同样的值:

function outer() {
  inner();
}
function inner() {
  console.log(arguments.callee.caller);
}
outer();

严格模式下访问arguments.callee会报错。ECMAScript 5 也定义了arguments.caller,但在严格模式下访问它会报错,在非严格模式下则始终是undefined严格模式下还有一个限制,就是不能给函数的 caller 属性赋值,否则会导致错误。

new.target

ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。 ECMAScript 6 新增了检测函数是否使用new关键字调用的new.target属性。如果函数是正常调用的,则new.target 的值是 undefined;如果是使用 new 关键字调用的,则new.target将引用被调用的构造函数。

function King() {
  if (!new.target) {
    throw new Error('King must be instantiated using "new"');
  }
  console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"

函数属性与方法

ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:lengthprototype

其中,length属性保存函数定义的命名参数的个数

function sayName(name) {
  console.log(name);
}
function sum(num1, num2) {
  return num1 + num2;
}
function sayHi() {
  console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0

prototype 是保存引用类型所有实例方法的地方,这意味着toString()valueOf()等方法实际上都保存在 prototype 上,进而由所有实例共享。在 ECMAScript 5 中,prototype 属性是不可枚举的,因此使用for-in循环不会返回这个属性。

函数还有 3 个方法:apply()call()bind(),这三个方法都用于改变函数体内 this 的指向。

  • call apply 都是立即调用函数,而 bind 是返回一个绑定了 this 的新函数,需要再次调用。
  • call apply 的区别在于参数传递方式:call 是逐个传递,apply 是数组形式。

call

call 方法调用一个函数,并指定函数体内 this 的指向,以及参数列表(逐个传递)。

function.call(thisArg, arg1, arg2, ...)

function greet(message) {
  console.log(`${message}, ${this.name}`);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello'); // 输出:Hello, Alice

apply

apply 方法与 call 方法类似,也是调用函数,并指定 this 的指向,但参数是以数组(或类数组对象)的形式传递。

function.apply(thisArg, [argsArray])

function greet(message1, message2) {
  console.log(`${message1}, ${this.name}. ${message2}`);
}

const person = { name: 'Bob' };

greet.apply(person, ['Hello', 'How are you?']); // 输出:Hello, Bob. How are you?

bind

bind 方法创建一个新的函数,这个新函数的 this 被指定为 bind 的第一个参数,其余参数将作为新函数的参数,供调用时使用。

function.bind(thisArg[, arg1[, arg2[, ...]]])

function greet(message) {
  console.log(`${message}, ${this.name}`);
}

const person = { name: 'Charlie' };
// 写法一
//const greetCharlie = greet.bind(person);
//greetCharlie('Hi'); // 输出:Hi, Charlie

//写法二
const greetCharlie = greet.bind(person, "Hi");
greetCharlie();

bind 方法返回的函数,如果使用 new 操作符调用,则绑定的 this 会被忽略(因为 new 操作符会创建一个新对象,并将 this 指向它)。但是,传入的参数仍然会被使用。

function Person(name) {
  this.name = name;
}

const BoundPerson = Person.bind({}); // 绑定一个空对象

const instance = new BoundPerson("Alice");
console.log(instance.name); // Alice
console.log(instance instanceof Person); // true

call、apply 和 bind 的应用场景示例

  1. 继承中调用父构造函数

    function Parent(name) {
      this.name = name;
    }
    
    function Child(name, age) {
      Parent.call(this, name); // 调用父构造函数,继承属性
      this.age = age;
    }
    
    const child = new Child("David", 10);
    console.log(child); // { name: 'David', age: 10 }
    
  2. 获取数组的最大值

    const numbers = [5, 6, 2, 3, 7];
    
    // 因为 Math.max 不接受数组,所以用 apply 将数组展开为参数
    const max = Math.max.apply(null, numbers);
    console.log(max); // 7
    
    // 在 ES6 中,我们也可以用扩展运算符,但 apply 在 ES5 中很常用
    const max2 = Math.max(...numbers);
    
  3. 绑定事件处理函数的 this

    const obj = {
      value: "Hello",
      handleClick: function () {
        console.log(this.value);
      },
    };
    
    // 如果不绑定,this 会指向触发事件的元素
    // 使用 bind 确保 this 指向 obj
    document
      .getElementById("button")
      .addEventListener("click", obj.handleClick.bind(obj));
    
  4. 部分应用函数(柯里化)

    function multiply(a, b) {
      return a * b;
    }
    
    // 创建一个新函数,将第一个参数固定为 2
    const double = multiply.bind(null, 2);
    
    console.log(double(5)); // 10
    
  5. 手写

    Function.prototype.myCall = function (context, ...args) {
      // 如果context是null或undefined,默认指向全局对象(浏览器中为window)
      context = context || window;
      // 将当前函数作为context的一个方法
      const fn = Symbol("fn");
      context[fn] = this;
      // 调用函数,传递参数
      const result = context[fn](...args);
      // 删除添加的方法
      delete context[fn];
      return result;
    };
    
    Function.prototype.myApply = function (context, argsArray) {
      // 如果context是null或undefined,默认指向全局对象(浏览器中为window)
      context = context || window;
      // 将当前函数作为context的一个方法
      const fn = Symbol("fn");
      context[fn] = this;
      // 调用函数,传递参数
      const result = context[fn](...argsArray);
      // 删除添加的方法
      delete context[fn];
      return result;
    };
    Function.prototype.myBind = function (context, ...bindArgs) {
      const self = this;
      return function (...callArgs) {
        // 合并绑定参数和调用参数
        const args = bindArgs.concat(callArgs);
        return self.apply(context, args);
      };
    };
    
    function greet(message) {
      console.log(`${message},I am ${this.name}`);
    }
    
    const obj = { name: "Alice" };
    const greetCharlie = greet.myBind(obj);
    
    greet.myCall(obj, "Hello"); // Hello,I am Alice
    greet.myApply(obj, ["Hi"]); // Hi, I am Alice
    greetCharlie("Welcome"); // Welcome,I am Alice
    

递归

递归函数通常的形式是一个函数通过名称调用自己

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

优化:在写递归函数时使用arguments.calleearguments.callee是一个指向正在执行的函数的指针,可以避免函数变量名对原始函数的引用丢失问题。

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}

尾递归

在 JavaScript 中,递归调用可能会遇到栈溢出的问题,尤其是当递归深度很大时。尾递归是一种优化递归的技术,它可以避免栈溢出,但是需要注意的是,目前 JavaScript 引擎对尾递归的支持有限,但在 ES6 标准中已经提出了尾递归优化的规范,不过在实际浏览器中可能并未完全实现。

尾递归的概念:当一个函数在最后一步(不一定是最后一行)调用自身,并且该调用是函数体中最后执行的语句,且该调用的返回值直接被当前函数返回,这样的递归称为尾递归。

尾递归优化(Tail Call Optimization, TCO)的原理:由于尾递归调用是函数最后一步操作,所以当前函数的栈帧已经不需要保留,因此可以复用当前栈帧,从而避免栈帧的不断累积,防止栈溢出。

前面的递归代码不是尾递归,因为最后一步是乘法运算,而不是直接调用自身。在尾递归版本中,则是使用一个累加器来保存中间结果,最后一步是直接调用自身,并且没有其他的运算。

function factorialTail(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return arguments.callee(n - 1, n * accumulator);
}

对这两者的调用栈简单可视化:

// 普通递归的调用栈(深度为 n)
factorial(5);
5 * factorial(4);
5 * (4 * factorial(3));
5 * (4 * (3 * factorial(2)));
5 * (4 * (3 * (2 * factorial(1))));
5 * (4 * (3 * (2 * 1)));
5 * (4 * (3 * 2));
5 * (4 * 6);
5 * 24;
120;

// 尾递归优化的调用栈(深度为 1)
factorialTail(5, 1);
factorialTail(4, 5);
factorialTail(3, 20);
factorialTail(2, 60);
factorialTail(1, 120);
120;

第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多少次嵌套函数,都只有一个栈帧。

同样的对斐波那契数列进行改造:

//非尾递归版本
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

//尾递归版本
function fibonacciTail(n, a = 0, b = 1) {
  if (n === 0) return a;
  if (n === 1) return b;
  return fibonacciTail(n - 1, b, a + b);
}

闭包

概念

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

function outerFunction() {
  const outerVariable = "我在外部函数中";

  function innerFunction() {
    console.log(outerVariable); // 访问外部函数的变量
  }

  return innerFunction;
}

const closure = outerFunction();
closure(); // "我在外部函数中"

闭包形成条件

形成闭包有三个条件

  1. 函数嵌套
  2. 内部函数引用外部函数
  3. 内部函数在外部函数之外被调用
function createClosure() {
  let count = 0; // 外部变量
  //内部函数(闭包)
  return function () {
    count++;
    return count;
  };
}
// 即使 createClosure 执行完毕,返回的函数仍然可以访问 count
const counter = createClosure();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

闭包中的 this

当在闭包中使用 this 时,由于闭包通常是在另一个函数内部定义的函数,它不会自动继承外部函数的 this。因此,闭包中的 this 通常指向全局对象(非严格模式)或 undefined(严格模式)

为了在闭包中访问外部函数的 this,可以使用以下方法:

  • 将外部函数的 this 赋值给一个变量(通常命名为 thatself_this),然后在闭包中使用该变量。

    function OuterFunction() {
      // 将 this 保存到变量中
      let self = this;
      this.value = 10;
    
      function InnerFunction() {
        // 使用 self 访问外部函数的 this
        console.log(self.value); // 10
      }
    
      InnerFunction();
    }
    
    new OuterFunction();
    
  • 使用 bindcallapply 来绑定 this

    function OuterFunction() {
      this.value = 10;
    
      function InnerFunction() {
        console.log(this.value); // 10
      }
    
      // 使用 bind 将 InnerFunction 的 this 绑定到 OuterFunction 的 this
      let boundInnerFunction = InnerFunction.bind(this);
      boundInnerFunction();
    }
    
    new OuterFunction();
    
  • 使用箭头函数,因为箭头函数不会创建自己的 this,它会捕获其所在上下文的 this

    function OuterFunction() {
      this.value = 10;
    
      // 箭头函数捕获定义时的 this
      const InnerFunction = () => {
        console.log(this.value); // 10
      };
    
      InnerFunction();
    }
    
    new OuterFunction();
    

在事件处理回调中,我们经常使用闭包,而事件处理函数中的 this 通常指向触发事件的元素。如果我们想在闭包中访问外部函数的 this,就需要使用上述方法。

示例:事件处理中的闭包

function MyClass() {
  this.value = 42;
  this.element = document.getElementById("myButton");

  this.element.addEventListener("click", function () {
    // 这个回调函数中的 this 指向 element,而不是 MyClass 的实例
    console.log(this); // 输出:element
    // 如果我们想访问 MyClass 实例的 value,我们可以使用闭包和变量保存
  });
}

改进:

function MyClass() {
  this.value = 42;
  this.element = document.getElementById("myButton");

  // 方法1:使用变量保存 this
  let self = this;
  this.element.addEventListener("click", function () {
    console.log(self.value); // 42
  });

  // 方法2:使用箭头函数
  this.element.addEventListener("click", () => {
    console.log(this.value); // 42
  });

  // 方法3:使用 bind
  this.element.addEventListener(
    "click",
    function () {
      console.log(this.value); // 42
    }.bind(this)
  );
}

内存泄露

闭包内存泄漏通常是因为闭包持有对外部变量的引用,导致这些变量无法被垃圾回收。常见于不再需要的变量仍然被闭包引用,从而无法释放内存。

以下是常见的闭包内存泄漏模式:

  1. DOM 元素引用泄露

    // 错误的做法:闭包间接引用 DOM 元素
    function createDOMLeak() {
      const container = document.getElementById("container");
      const bigData = new Array(1000000).fill("leaky data");
    
      // 事件处理函数形成闭包,引用 bigData 和 container
      container.addEventListener("click", function () {
        console.log("Container clicked, data size:", bigData.length);
        console.log("Container id:", container.id);
      });
    
      // 即使从 DOM 中移除 container,它也无法被垃圾回收
      // 因为事件处理函数的闭包仍然引用着 container 和 bigData
    }
    
    // 正确的做法:避免不必要的引用
    function createSafeDOMHandler() {
      const container = document.getElementById("container");
    
      // 使用弱引用或者避免在闭包中保存不必要的引用
      function handleClick() {
        // 不引用外部变量,或者只引用真正需要的变量
        console.log("Container clicked");
      }
    
      container.addEventListener("click", handleClick);
    
      // 提供清理方法
      return {
        destroy: function () {
          container.removeEventListener("click", handleClick);
        },
      };
    }
    
  2. 定时器和回调函数泄漏

    // ❌ 错误的做法:闭包引用外部大对象
    function createTimerWithLeak() {
      // 大数据对象 - 会被闭包引用
      const bigData = {
        items: new Array(100000).fill("some data"),
        metadata: { created: Date.now(), type: "large dataset" },
      };
    
      let count = 0;
    
      const timerId = setInterval(function () {
        // 这个匿名函数形成了闭包,引用了外部的 bigData
        count++;
        console.log(
          `Timer execution ${count}, data items: ${bigData.items.length}`
        );
    
        if (count >= 3) {
          clearInterval(timerId);
          console.log("Timer stopped");
          // 问题:bigData 仍然被闭包引用,无法被垃圾回收!
        }
      }, 1000);
    }
    
    // ✅ 正确的做法:分离大数据和定时器逻辑
    function createTimerWithoutLeak() {
      let count = 0;
      const timerId = setInterval(function () {
        // 这个闭包不再引用外部大数据
        count++;
        console.log(`Timer execution ${count}`);
    
        if (count >= 3) {
          clearInterval(timerId);
          console.log("Timer stopped - bigData can be garbage collected");
        }
      }, 1000);
    
      // 大数据只在函数内部临时使用,不会被闭包长期引用
      const bigData = {
        items: new Array(100000).fill("some data"),
        metadata: { created: Date.now(), type: "large dataset" },
      };
    
      // 立即使用大数据,然后让它超出作用域
      console.log("Initial data size:", bigData.items.length);
      // bigData 在这里使用后,就可以被垃圾回收了
    }
    
  3. 循环引用问题

    // ❌ 错误的做法
    function setupLeakyEventListeners() {
      const buttons = document.querySelectorAll(".button");
      const bigData = new Array(100000).fill("shared data");
    
      buttons.forEach(function (button, index) {
        button.addEventListener("click", function () {
          // 每个事件监听器都闭包引用了整个 bigData
          console.log(`Button ${index} clicked, data:`, bigData.length);
        });
      });
    
      // 即使移除所有按钮,bigData 仍然被所有事件监听器引用
    }
    
    // ✅ 正确的做法
    function setupSafeEventListeners() {
      const buttons = document.querySelectorAll(".button");
      const listeners = [];
    
      buttons.forEach(function (button, index) {
        // 只保存需要的最小数据
        const buttonData = { index: index };
    
        function handleClick() {
          console.log(`Button ${buttonData.index} clicked`);
          // 不引用大数据
        }
    
        button.addEventListener("click", handleClick);
        listeners.push({ button, handleClick });
      });
    
      // 提供清理方法
      return {
        destroy: function () {
          listeners.forEach(({ button, handleClick }) => {
            button.removeEventListener("click", handleClick);
          });
          listeners.length = 0; // 清空数组
        },
      };
    }
    

立即调用的函数表达式

立即调用匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。下面是一个简单的例子:

(function () {
  //块级作用域
})();

使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这 样位于函数体作用域的变量就像是在块级作用域中一样。

let count = 5;
// for (var i = 0; i < count; i++) {
//   console.log(i);
// }
// console.log(i); // 5

//IIFE
(function () {
  for (var i = 0; i < count; i++) {
    console.log(i);
  }
})();
console.log(i); // i is not defined

私有变量

在 JavaScript 中,私有变量是指那些只能在定义它们的类或函数内部访问的变量。从外部无法直接访问这些变量。实现私有变量的方式有多种,包括使用闭包、Symbol、WeakMap 以及最新的 ES2022 中引入的类私有字段(以#前缀表示)。

使用闭包(Closure)

闭包是 JavaScript 中实现私有变量的一种传统方式。通过函数作用域来隐藏变量,只暴露必要的接口。

function createCounter() {
  let count = 0; // 私有变量
  return {
    increment: function () {
      count++;
      return count;
    },
    decrement: function () {
      count--;
      return count;
    },
    getCount: function () {
      return count;
    },
  };
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1

// 无法直接访问 count
console.log(counter.count); // undefined

使用类构造函数和闭包

在类构造函数中,可以利用闭包来创建私有变量。但是这种方法会导致每个实例都拥有自己的一套方法,而不是共享原型上的方法,可能会消耗更多内存。

function Person(name) {
  // 私有变量
  let age = 0;

  this.getName = function () {
    return name;
  };

  this.getAge = function () {
    return age;
  };

  this.setAge = function (newAge) {
    if (newAge >= 0) {
      age = newAge;
    }
  };

  this.growOlder = function () {
    age++;
  };
}

const person = new Person("Alice");
person.growOlder();
console.log(person.getName()); // 'Alice'
console.log(person.getAge()); // 1

// 无法直接访问 name 和 age
console.log(person.name); // undefined
console.log(person.age); // undefined

使用模块模式

模块模式是闭包的一种扩展,它可以将私有变量和函数封装在一个立即调用的函数表达式(IIFE)中,并返回一个公共接口。

const MyModule = (function () {
  let privateVar = 0;

  function privateFunction() {
    return privateVar;
  }

  return {
    publicMethod: function () {
      privateVar++;
      return privateFunction();
    },
    anotherPublicMethod: function () {
      return privateVar;
    },
  };
})();

console.log(MyModule.publicMethod()); // 1
console.log(MyModule.anotherPublicMethod()); // 1

// 无法访问 privateVar 和 privateFunction

使用 Symbol

Symbol是 ES6 引入的一种新的原始数据类型,每个Symbol都是唯一的。可以使用 Symbol 作为属性键,这样在类外部就无法直接访问这些属性,因为它们不在外部作用域中。但是,这种方法并不是真正的私有,因为可以通过Object.getOwnPropertySymbols获取到 Symbol 属性。

const Person = (function () {
  const ageSymbol = Symbol("age");
  class Person {
    constructor(name) {
      this.name = name;
      this[ageSymbol] = 0;
    }
    getAge() {
      return this[ageSymbol];
    }

    setAge(age) {
      if (age >= 0) {
        this[ageSymbol] = age;
      }
    }

    growOlder() {
      this[ageSymbol]++;
    }
  }
  return Person;
})();

const person = new Person("Bob");
person.growOlder();
console.log(person.name); // 'Bob'
console.log(person.getAge()); // 1

// 无法直接访问 ageSymbol 对应的属性
// console.log(person[ageSymbol]); // ReferenceError: ageSymbol is not defined

// 但是可以通过 Object.getOwnPropertySymbols 获取
const symbols = Object.getOwnPropertySymbols(person);
console.log(person[symbols[0]]); // 1

使用 WeakMap

WeakMap 是 ES6 中一种新的集合类型,它允许将对象作为键。可以使用 WeakMap 来存储每个实例的私有变量。这样,私有变量与实例绑定,且外部无法直接访问 WeakMap 中的值。

const Person = (function () {
  const privateData = new WeakMap();

  class Person {
    constructor(name) {
      // 将私有数据存储在WeakMap中,以实例为键
      privateData.set(this, { age: 0, name: name });
    }

    getName() {
      return privateData.get(this).name;
    }

    getAge() {
      return privateData.get(this).age;
    }

    setAge(age) {
      if (age >= 0) {
        privateData.get(this).age = age;
      }
    }

    growOlder() {
      const data = privateData.get(this);
      data.age++;
    }
  }

  return Person;
})();

const person = new Person("Charlie");
person.growOlder();
console.log(person.getName()); // 'Charlie'
console.log(person.getAge()); // 1

// 无法直接访问私有数据
console.log(person.name); // undefined
console.log(person.age); // undefined

期约与异步函数

在 JavaScript 中,异步编程是一个核心概念,它允许我们执行长时间运行的任务(如网络请求、文件读取等)而不会阻塞主线程。传统的异步编程方式包括回调函数,但回调函数容易导致“回调地狱”(Callback Hell),使得代码难以阅读和维护。为了解决这些问题,ES6 引入了 Promise(期约),而 ES8 则引入了 async/await(异步函数),使得异步代码的编写和阅读更加直观。

Promise(期约)

Promise 是一个对象,它代表了一个异步操作的最终完成(或失败)及其结果值。

Promise 的状态

一个 Promise 有三种状态:

  • pending(待定):初始状态,既没有被兑现,也没有被拒绝。
  • fulfilled(已兑现):意味着操作成功完成。
  • rejected(已拒绝):意味着操作失败。

Promise 的状态一旦改变,就不会再变。状态改变只有两种可能:从 pending 变为 fulfilled,或者从 pending 变为 rejected。

创建 Promise

可以使用 Promise 构造函数来创建一个 Promise 对象。构造函数接受一个函数作为参数,这个函数有两个参数:resolvereject,它们都是函数。

  • 当异步操作成功时,我们调用resolve(value),将 Promise 的状态改为 fulfilled,并将结果值传递出去。
  • 当异步操作失败时,我们调用reject(reason),将 Promise 的状态改为 rejected,并将错误原因传递出去。
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const random = Math.random();
    if (random > 0.5) {
      resolve("成功!");
    } else {
      reject("失败!");
    }
  }, 1000);
});

使用 Promise

可以使用.then().catch().finally()方法来处理 Promise 的结果。

  • .then(onFulfilled, onRejected):接受两个函数参数,第一个是 Promise 成功时的回调,第二个是失败时的回调(可选)。
  • .catch(onRejected):是.then(null, onRejected)的别名,用于指定发生错误时的回调函数。
  • .finally(onFinally):无论 Promise 最终状态如何,都会执行的操作。
myPromise
  .then((value) => {
    console.log(value); // 当Promise被兑现时执行
  })
  .catch((error) => {
    console.error(error); // 当Promise被拒绝时执行
  })
  .finally(() => {
    console.log("无论成功或失败都会执行");
  });

Promise 链

.then()方法返回一个新的 Promise,因此我们可以链式调用。在链式中,前一个.then()返回的值会被后一个.then()接收。如果返回的是一个 Promise,则后一个.then()会等待这个 Promise 解决。

const promise = new Promise((resolve, reject) => {
  resolve(1);
});

promise
  .then((value) => {
    console.log(value); // 1
    return value + 1;
  })
  .then((value) => {
    console.log(value); // 2
    return new Promise((resolve) => {
      setTimeout(() => resolve(value + 1), 1000);
    });
  })
  .then((value) => {
    console.log(value); // 3
  });

Promise 的静态方法

  • Promise.resolve(value): 返回一个以给定值解析的 Promise。

  • Promise.reject(reason): 返回一个带有拒绝原因的 Promise。

  • Promise.all(iterable): 等待所有 Promise 完成,或者任何一个拒绝。

    const promise1 = Promise.resolve(3);
    const promise2 = 42;
    const promise3 = new Promise((resolve, reject) => {
      setTimeout(resolve, 100, "foo");
    });
    
    Promise.all([promise1, promise2, promise3])
      .then((values) => {
        console.log(values); // [3, 42, "foo"]
      })
      .catch((error) => {
        console.error(error);
      });
    
  • Promise.race(iterable): 等待任何一个 Promise 完成或拒绝。

    const promise1 = new Promise((resolve, reject) => {
      setTimeout(resolve, 500, "one");
    });
    
    const promise2 = new Promise((resolve, reject) => {
      setTimeout(resolve, 100, "two");
    });
    
    Promise.race([promise1, promise2])
      .then((value) => {
        console.log(value); // "two" 因为promise2更快
      })
      .catch((error) => {
        console.error(error);
      });
    
  • Promise.allSettled(iterable): 等待所有 Promise 完成(不管成功或失败)。

    const promises = [
      Promise.resolve(1),
      Promise.reject("错误"),
      Promise.resolve(3),
    ];
    
    Promise.allSettled(promises).then((results) => {
      results.forEach((result, index) => {
        if (result.status === "fulfilled") {
          console.log(`Promise ${index}:`, result.value);
        } else {
          console.log(`Promise ${index}:`, result.reason);
        }
      });
    });
    
  • Promise.any(iterable): 等待任何一个 Promise 成功,如果全部拒绝则拒绝。

    const promises = [
      Promise.reject("错误1"),
      Promise.resolve("成功1"),
      Promise.resolve("成功2"),
    ];
    
    Promise.any(promises)
      .then((result) => {
        console.log(result); // "成功1"
      })
      .catch((error) => {
        console.error("所有 Promise 都失败");
      });
    

异步函数(Async/Await)

异步函数是使用async关键字声明的函数,它使得异步代码的书写更加像同步代码。异步函数内部可以使用await关键字,await会暂停异步函数的执行,等待Promise解决,然后继续执行异步函数并返回结果。

使用 async 和 await

  • async 函数返回一个 Promise。如果函数返回一个值,这个值会被包装成 Promise(相当于 Promise.resolve(返回值))。如果函数抛出错误,则返回的 Promise 会被拒绝。
  • await 只能在 async 函数内部使用。await 后面可以跟一个 Promise,然后等待这个 Promise 解决。如果等待的不是 Promise,则会被转换成已解决的 Promise。
async function myAsyncFunction() {
  try {
    const result = await myPromise; // 等待myPromise解决,然后将结果赋值给result
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}

myAsyncFunction();

错误处理

在 async 函数中,可以使用 try/catch 来捕获 await 表达式中的错误。如果 await 后面的 Promise 被拒绝,await 会抛出拒绝的原因,因此可以使用 try/catch 来捕获。

异步函数的优势

  • 代码更清晰,避免了回调地狱(Callback Hell)和复杂的 Promise 链。
  • 错误处理更直观,可以使用 try/catch。
  • 使得异步代码的流程控制更加容易,例如使用循环和条件语句。

注意事项

  • 异步函数不会阻塞整个程序,它内部的 await 会暂停该函数的执行,但不会阻塞其他代码。
  • 多个 await 如果没有依赖关系,可以考虑使用 Promise.all 来并行执行,提高效率。

示例:并行执行

async function parallel() {
  const [result1, result2] = await Promise.all([asyncTask1(), asyncTask2()]);
  console.log(result1, result2);
}

异步迭代

使用for-await-of循环来处理异步可迭代对象。

async function processAsyncIterable(asyncIterable) {
  for await (const item of asyncIterable) {
    console.log(item);
  }
}
posted @ 2025-12-01 17:33  小风车吱呀转  阅读(1)  评论(0)    收藏  举报