面试题-js篇

文章目录

  • 一、数据类型
    • [1. JavaScript有哪些数据类型,它们的区别?](#1. JavaScript有哪些数据类型,它们的区别?)
      • 1.1.null和undefined区别
      • [1.2. typeof null 的结果是什么,为什么?](#1.2. typeof null 的结果是什么,为什么?)
    • 2.NaN
      • [2.1.typeof NaN 的结果是什么?](#2.1.typeof NaN 的结果是什么?)
      • [2.2.isNaN 和 Number.isNaN 函数的区别?](#2.2.isNaN 和 Number.isNaN 函数的区别?)
    • 3.判断数据类型方式
      • [3.1.intanceof 操作符的实现原理及实现](#3.1.intanceof 操作符的实现原理及实现)
    • [4. 判断数组的方式有哪些](#4. 判断数组的方式有哪些)
    • [4. 为什么0.1+0.2 ! == 0.3,如何让其相等](#4. 为什么0.1+0.2 ! == 0.3,如何让其相等)
    • [5. 如何获取安全的 undefined 值?](#5. 如何获取安全的 undefined 值?)
    • 6.js类型转换规则
      • [6.1. `==` 操作符的强制类型转换规则?及与 `===`、`Object.is()`的区别?](#6.1. == 操作符的强制类型转换规则?及与 ===Object.is()的区别?)
    • [7. || 和 && 操作符的返回值?](#7. || 和 && 操作符的返回值?)
    • [9. 什么是 JavaScript 中的包装类型?](#9. 什么是 JavaScript 中的包装类型?)
    • [10. 为什么会有BigInt的提案?](#10. 为什么会有BigInt的提案?)
    • [11. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别](#11. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别)
    • [12. 如何判断一个对象是空对象](#12. 如何判断一个对象是空对象)
  • 二、ES6
    • [1. ES6常用新特性](#1. ES6常用新特性)
    • 1.let、const、var的区别
    • 2、箭头函数及和普通函数的区别
    • 3.模板字符串
    • [4.函数参数增强:默认参数 + 剩余参数](#4.函数参数增强:默认参数 + 剩余参数)
    • 5.对象、数组的解构
    • [6.数组 / 对象扩展:扩展运算符(...)(浅拷贝)](#6.数组 / 对象扩展:扩展运算符(...)(浅拷贝))
    • [7. 面向对象:Class](#7. 面向对象:Class)
    • [8. 模块化:import /export](#8. 模块化:import /export)
    • [9. 新数据结构:Set / Map](#9. 新数据结构:Set / Map)
    • [10. 数组、字符串方法](#10. 数组、字符串方法)
      • [10.1. 字符串](#10.1. 字符串)
      • [10.2. 数组](#10.2. 数组)
    • [11. Symbol](#11. Symbol)
    • 12、for...in(es5)、for...of(es6)
    • [13. Proxy 可以实现什么功能?](#13. Proxy 可以实现什么功能?)
    • [14. 如何提取高度嵌套的对象里的指定属性?](#14. 如何提取高度嵌套的对象里的指定属性?)
  • 三、异步编程
    • [1. 什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?](#1. 什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?)
    • 2、Promise(解决回调地狱)
    • 3.async/await(es8)
    • [4. async/await对比Promise的区别](#4. async/await对比Promise的区别)
    • [5、generator. ??](#5、generator. ??)
    • [6. 并发与并行的区别?](#6. 并发与并行的区别?)
    • [7. setTimeout、setInterval、requestAnimationFrame 各有什么特点?](#7. setTimeout、setInterval、requestAnimationFrame 各有什么特点?)
    • [8、事件循环 EventLoop ?](#8、事件循环 EventLoop ?)
  • 四、JavaScript基础
    • [1. new操作符的实现原理](#1. new操作符的实现原理)
    • [2. js有哪些内置对象](#2. js有哪些内置对象)
    • [3. 对JSON的理解](#3. 对JSON的理解)
    • [4. js脚本`<script>`延迟加载的方式有哪些?](#4. js脚本<script>延迟加载的方式有哪些?)
    • [5. js 类数组对象的定义?](#5. js 类数组对象的定义?)
    • [6. Unicode、UTF-8、UTF-16、UTF-32的区别?](#6. Unicode、UTF-8、UTF-16、UTF-32的区别?)
    • [7. 常见的位运算符有哪些?其计算规则是什么?](#7. 常见的位运算符有哪些?其计算规则是什么?)
    • [8. 什么是 DOM 和 BOM?](#8. 什么是 DOM 和 BOM?)
    • [9. escape、encodeURI、encodeURIComponent 的区别](#9. escape、encodeURI、encodeURIComponent 的区别)
    • [10. JavaScript为什么要进行变量提升,它导致了什么问题?](#10. JavaScript为什么要进行变量提升,它导致了什么问题?)
    • [11. 什么是尾调用,使用尾调用有什么好处?](#11. 什么是尾调用,使用尾调用有什么好处?)
    • [12. use strict是什么意思 ? 使用它区别是什么?](#12. use strict是什么意思 ? 使用它区别是什么?)
    • [13. 如何判断一个对象是否属于某个类?](#13. 如何判断一个对象是否属于某个类?)
    • [14. 对AJAX的理解,实现一个AJAX请求](#14. 对AJAX的理解,实现一个AJAX请求)
    • [15. ajax、axios、fetch的区别](#15. ajax、axios、fetch的区别)
  • [五、原型与原型链 ??](#五、原型与原型链 ??)
    • [41. 对原型、原型链的理解](#41. 对原型、原型链的理解)
    • [42. 原型修改、重写](#42. 原型修改、重写)
    • [43. 原型链指向](#43. 原型链指向)
    • [44. 原型链的终点是什么?如何打印出原型链的终点?](#44. 原型链的终点是什么?如何打印出原型链的终点?)
    • [45. 如何获得对象非原型链上的属性?](#45. 如何获得对象非原型链上的属性?)
  • 六、执行上下文/作用域链/闭包
    • [1. 对闭包的理解](#1. 对闭包的理解)
      • [1.1 .使用场景四、防抖与节流](#1.1 .使用场景四、防抖与节流)
    • [2. 对作用域、作用域链的理解](#2. 对作用域、作用域链的理解)
    • [3. 对执行上下文的理解](#3. 对执行上下文的理解)
  • 七、this/call/apply/bind
    • [1. 对this对象的理解](#1. 对this对象的理解)
    • [2. call() 和 apply() 的区别?](#2. call() 和 apply() 的区别?)
    • [3. 实现call、apply 及 bind 函数](#3. 实现call、apply 及 bind 函数)
    • [连续多个 bind,最后this指向是什么?](#连续多个 bind,最后this指向是什么?)
  • 八、面向对象
    • [1. 对象创建的方式有哪些?](#1. 对象创建的方式有哪些?)
    • [2. 对象继承的方式有哪些?](#2. 对象继承的方式有哪些?)
  • 九、垃圾回收与内存泄漏
    • [1. 浏览器的垃圾回收机制](#1. 浏览器的垃圾回收机制)
    • [2. 哪些情况会导致内存泄漏](#2. 哪些情况会导致内存泄漏)

一、数据类型

1. JavaScript有哪些数据类型,它们的区别?

总结: 7种基本数据类型 (Undefined、Null、Boolean、Number、String、Symbol、BigInt)+ 1种引用数据类型(Object)

区别:

  • 存储位置不同: 基本 (值本身存储在栈中);引用(地址存储在栈中,值存储在堆中)
  • 可变性: 基本 (不可变。修改,本质是创建了新的值,而非修改原有的值);引用(可变)
  • 比较方式: 基本 (按值比较);引用(按引用比较,比较地址)
  • 复制方式: 基本 (复制后会创建一个新的独立值);引用(复制的是地址引用,指向同一个对象)

其中Symbol 和 BigInt 是ES6 中新增的数据类型:

  • Symbol(ES6):唯一且不可变的标识符,用于对象属性名。
  • BigInt(ES2020):任意精度整数,后缀需加 n,如 9007199254740993n。

1.1.null和undefined区别

总结: 都是基本数据类型 ;undefined表示未定义 ,如声明了还没定义会返回它;null表示空对象 ,通常用来初始化,赋值给会返回对象的变量。

相同点: undefined 和 null 都是基本数据类型

不同点

  • undefined 代表的含义是未定义null 代表的含义是空对象
  • 变量声明了但还没有定义 的时候会返回 undefinednull主要用于赋值 给一些可能会返回对象的变量,作为初始化。
  • 使用 typeof 进行判断时,Null 类型化会返回 "object"

1.2. typeof null 的结果是什么,为什么?

总结: null使用typeof判断时,会返回object,因为对象类型的二进制标识符是000,而null被认为是一个空指针,即零地址(值全是0),所以会被错误的标识为object

结果是"object"

javascript的值是二进制数据 ,用低位表示数据类型
对象的类型 标识符 是000 ,null被认为是一个空指针,即零地址,值全是0 ,所以被错误的标识为object

javascript 复制代码
000: object   - 当前存储的数据指向一个对象。
  1: int      - 当前存储的数据是一个 31 位的有符号整数。
010: double   - 当前存储的数据指向一个双精度的浮点数。
100: string   - 当前存储的数据指向一个字符串。
110: boolean  - 当前存储的数据是布尔值。

2.NaN

NaN(Not-a-Number)是 js 中被视为数字类型的一个特殊值 ,表示无法计算出合法的数值
特点:

  • typeof NaN 仍然是 number,尽管它表示"不是数字"。
  • NaN任何值都不相等,包括它自身 ,即 NaN !== NaN。
    - isNaN(NaN) 返回 true

2.1.typeof NaN 的结果是什么?

总结: NaN意思不是一个数字 ,用来标识数学运算的结果非法 的或者未定义 的。在js中它被视为数字类型里 的一个特殊值 。所以typeof NaN 的结果是number

NaN 表示 "Not a Number",用于表示非法的或未定义的数学运算结果。

typeof NaN 的结果是 "number" ,因为 NaN 在 js 中被视为数字类型的一个特殊值

javascript 复制代码
typeof NaN; // "number"
//不用typeof判断,用isNaN判断
console.log(isNaN(123)) // 输出 false,因为 123 可以转换成数值
console.log(isNaN(NaN)) // 输出 true,因为 NaN 是 NaN

2.2.isNaN 和 Number.isNaN 函数的区别?

总结: 两者都是用来判断数据是否是NaN数据类型 的。区别在isNaN 判断之前会先将值转成数值 再去判断,这就导致一些不是NaN的值也被误判为NaN ;而Number.isNaN不转类型 ,只有参数本身是NaN 时才会返回 true。

  • isNaN(value)是 js 提供的全局方法。
    • 作用:将传入的值转换为数值,然后检查该数值是否为 NaN
    • 注意点:它会在检查前 尝试将参数转换为数值 ,导致一些非 NaN 值也被误判为 NaN
  • Number.isNaN(value)ES6 引入的方法,它不会进行类型转换 ,只有当参数本身是 NaN 时才会返回 true
javascript 复制代码
console.log(isNaN(NaN));        // true
console.log(isNaN('hello'));    // true ("hello" 无法转换成数字,最终变成 NaN)
console.log(isNaN('123'));      // false (字符串 "123" 可转换为数字 123)
console.log(isNaN(undefined));  // true (undefined 转换成 NaN)
console.log(isNaN(null));       // false (null 转换为 0)

//
console.log(Number.isNaN(NaN));        // true
console.log(Number.isNaN('hello'));    // false (不会进行类型转换)

3.判断数据类型方式

总结: 判断基本数据类型typeof;判断引用 数据类型用instanceof(判断数组还可以用Array.isArray);更复杂的用Object.prototype.toString.call()

  • typeof
    • 判断基本数据 类型(除了null),返回字符串
    • 注意点:数组、对象、null都会被判断为object
  • instanceof:只能判断引用数据 类型,而不能判断基本数据类型
  • Object.prototype.toString.call():Object 对象的原型方法 toString 来判断数据类型
  • constructor不能判断null,undefined。其它的都可以。由于类的constructor可以随意更改,此时会存在判断不准确的问题
  • Array.isArray():判断是否是数组
javascript 复制代码
console.log(typeof true);            // boolean

console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true

var a = Object.prototype.toString;
console.log(a.call(function(){}));
console.log(a.call(null));

console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true

3.1.intanceof 操作符的实现原理及实现

原理:通过检查对象的原型链 ,看对象的原型链上是否存在指定类型的原型
实现 :当你使用 object instanceof Type 时,js 引擎会沿着 object 的原型链向上查找 ,看是否能在原型链中找到 Type.prototype 。如果找到,就返回 true;否则返回 false。

javascript 复制代码
function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

4. 判断数组的方式有哪些

  • Object.prototype.toString.call()
  • 原型链
  • ES6的Array.isArray()(常用)
  • instanceof(常用)
  • Array.prototype.isPrototypeOf
javascript 复制代码
Object.prototype.toString.call(obj) === 'Array';
obj.__proto__ === Array.prototype;
Array.isArrray(obj);
obj instanceof Array
Array.prototype.isPrototypeOf(obj)

4. 为什么0.1+0.2 ! == 0.3,如何让其相等

总结: 浮点数问题的本质是二进制存储精度误差 ,像0.1这样的小数无法被二进制精确表示,所以计算时会出现误差。
解决方法: toFixed(控制误差范围);转成整数计算

javascript 复制代码
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2)  // 0.30000000000000004

console.log(n1*10+n2*10)
//让其相等。toFixed(num)方法可把 Number 四舍五入为指定小数位数的数字
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入

5. 如何获取安全的 undefined 值?

总结: es5之前 它是个可写属性,可以修改它的值会出现问题,所以使用void标识 符,后面放任意表达式,永远返回undefined;es6之后,它被定义为不可写了。

es5 之前undefined是全局对象的一个可写属性 ,开发者可以修改它的值,会出现问题
es5 之后,全局的 undefined 被定义为不可写

void 运算符的特性

  • void 会执行其后的表达式 ,但无论表达式结果是什么永远返回 undefined
  • void 0 是最简洁的写法(0 是合法且最短的表达式)
javascript 复制代码
undefined = 123; // 在旧环境中可能成功
console.log(undefined); // 可能输出 123,而不是预期的 undefined

//void 
console.log(void 0);       // undefined
console.log(void "hello"); // undefined
console.log(void (1+1));   // undefined

6.js类型转换规则

总结: 分为显式和隐式类型转换显式 主要用到String()、Number()、Boolean();Boolean()主要是用运算符,如比较运算符、算术运算 符。其中><优先转数字;+有字符串转字符串,没转数字;其他-、*、/都转数字;==按优先级转。

  • 显式类型转换
    • 转为字符串: toString()String()
      • boolean->"true"/"false"
      • undefined、null->"undefined"/"null"
      • {}->"[object Object]"
      • [1,2,3]->"1,2,3"
    • 转为数字: Number()parseInt()(没Number严格,逐个解析字符,遇到不能转换的字符就停下来)、parseFloat()
      • string->空字符串0;仅数字的字符串数字;非纯数字字符串NaN
      • boolean->0/1
      • undefined、null->NaN/0
      • Object->仅包含单个数字的数组数字;其他NaN
    • 转为布尔: Boolean()
      • 转为false的情况:undefined、null、0、-0、""、NaN
      • 转为true的情况:{}、[]、其他非空值
  • 隐式类型转换
    • 比较运算(==、>、<)、ifwhile需要布尔值地方
      • >、<:优先转数字
      • if、while:自动转布尔值
      • ==:按优先级转换类型
    • 算术运算+、-、*、/、%
      • +: 有一边是string,就拼接;没有string,转number
      • 其他运算符:转成Number

显式类型转换

javascript 复制代码
//转为字符串
String(true) // "true"/"false"
String(undefined) // "undefined"
String(null) // "null"
//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

//转数字
Number('') // 0
Number('324') // 324
Number('324abc') // NaN
Number(true) // 1/0
Number(undefined) // NaN
Number(null) // 0
// 对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

隐式类型转换

javascript 复制代码
"3" + 4;        // "34"(数字转字符串)
[] + {};        // "[object Object]"([] 转 "",{} 转 "[object Object]")
true + 1;       // 2(true → 1)
null + 1;       // 1(null → 0)

"5" - 2;    // 3
"10" / "2"; // 5
"abc" - 1;  // NaN

"10" > 2;    // true("10" → 10)
"abc" > 1;   // false("abc" → NaN,比较返回 false)

if ("hello") { ... }  // true(非空字符串)

//对象先调用 valueOf()(默认返回对象自身),再调用 toString()
[] + 1;          // "1"([] → "" → 字符串拼接)
{} + [];         // 0({} 被解析为空代码块,实际计算 +[] → 0)

6.1. == 操作符的强制类型转换规则?及与 ===Object.is()的区别?

  • == :抽象相等,比较前先类型转换,再比值
  • ===:严格相等,比较两个值的数据和类型是否全相等
  • Object.is():行为与 === 基本一致,除**+0不等于-0;NaN等于自身**

== 比较规则

  • NaN不等于任何值,包括自己
  • null和undefined不等于其它,除了null和undefined
  • 字符串 vs 数字 → 字符串转数字
  • 布尔值 vs 其他类型 → 布尔值转数字
  • 对象 vs 原始类型 → 对象转原始值(先使用valueOf,再使用toString)

对象转原始值 (valueOf、toString)

调用 valueOf():尝试获取原始值。

  • 如果返回原始值,则使用它。
  • 如果返回对象 ,继续调用 toString():将对象转为字符串

内置对象使用valueof()返回

  • 对象: 返回 对象本身
  • 数组: 返回 数组本身
  • Date: 返回 时间戳(数字)
  • 包装对象: 返回 原始值
javascript 复制代码
//对象 valueOf() 返回对象本身
{} == "[object Object]";  // true
{} == 0;                  // "[object Object]" → NaN → false

//数组 valueOf() 返回数组本身
[] == 0;       // [] → "" → 0 → true
[1] == "1";    // [1] → "1" → true
[1, 2] == 0;   // [1,2] → "1,2" → NaN → false

//Date valueOf() 返回时间戳(数字)
const date = new Date();
date == date.toString();  // date → 时间戳 → 与字符串比较 → false
date == date.valueOf();    // 时间戳 == 时间戳 → true

//包装对象(Number、String、Boolean)valueOf() 返回原始值
new Number(1) == 1;    // true(Number对象 → 1)
new String("a") == "a";// true(String对象 → "a")

7. || 和 && 操作符的返回值?

  • ||:寻找第一个真值,常用于默认值。
  • &&:寻找第一个假值,常用于条件执行或安全访问
  • && 的优先级高于 ||

9. 什么是 JavaScript 中的包装类型?

总结:基本数据类型 (String、Number、Boolean)包装成有属性和方法的对象 。如string.length、number.toFixed等。js在内部自动包装 ,所以可以直接使用,就等同于new实例化

js中,原始值没有方法或属性 ,但为了能够 使用方法 和属性。

js提供了包装类型允许基本类型(如String、Number、Boolean)临时"包装"为对象,从而调用对象方法。,比如字符串和数字的 length 属性、toUpperCase() 方法等。

三种基本包装类型

  • String(字符串包装类型): 对原始字符串类型 string 的包装(如 toUpperCase()、toLowerCase() 等)
  • **Number(数字包装类型):**对原始 number 类型的包装,它为数字提供一些方法(如toFixed() 或 toPrecision())
  • **Boolean(布尔值包装类型):**原始布尔类型 boolean 的包装
javascript 复制代码
//js在内部自动包装为 String 对象
let greeting = "hello";
console.log(greeting.toUpperCase());  // 输出: "HELLO"
//等同于
let wrappedStr = new String("hello");
console.log(wrappedStr.toUpperCase());  // 输出: "HELLO"

console.log((3.213445).toFixed(2));  // 输出: "3.14"
//等同于
let wrappedNum = new Number(num);
console.log(wrappedNum.toFixed(2));  // 输出: "42.00"

let isActive = false;
if (new Boolean(isActive)) {
  console.log("This is active");
} else {
  console.log("This is not active");  // 这行代码会被执行
}

10. 为什么会有BigInt的提案?

总结: 因为js中number类型 有一个最大安全整数限制 ,在这个范围内,精度不会丢失;超过这个范围,计算就会不准确了。因此用BigInt处理大数计算时精度问题

BigInt的提案主要是由于js中Number类型在处理大数时的精度问题。‌

js中的Number类型 有一个最大安全整数限制 ,即Number.MAX_SAFE_INTEGER。

在这个范围内,计算结果不会出现精度丢失

但一旦超过这个范围,就会出现计算不准确的情况。

这在需要进行大数计算时,不得不依靠一些第三方库来解决,因此官方提出了BigInt来解决此问题‌。

11. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别

  • 浅拷贝 ‌:
    • 只复制对象的第一层属性 ,嵌套对象是引用复制。修改嵌套对象的属性会影响原对象。
    • 实现方式: 扩展运算符...Object.assign()
  • 深拷贝 ‌:
    • 递归复制对象及其所有嵌套对象 ,确保每个层级的内容都被独立复制,不再共享引用地址。
    • 实现方式: JSON.parse(JSON.strigify())

JSON.parse(JSON.strigify())缺点

  • 无法 处理函数和undefined
  • 无法 处理循环饮用(对象某属性引用了对象本身)
  • 无法 处理一些特殊对象 ,如Error、Date,会被转为空对象
  • 对象的原型链上的属性会丢失(如果属性是继承下来的)

12. 如何判断一个对象是空对象

  • JSON.strigify(obj) == '{}'
  • Object.keys(obj).length === 0

二、ES6

1. ES6常用新特性

  • let、const(解决 var声明的变量存在变量提升、重复声明覆盖、块级无隔离三大问题)
  • 箭头函数(解决了「this 绑定混乱」的核心问题,简化写法)
  • 模板字符串(优化字符串)
  • 函数参数增强:默认参数 + 剩余参数(rest) ...rest(在键那边,用了存储数据)
  • 对象、数组的解构(简化变量赋值)
  • 数组 / 对象扩展:扩展运算符(...) ...[1,2] (在值那边,用来解构值)
  • 面向对象:Class
  • 模块化:import /export
  • 新数据结构:Set / Map
  • 异步编程:Promise(解决回调地狱)
  • 数组、字符串新方法
  • Symbol

1.let、const、var的区别

总结: 从5个方面来说,

1、作用域上 ,var有函数作用域(全局变量和局部变量);let、const有块级作用域({})。

2、变量提升 上,var可以变量提升,在声明前访问;let、const不行。

3、重复声明 ,var可以;let、const不可以

4、值的可变性 ,var、let可以,const不可以

5、全局对象属性,var全局变量会挂载到window对象上;而let、const没有

let 声明变量;const声明常量
区别

  • 作用域:
    • var:有函数作用域。有全局变量和函数级变量
    • let、const:有块级作用域,仅在声明的代码块(由 {} 界定)内有效
  • 变量提升‌:
    • var存在变量提升可以在声明之前被访问,其初始值为 undefined。‌
    • let、const不存在变量提升 。在声明之前访问会引发引用错误,此区域称为"暂时性死区"。
  • 重复声明‌:
    • var允许 在同一作用域内重复声明 同一个变量,后者会覆盖前者。‌
    • let、const不允许在 同一作用域内重复声明 同一个变量,否则会引发语法错误。
  • 值的可变性‌:
    • var、let:变量在初始化后,其值可以被重新赋值修改
    • const:声明常量时必须立即初始化 ,且基本类型的值不可更改引用类型 (如对象、数组),其指向的内存地址不可变 ,但对象属性或数组元素可以修改
  • 全局对象属性:
    • var全局作用域下声明变量时,该变量会挂载到window对象上
    • let、const:没有全局对象属性

2、箭头函数及和普通函数的区别

总结: 箭头函数简化 了普通函数的写法 ;箭头函数没有自己的this ,它的this来自外层包裹函数(声明时的this);没有arguments对象,也不能new调用。

简化了函数的声明语法,更解决了「this 绑定混乱」的核心问题

特性:(和普通函数的区别)

  • **语法简化:**省略function关键字,写法:()=>{}
  • this 绑定固定: 箭头函数没有自己的this ,它的this继承自「外层作用域的 this」 (即声明时的 this,而非调用时的 this);
  • **无 arguments 对象:**无法通过arguments获取参数列表,需用「剩余参数」替代
  • **不能作为构造函数:**不能用new关键字调用,否则报错
javascript 复制代码
const add = (a, b) => a + b;
console.log(add(3, 4)); // 7

const calculate = (a, b) => {
  const sum = a + b;
  const product = a * b;
  return sum + product; // 必须显式return
};
console.log(calculate(2, 3)); // 2+3 + 2*3 = 11

const user = {
  name: "张三",
  // 传统函数:this指向调用者(user)
  sayName1: function() {
    setTimeout(function() {
      console.log(this.name); // undefined(this指向window/global)
    }, 100);
  },
  // 箭头函数:this继承外层作用域(user)
  sayName2: function() {
    setTimeout(() => {
      console.log(this.name); // 张三(正确)
    }, 100);
  }
};
user.sayName1(); // undefined
user.sayName2(); // 张三

3.模板字符串

ES6 之前,拼接字符串需要用**+连接**,遇到多行字符串或变量插值时,代码冗长且易出错。

ES6之后,使用模板字符串 用 **反引号()** 包裹,用${}`来插入变量

特性:

  • 多行字符串: 直接换行即可,无需\n或+
  • 变量 / 表达式插值:${}包裹变量或表达式,自动解析并拼接
  • **标签模板:**可通过函数自定义模板字符串的解析逻辑
js 复制代码
// 1. 多行字符串:直接换行
const html = `
  <div class="user">
    <h1>用户信息</h1>
    <p>姓名:张三</p>
  </div>
`;
console.log(html); // 输出带换行的完整HTML字符串
 
// 2. 变量插值:${}包裹变量
const name = "张三";
const age = 25;
const info = `我叫${name},今年${age}岁`;
console.log(info); // 我叫张三,今年25岁
 
// 3. 表达式插值:${}内可写任意表达式
const a = 10;
const b = 20;
const calc = `${a} + ${b} = ${a + b}`;
console.log(calc); // 10 + 20 = 30
 
// 4. 标签模板(进阶):自定义解析逻辑
function format(strings, ...values) {
  // strings:模板字符串的静态部分数组
  // values:插值部分的数组
  return strings[0] + values[0].toUpperCase() + strings[1];
}
const result = format`Hello, ${name}!`;
console.log(result); // Hello, 张三!(若name是小写,会转为大写)

4.函数参数增强:默认参数 + 剩余参数

默认参数: 在函数声明时直接为参数设置默认值,当参数未传递或为undefined时生效
剩余参数: 用...rest表示,收集函数的「剩余参数」并组成一个数组,替代arguments(arguments是类数组,需转换为数组才能用数组方法)

javascript 复制代码
//默认参数
function add(a, b = 0) { // 正确:非默认参数在前
  return a + b;
}

//剩余参数
function printFirstAndRest(first, ...rest) {
  console.log("第一个参数:", first);  //1
  console.log("剩余参数:", rest); // [2,3,4]
}
printFirstAndRest(1, 2, 3, 4);

// 3. 对比arguments:剩余参数是数组,arguments是类数组
function compareArgs(...rest) {
  console.log(rest instanceof Array); // true(数组)
  console.log(arguments instanceof Array); // false(类数组)
  // arguments需转换为数组才能用reduce:Array.from(arguments).reduce(...)
}
compareArgs(1, 2, 3);

5.对象、数组的解构

总结: 对象按属性名匹配;数组按下标匹配

数组解构:按位置匹配 (根据数组的「索引位置 」匹配变量,支持默认值、嵌套解构)
对象解构:按属性名匹配 (根据对象的「属性名」匹配变量,支持别名、默认值、嵌套解构)

javascript 复制代码
// 1. 基本数组解构
const [a, b, c] = [10, 20, 30];
// 2. 跳过元素:用空逗号占位
const [x, , y] = [1, 2, 3];  //x=1;y=3
// 3. 默认值:当解构的值为undefined时使用默认值
const [m, n = 5] = [3]; // m=3;n=5
// 4. 嵌套数组解构
const [p, [q, r]] = [1, [2, 3]]; //p=1;q=2;r=3
// 5. 与扩展运算符结合:获取剩余元素
const [first, ...rest] = [1, 2, 3, 4];  //first=1;rest=[2,3,4]

6.数组 / 对象扩展:扩展运算符(...)(浅拷贝)

扩展运算符(...)可以将「可迭代对象 」(数组、Set、字符串等)或「对象」展开为单个元素 / 属性,常用于数组拷贝、 合并 ,对象拷贝、合并等场景

**数组扩展:**将数组展开为逗号分隔的元素列表,支持拷贝、合并、函数传参

**对象扩展:**将对象展开为键值对列表,支持对象拷贝、合并、添加新属性

javascript 复制代码
// 1. 数组拷贝:创建新数组(浅拷贝)
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 等价于arr1.slice() 
arr2.push(4);
console.log(arr2); // [1, 2, 3, 4]
 
// 2. 数组合并:替代concat方法
const arr3 = [4, 5, 6];
const mergedArr = [...arr1, ...arr3]; // 合并arr1和arr3, 结果[1, 2, 3, 4, 5, 6]
 
// 3. 函数传参:将数组展开为函数参数
function add(a, b, c) {
  return a + b + c;
}
const nums = [1, 2, 3];
console.log(add(...nums)); // 6(等价于add(1, 2, 3))

//对象用法同数组
const obj1 = { a: 1, b: 2 };
const newObj = { ...obj1, c: 3, a: 10 }; // 新增c,修改a。结果{ a: 10, b: 2, c: 3 }

7. 面向对象:Class

总结: 使用class定义类 ,里面可以定义属性、方法、constructor(构造函数)。使用它的好处是,可以使用extends继承 这个类,还可以继承后再扩展 ,增加了代码的复用性。(子类使用父类的构造函数 需要用super调用

特性

  • 类定义 :用class关键字定义 ,内部包含constructor(构造函数)和方法
  • 继承 :用extends关键字实现类的继承super()调用父类构造函数
  • 静态方法 :用static关键字定义静态方法 ,只能通过类调用,不能通过实例调用;
  • 实例方法:类内部定义的方法,只能通过实例调用
javascript 复制代码
// 1. 定义基础类
class Person {
  // 构造函数:实例化时执行,初始化属性
  constructor(name, age) {
    this.name = name; // 实例属性
    this.age = age;
  }
  // 实例方法:通过实例调用
  sayHi() {
    console.log(`我叫${this.name},今年${this.age}岁`);
  }
  // 静态方法:通过类调用,this指向类本身
  static createPerson(name, age) {
    return new Person(name, age); // 工厂方法,创建Person实例
  }
}
 
// 实例化类
const person1 = new Person("张三", 25);
person1.sayHi(); // 我叫张三,今年25岁(调用实例方法)
 
// 调用静态方法
const person2 = Person.createPerson("李四", 30);
person2.sayHi(); // 我叫李四,今年30岁
 
// 2. 继承:定义子类Student继承自Person
class Student extends Person {
  // 子类构造函数:必须调用super()
  constructor(name, age, studentId) {
    super(name, age); // 调用父类构造函数,初始化name和age
    this.studentId = studentId; // 子类新增属性
  }
  // 子类重写父类方法
  sayHi() {
    console.log(`我是学生${this.name},学号${this.studentId}`);
  }
  // 子类新增方法
  study() {
    console.log(`${this.name}正在学习`);
  }
}
 
// 实例化子类
const student1 = new Student("王五", 20, "2024001");
student1.sayHi(); // 我是学生王五,学号2024001(重写后的方法)
student1.study(); // 王五正在学习(子类新增方法)
console.log(student1 instanceof Student); // true
console.log(student1 instanceof Person); // true(子类实例也是父类实例)

8. 模块化:import /export

核心概念:

  • 模块(Module):一个 JS 文件就是一个模块,内部变量 / 函数默认对外不可见;
  • 导出(Export) :将模块内的变量 / 函数暴露给外部,分为「命名导出」和「默认导出」;
  • 导入(Import) :在其他模块中引入已导出的变量 / 函数
javascript 复制代码
// 方式1:声明时直接导出
export const PI = 3.14159;
export function add(a, b) {
  return a + b;
}
// 导出时重命名:用"原名称 as 新名称"
export { add as addFn,funA,... };
// 默认导出
export default greet = (name) => `Hello, ${name}`;

//导入其他模块
import { PI, add, funA as  testFn } from './moduleA.js';
//导入所有
import * as moduleA from './moduleA.js';
console.log(moduleA.PI); 

9. 新数据结构:Set / Map

总结: 都是es6新增的数据类型,Set类似于数组 ,存值(且无重复,自动去重),可以用for...of遍历 ,常用方法add、delete、hasMap类似对象 ,它的键可以是任意类型可以遍历forEachfor...ofkeys(所有键名)、values ( 所有键值)等),常用方法get、set、delete、has

  • Set:无重复值的集合
    • 存储唯一值:自动去重,不允许重复元素
    • 可迭代:支持for...of遍历,可转换为数组
    • 常用方法add()(添加)、delete()(删除)、has()(判断是否存在)、size(获取长度)、clear()(清空)
  • Map:键值对集合(键可任意类型)
    • 键可以是任意类型:如 Number、String、Object、Symbol 等(对象的键只能是字符串 / Symbol);
    • 可迭代 :按插入顺序排列,遍历:forEachfor...ofkeys()(所有键名)、values() ( 所有键值)、entries()(所有键值对);
    • 常用方法set(key, value)(添加键值对)、get(key)(获取值)、delete(key)(删除)、has(key)(判断)、size(长度)、clear()(清空)

9.1.Map和Object的区别

总结: 从5个方面来说,

1、键的数据类型 :map是任意值 ;object必须是string或Symbol

2、键值对顺序 :map有序;object无序

3、方法 :map有增删改方法;object没有

4、遍历 :map可以遍历;object不行

5、性能:map在增删改场景下更好

区别:

  • 键的数据类型: Map的键可以是任意值,而Object的键必须是一个String或是Symbol。
  • 键值对的顺序: Map中的key是有序的,而Object的键是无序的。
  • 功能和方法: Map提供 set(), get(), has(), delete(), size 等方法,而Object没有内置的方法支持快速增删查改,需手动实现或用工具。
  • 迭代: Map可以直接被迭代,而Object不可以直接被迭代。
  • 性能: Map在频繁增删键值对的场景下表现更好,而Object的效率比较差。

9.2. weakSet和Set区别

  • WeakSet中只能存放对象类型
  • WeakSet对元素的引用是弱引用,若没有对某个对象进行引用,那么垃圾回收机制可以对该对象进行回收;
  • 常见方法: add、delete、has
  • WeakSet不能遍历

9.3.weakMap和Map区别

  • WeakMap的key只能使用对象
  • WeakMap的key对 对象的引用是弱引用,如果没有引用这个对象,那么垃圾回收机制可以回收该对象
  • 常见方法: set、get、delete、has
  • WeakSet不能遍历

10. 数组、字符串方法

  • 字符串方法总结(不修改原字符串)
    • ES5 是基础,重点掌握:indexOf、slice(截取)、split(切割)、trim、replace、toUpperCase/toLowerCase
    • ES6+ 是升级,优先使用:includes(替代 indexOf)、startsWith/endsWith、模板字符串
  • 数组方法总结
    • 修改原数组的方法: push(后加)、pop(后删)、unshift(前加)、shift(前删)、splice(增删改)、reverse(翻转)、sort(排序)、fill(填充)
    • 不改原数组的方法: includes、indexOf、lastIndexOf、slice(截取)、concat(拼接)
    • 遍历: forEach、map、filter、every、some、find、findIndex、reduce(累积)
    • 创建数组/类数组转数组: Array.of (创建)、Array.from(转)

10.1. 字符串

  • ES5 原生方法(所有浏览器兼容)‌
    • 字符 / 索引 查找类(获取字符 / 查找位置)
      • charAt(下标):获取指定下标的单个字符
      • charCodeAt(下标):获取指定下标 的单个字符的 Unicode 编码值
      • indexOf(指定字串, 起始下标):查找指定的子串 / 字符 ,返回首次出现的索引值
      • lastIndexOf(指定字串):末尾向前查找指定的子串 / 字符,返回最后一次出现的索引值
    • 字符串 截取 / 切割类(提取指定片段)
      • slice(起始下标, 截止下标)截取字符串中指定区间的内容,返回新字符串
      • substring(起始下标, 截止下标):同上
    • 大小写 转换类
      • toUpperCase() / toLowerCase():全部转为大写/小写,返回新字符串
    • 内容 替换 / 分割/拼接类
      • split(分隔符, 限制返回的长度):按照指定分隔符切割 ,转为一个数组返回
      • replace(替换前字串, 替换后字串):第一个匹配到 的 替换前字串 替换成 替换后字串,返回新字符串
      • concat(str1, str2, ...):拼接 字符串,返回拼接后的新字符串
      • trim():去除首尾的所有空白符(空格、换行符、制表符)(注:部分早期浏览器如 IE8 不支持,但通常视为 ES5 基础方法)‌
  • ES6 及之后新增方法‌
    • 包含判断类
      • includes(查找字串, 起始下标) :是否包含指定的子串 / 字符,返回 true / false
      • startsWith(查找字串, 起始下标):是否以指定的子串开头
      • endsWith(查找字串, 检测的长度):是否以指定的子串结尾
    • 重复拼接类
      • repeat(数字):将原字符串 重复拼接 n 次,返回拼接后的新字符串
    • 补全填充类(字符串补位,实用)
      • padStart(补全后长度, 填充字串):从字符串的开头 填充指定内容,直到字符串的长度达到设置的补全后长度,返回补全后的新字符串
      • padEnd(补全后长度, 填充字串):从字符串的末尾 填充指定内容,直到字符串的长度达到 设置的补全后长度,返回补全后的新字符串
    • 模板字符串(反引号 ${var}

10.2. 数组

  • ES5 原生方法(不会修改原数组)‌
    • slice(起始下标, 截止下标):数组截取(切片)
    • concat(arr1, arr2, ...):数组 合并
    • join(分隔符):数组转字符串
    • indexOf(查找元素,起始下标):查找元素索引(前--->后查找)
    • lastIndexOf(查找元素,起始下标):查找元素索引(后--->前查找)
    • includes (查找元素):判断数组是否包含指定元素
    • arr.flat(深度):数组扁平化,将「多维数组」转换为「一维数组」。arr.flat(1/Infinity)扁平化1层/所有层
    • flatMap (callback):先 map ,再 flat (1) 扁平化,相当于 arr.map().flat(1)
    • 数组遍历
      • forEach(callback):数组遍历。(无返回值)arr.forEach((item, index, arr) => { 执行逻辑 })
      • find(callback)/findIndex(callback) :→ 遍历数组,返回第一个满足条件的元素 / 索引
      • map(callback):数组(格式化、数据转换 ),对每个元素做处理 / 转换。(返回新数组)arr.map((item, index, arr) => { return 处理后的元素 })
      • filter(callback):数组过滤,根据条件筛选 出符合要求。(返回新数组)arr.filter((item, index, arr) => { return 筛选条件 })
      • some(callback):数组是否有元素 满足条件。arr.some((item, index, arr) => { return 判断条件 })
      • every(callback):数组所有元素是否 满足条件arr.every((item, index, arr) => { return 判断条件 })
      • reduce(callback, 初始值):数组累积(用于数组求和、求平均值、去重、扁平化、分组统计),每个元素的处理结果累积到一个变量中,返回这个累积值。`arr.reduce((prev, item) => prev + item, 0)
  • ES5 原生方法(会修改原数组)‌
    • push(...元素):末尾追加元素
    • pop():末尾删除元素(只删除一次最后一个元素)
    • unshift(...items):开头追加元素
    • shift():开头删除元素
    • sort(compareFunction):数组排序。(不传参:字符串排序;传参a-b升序、b-a降序)sort((a,b) => a - b)
    • reverse():数组反转
    • splice(起始下标, 删除的个数0, ...添加 / 替换的元素)‌:万能增删改。 splice(2,0,3,4)增34splice(2,1,99)替换下标为2的元素
    • fill(填充的值, 起始下标, 截止下标):数组填充。fill(0)全部填0;fill(99,1,3)下标1到下标3填99
  • ES6 及之后新增方法‌
    • Array.from(伪数组) : 伪数组转真数组
    • Array.of(...items/数字) :传单个数字:数组长度;传多个值:数组的参数。Array.of(3)长度为3的数组;Array.of(2,3)数组为[2,3]
javascript 复制代码
//reduce
// 场景1:求和(最基础)
const sum = arr.reduce((prev, item) => prev + item, 0)
// 场景2:求数组中最大值
const max = arr.reduce((prev, item) => prev > item ? prev : item, 0)
// 场景3:数组去重(进阶)
const uniqueArr = arr.reduce((prev, item) => {
  if(!prev.includes(item)) prev.push(item)
  return prev
}, [])

11. Symbol

总结: 主要特点是独一无二,适合用来做对象的唯一属性值,避免属性名冲突;也常用来定义一些不希望被遍历到的私有属性;或者是定义常量集合。

通过 Symbol() 函数创建,调用都会生成一个完全唯一的值 ,即使传入相同的描述文本,生成的 Symbol 也不相等

使用 Symbol.for() 创建全局可复用的 Symbol

使用场景

  • 作为对象的唯一属性键,避免属性名冲突
  • 定义对象的 "私有" 属性(模拟私有性): Symbol 键名不会出现在遍历 中,只能通过**Object.getOwnPropertySymbols() 访问**,起到隐藏属性的作用
  • 定义常量集合,避免魔法字符串:
javascript 复制代码
console.log(Symbol('foo') !== Symbol('foo')); // true

//对象的唯一属性键
const user = {};
const nameA = Symbol('name');
const nameB = Symbol('name');
user[nameA] = 'A';
user[nameB] = 'B';
console.log(user[nameA] !== user[nameB]) //true

//对象的 "私有" 属性
const secretKey = Symbol('secret');
const obj = {
  public: '公开信息',
  [secretKey]: '敏感信息' // 模拟私有属性
};

//常量集合
const STATUS = {
  PENDING: Symbol('pending'),
  SUCCESS: Symbol('success'),
  ERROR: Symbol('error')
};

12、for...in(es5)、for...of(es6)

总结

  • for...in 循环只遍历对象,获取键
  • for...of 循环可以用来遍历数组、 类数组对象, 字符串、 Set、 Map 以及 Generator 对象,获取值

详解
for...of概念 :允许遍历一个含有 迭代器 接口的数据结构 。(数组、对象等)并且返回各项的值
for...in 和 for...of 的区别

  • for...of 遍历获取 的是对象的键值 for (let key in obj) for...in 获取 的是对象的键名for (let val in arr)
  • for... in遍历对象整个原型链性能非常差 不推荐使用; 而for ... of 只遍历 当前对象 不会遍历 原型链
  • 遍历数组for...in 会返回数组中所有可枚举的属性 (包括原型链上 可枚举的属性); for...of返回数组的下标对应的属性值;

13. Proxy 可以实现什么功能?

总结: 意思是代理。用来拦截对象的操作,使用方法get、set、has等自定义逻辑处理

定义: Proxy 是 ES6 引入的一个强大功能,意思是"代理"。它可以用来拦截对象的操作 ,并在操作时进行自定义的逻辑处理new Proxy(target, handler)

常用的拦截操作(handler )

  • get ------ 拦截对象属性的读取
  • set ------ 拦截对象属性的 赋值
  • has ------ 拦截 in 操作符
  • deleteProperty ------ 拦截 delete 操作
  • apply ------ 拦截函数调用
  • construct ------ 拦截 new 操作
javascript 复制代码
const user = {name: "Tom",age: 18};
const proxy = new Proxy(user, {
	  get(target, prop) {
	    if (prop === "age") {
	      return target[prop] + " 岁"; 
	    }
	    return target[prop];
	  },
	  set(target, prop, value) {
	    if (prop === "age") {
	      if (typeof value !== "number") {
	        throw new TypeError("年龄必须是数字!");
	      }
	    }
	    target[prop] = value;
	    return true; // 必须返回 true 表示赋值成功
	  },
	  has(target, prop) {
	    if (prop === "password") {
	      return false; // 隐藏 password 属性
	    }
	    return prop in target;
	  },
	  deleteProperty(target, prop) {
	    if (prop === "name") {
	      console.log("name 属性不能被删除!");
	      return false;
	    }
	    return true;
	  }
 })
console.log("password" in proxy); // false
javascript 复制代码
//函数
function sum(a, b) {
  return a + b;
}
const proxy = new Proxy(sum, {
  apply(target, thisArg, args) {
    console.log("拦截函数调用,参数是:", args);
    return target(...args) * 2; // 返回值改为原函数结果的两倍
  }
});
console.log(proxy(1, 2)); // 6

// new
class Person {
  constructor(name) {
    this.name = name;
  }
}
const ProxyPerson = new Proxy(Person, {
  construct(target, args) {
    console.log("正在创建实例,参数:", args);
    return new target(...args);
  }
});
const p = new ProxyPerson("Tom"); // 正在创建实例,参数: [ 'Tom' ]

14. 如何提取高度嵌套的对象里的指定属性?

  • 已知结构:使用解构赋值
  • 结构不确定:使用可选链操作符?.
javascript 复制代码
const school = {
  classes: {
    stu: {
      name: 'Bob',
      age: 24
    }
  }
};

// 一行解构提取深层属性
const { classes: { stu: { name } } } = school;
console.log(name); // 'Bob'

//可选链操作符
const phone = school?.classes?.stu?.phone ?? '未提供';

三、异步编程

1. 什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

概念: 回调函数(Callback)是指将一个函数作为参数传递给另一个函数 ,并在特定时刻执行它 。在 JavaScript 中,回调函数常用于处理异步操作,如读取文件、请求数据、 定时器 等。

javascript 复制代码
function fetchData(callback) {
  setTimeout(() => {
    callback("Data loaded");
  }, 1000);
}
fetchData((result) => {
  console.log(result); // 输出:Data loaded
});

回调地狱产生的原因: 当需要执行多个异步操作时,每个操作都依赖前一个操作的结果,这时就会出现"回调嵌套"的情况。导致代码层级过深,形成"金字塔"结构,称为回调地狱

javascript 复制代码
doTask1((result1) => {
  doTask2(result1, (result2) => {
    doTask3(result2, (result3) => {
      console.log("All tasks completed:", result3);
    });
  });
});

缺点:

  • 可读性差:嵌套层级过深,代码逻辑混乱。
  • 维护困难:修改或调试时容易出错。
  • 错误处理复杂:多层嵌套导致异常捕获困难。

解决方案:(异步编程的实现方式)

  • Promise
  • async/await
  • Generator 函数

2、Promise(解决回调地狱)

总结: promise解决了回调地狱问题,使用.then链条式调用,避免嵌套。有三个状态pending、fulfilled、rejected;Promise对象方法.all(全成功才返回)、.race(跑的快的决定结果);实例方法.then(成功)、.catch(失败)、.finally(无论成功还是失败)

优势:

  • 链式调用 :通过 .then() 避免嵌套。
  • 统一错误处理 :通过 .catch() 捕获异常。

核心概念

  • 三个状态状态的改变 是通过在异步操作结束后调用 resolve() 和 reject() 函数来实现的)
    • pending初始状态,既未成功也未失败;
    • fulfilled:操作成功,状态不可逆;
    • rejected:操作失败,状态不可逆。
  • Promise对象-静态方法
    • ‌Promise.resolve()‌:将一个普通的值 转换 成 Promise 类型的数据。
      • 非 Promise 对象,将返回一个 Promise 对象,状态为 fulfilled,结果为参数值
      • Promise 对象,将返回的 Promise 对象的状态和参数,与参数相同
    • Promise.reject()‌:始终返回一个失败的 Promise 对象,并且不受参数的影响。‌‌
    • Promise.all([promise1, promise2]):同时执行多个Promise,全部成功才返回,有一个失败则立即失败
    • Promise.race([promise1, promise2]):同时执行多个Promise,第一个改变状态的Promise决定结果
  • 实例方法
    • then:状态变为fulfilled时执行,返回新的 Promise,支持链式调用(resolve())
    • catch:状态变为rejected时执行,捕获错误(reject())
    • finally:无论成功或失败,都会执行

写法结构

javascript 复制代码
let p1 = Promise.resolve(123);
let p1 = Promise.reject(456);
//等同于
const promise = new Promise(function(resolve, reject) {
  if (/* 异步操作成功 */){
    resolve(123);
  } else {
    reject(456);
  }
});

//Promise.resolve方法 参数及结果
let p1 = Promise.resolve(123);
console.log(p1); // Promise {<fulfilled>: 123}
let p2 = Promise.resolve(new Promise((resolve, reject) => { resolve(456); }));
console.log(p2); // Promise {<fulfilled>: 456}
let p3 = Promise.resolve(new Promise((resolve, reject) => { reject("error"); }));
console.log(p3); // Promise {<rejected>: "error"}

详细使用

javascript 复制代码
// 1. 创建Promise
function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟异步操作(如AJAX请求)
    setTimeout(() => {
      const success = true; // 模拟成功/失败
      if (success) {
        const data = { name: "张三", age: 25 };
        resolve(data); // 成功:调用resolve,状态变为fulfilled
      } else {
        const error = new Error("数据获取失败");
        reject(error); // 失败:调用reject,状态变为rejected
      }
    }, 1000);
  });
}
 
// 2. 使用Promise:链式调用
fetchData()
  .then((data) => {
    console.log("获取数据成功:", data);
    // 返回新的Promise,继续链式调用
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(data.name); // 将name传递给下一个then
      }, 500);
    });
  })
  .then((name) => {
    console.log("处理后的数据:", name); // 张三
  })
  .catch((error) => {
    console.log("错误:", error.message); // 捕获所有前面的错误
  })
  .finally(() => {
    console.log("无论成功或失败,都会执行"); // 可选,用于清理操作(如关闭加载动画)
  });
 
// 3. Promise常用静态方法
// 3.1 Promise.all():同时执行多个Promise,全部成功才返回,有一个失败则立即失败
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
Promise.all([promise1, promise2])
  .then((values) => {
    console.log(values); // [1, 2](所有成功,返回结果数组)
  })
  .catch((error) => {
    console.log(error);
  });
 
// 3.2 Promise.race():同时执行多个Promise,第一个改变状态的Promise决定结果
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, "快的Promise"));
const promise4 = new Promise((resolve) => setTimeout(resolve, 200, "慢的Promise"));
Promise.race([promise3, promise4])
  .then((value) => {
    console.log(value); // "快的Promise"(第一个成功的结果)
  });

3.async/await(es8)

总结: 用同步的方式写异步代码,
async声明函数是异步 的,返回值是Promise对象return的值 会被封装成Promise对象 ,根据值的类型自动改变状态 ;没有return值,返回undefined值的Promise对象)
await 是用来等待后面的表达式返回结果 ,若后面是Promise对象 ,会阻塞代码中断执行 ,若不想中断用try...catch包裹多个await Promise 捕获异常

概念: 是一种基于 Promise 实现的异步编程方式async/await 也是一种语法糖
优势: async/await 实现了用同步方式写异步代码 (promise是链式调用形式写异步代码)
缺点: 性能问题,await会阻塞代码

  • async : 用来声明一个function是异步的,返回Promise对象
    • 使用 return 语句返回的值 会被自动封装成 Promise 对象 , 该对象的状态 会根据 return 语句返回的值 类 型自动变为 resolved 或 rejected
    • 没有return ,则该函数返回一个 undefined 值的 Promise 对象
  • await在async 函数内部,它会暂停 async 函数的执行,等待 Promise 对象的状态改变,然后再继续执行 async 函数。
    • await不仅仅用于等Promise对象,可以等任意表达式的结果
    • await后面的 Promise 对象如果变为 reject 状态 ,则 reject 的参数 会被 catch 方法的回调函数接收到
    • 任何一个 await 语句后面的 Promise 对象变为 reject 状态 ,那么整个 async 函数都会中断执行 。(不想中断:try...catch

async代码写法

javascript 复制代码
// 成功
async function foo() {
	return 'Hello World!'
}
foo().then(value => console.log(value)) // 'Hello World!'

//失败
async function bar() {
	throw new Error('Something went wrong!')
}
bar().catch(error => console.error(error))  // 'Error: Something went wrong!'

//async 函数中没有 return 语句,则该函数返回一个 undefined 值的 Promise 对象
async function foo() {
	console.log('Hello World!')
	// 等同于 return Promise.resolve(undefined)
}
foo().then(value => console.log(value)) // 'Hello World!' // undefined

await代码写法

javascript 复制代码
async function foo() {
	await Promise.resolve('xx')
}
async function f() {
	await Promise.reject('出错了')
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))

// 有一个await就会中断函数
async function f() {
	await Promise.reject('出错了')
	await Promise.resolve('hello world') // 不会执行
}
// 不想中断
async function f() {
	try {
		await Promise.reject('出错了')
	} catch(e) {
	}
	return await Promise.resolve('hello world')
}
f().then(v => console.log(v)) // hello world

//多个await
async function main() {
	try {
		const val1 = await firstStep()
		const val2 = await secondStep(val1)
		const val3 = await thirdStep(val1, val2)
		console.log('Final: ', val3)
	} catch (err) {
		console.error(err)
	}
}

4. async/await对比Promise的区别

总结: 分为4个方面

1、语法差异: Promise 基于回调函数的异步处理方式 ;async/await是一种的语法糖,基于generator和promise (async声明异步,await等待结果)

2、可读性: Promise 链式调用 书写异步;async/await是同步 书写异步

3、异步操作的顺序: Promise 处理多个异步,要嵌套then的方式 ,或者all、race;async/await 同步代码

4、错误捕获和处理: Promise 用catch方法 ;async/await用try...catch

区别:

  • 语法差异
    • promise 是基于回调函数的异步处理方式,使用 then 和 catch 方法来处理异步操作的结果和错误。
    • async/await 是一种基于 生成器 函数(Generator)和 promise 的语法糖,通过在函数前面加上 async 关键字,将函数转换为返回 promise 对象的异步函数,并使用 await 关键字等待异步操作的结果。
  • 可读性
    • promise 以链式调用的形式书写异步代码。
    • async/await 以同步形式书写异步代码 。更加直观和易读,代码结构更加清晰。
  • 错误处理
    • promise 中,通常使用 catch 方法来捕获和处理错误
    • async/await 中,通常使用 try...catch 语句来捕获和处理错误
  • 异步操作的顺序
    • promise 中,如果有多个异步操作需要按照特定的顺序执行 ,我们需要通过嵌套的 then 方法或使用 Promise.all、Promise.race 等组合方法来实现。
    • async/await 中,可以使用同步的代码风格来编写异步操作的顺序,使代码更加简洁和可读。
  • 错误堆栈
    • promise 中,当发生错误时,错误 信息 中会包含详细的堆栈信息,可以追踪到错误发生的位置
    • async/await 中,由于使用了 try...catch 语句捕获错误堆栈信息可能会被截断,不够详细

5、generator. ??

6. 并发与并行的区别?

  • 并发宏观 上同一时间段内处理多个任务 ,这些任务交替执行 (强调任务在‌时间段‌上的重叠)。
  • 并行微观 上同一时刻执行多个任务 ,这些任务真正同时进行‌ (强调任务在‌时刻‌上的同步)。

7. setTimeout、setInterval、requestAnimationFrame 各有什么特点?

  • setTimeout:指定延迟后‌执行一次‌回调函数。适用场景‌:一次性延迟任务(如倒计时、防抖、延迟加载)
  • setInterval每隔固定时间间隔重复执行‌回调函数。适用场景‌:周期性轮询、定时更新(需手动取消以避免内存泄漏)
  • requestAnimationFrame专为动画优化‌,在浏览器下一次重绘前执行回调。适用场景‌:所有视觉动画(Canvas、CSS 变换、游戏、滚动效果)
javascript 复制代码
// setTimeout
setTimeout(() => console.log("1秒后执行"), 1000);
//setInterval
const id = setInterval(() => console.log("每秒执行"), 1000);
clearInterval(id); // 手动停止
//requestAnimationFrame
function animate(timestamp) {
  // 动画逻辑
  requestAnimationFrame(animate); // 递归调用保持循环
}
requestAnimationFrame(animate);

8、事件循环 EventLoop ?

四、JavaScript基础

1. new操作符的实现原理

  • 创建新对象: {}
  • 设置原型链,将对象的原型设置为constructor构造函数的 prototype 对象:
  • 让函数的 this 指向这个对象,执行构造函数的代码: 为这个新对象添加属性
  • 返回新对象
javascript 复制代码
function createObject(constructor, ...args) {
    const obj = {}; // 步骤1: 创建新对象
    obj.__proto__ = constructor.prototype; // 步骤2: 设置原型链
    const result = constructor.apply(obj, args); // 步骤3: 绑定this并执行构造函数
    return (typeof result === 'object' && result !== null) ? result : obj; // 步骤5: 返回结果
}

2. js有哪些内置对象

总结: 常用的有对象、数组、函数、字符串、Date、JSON、正则、Promise等;

  • Object‌:所有对象的基类,提供如 keys()、values()、assign() 等方法。
  • Function‌:函数构造器,支持 .call()、.apply()、.bind() 等方法。
  • Array‌:用于处理有序集合,提供 push()、pop()、map()、filter() 等丰富方法。
  • String‌:字符串操作,如 substring()、split()、trim()、includes()。
  • Number‌:数值处理,包括 toFixed()、toPrecision()、isInteger()、isNaN()。
  • JSON‌:用于 JSON 序列化与反序列化,如 stringify() 和 parse()。
  • Date‌:处理日期和时间,支持创建、格式化、时间戳计算等。
  • Map‌:键值对集合,键可为任意类型。
  • Set‌:存储唯一值的集合。
  • Error‌ 及其子类型:如 TypeError、ReferenceError、SyntaxError、RangeError 等,用于异常处理。
  • RegExp‌:用于模式匹配和文本搜索,支持字面量 /.../ 或构造函数方式。
  • Promise‌:异步操作处理(ES6 引入)。
  • Proxy‌ 和 ‌Reflect‌:用于元编程,拦截对象操作(ES6 引入)。

3. 对JSON的理解

JSON的数据结构 主要由两种形式组成:对象(Object)和数组(Array)对象 是一个无序的键值对集合 ,用花括号{}表示;数组 是一个有序的值的集合 ,用方括号[]表示。
两个原生方法实现:

  • JSON.stringify():将 js 对象或值转换为 JSON 字符串。
  • JSON.parse():将 JSON 字符串解析为 js 对象或值。
javascript 复制代码
const obj = { name: "Alice", age: 25 };
const jsonString = JSON.stringify(obj);
console.log(jsonString); // '{"name":"Alice","age":25}'
const obj = JSON.parse(jsonString);
console.log(obj.name); // "Alice"

4. js脚本<script>延迟加载的方式有哪些?

  • defer 属性: 脚本在‌文档解析完成后 、DOMContentLoaded 事件触发前‌按顺序执行<script defer src="script.js">(按顺序执行‌且操作 DOM )
  • async 属性: 脚本‌异步下载,下载完成后立即执行‌,可能中断 HTML 解析<script async src="script.js">(完全独立‌且需尽快执行)
  • 动态创建 DOM 方式: 通过 js 动态创建 script 元素并插入 DOM,实现‌按需加载‌。
  • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件
  • <script> 放在 HTML 底部‌:js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

5. js 类数组对象的定义?

类数组: 具有一个 length 属性和若干索引属性任意对象 。这样的对象可以用数字索引来访问其元素 ,但它们不是真正的数组。

javascript 复制代码
const arrayLike = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
};

5.1.为什么函数的 arguments 参数是类数组而不是数组?

历史原因‌ :在早期js版本 中,arguments 被设计为类数组对象 ,主要是为了性能考虑减少内存占用和提高访问速度

兼容性‌早期的浏览器和js引擎不支持完整的数组方法 ,因此提供一个类似数组的对象可以保证基本的兼容性

5.2.如何转化为数组?

  • Array.prototype.slice
  • Array.from
  • 使用扩展运算符:仅适用于可迭代对象
  • Array.prototype.map:如果需要映射每个元素
javascript 复制代码
const trueArray = Array.prototype.slice.call(arrayLike);
const trueArray = Array.from(arrayLike);
const trueArray = [...arrayLike];
const trueArray = Array.prototype.map.call(arrayLike, (item, index) => item);

5.3.如何遍历类数组?

转成数组后再遍历

6. Unicode、UTF-8、UTF-16、UTF-32的区别?

Unicode 是字符集,定义了"有哪些字符";UTF-8、UTF-16、UTF-32 是编码方式,定义了"如何用字节存储这些字符"‌

假设:Unicode 是字典,UTF-8/16/32 是三种不同的"抄写方式"‌ ------ 你选哪种,取决于你用的是纸笔(UTF-8)、钢笔(UTF-16)还是金笔(UTF-32)。

7. 常见的位运算符有哪些?其计算规则是什么?

常见的位运算符共有 ‌6 种‌:按位与&、按位或|(表格写不出来)、按位异或^、按位取反~、左移<<和右移>>

运算符 符号 计算规则 示例(8位二进制) 结果(十进制)
按位与 & 两数对应位均为1时,结果为1,否则为0 12 & 10 → 1100 & 1010 8
按位或 两数对应位任一为1,结果为1 `12
按位异或 ^ 两数对应位不同时为1,相同时为0 12 ^ 10 → 1100 ^ 1010 6
按位取反 ~ 所有位取反(0变1,1变0) ~12 → ~00001100 -13(补码表示)
左移 << 所有位左移n位,右侧补0 12 << 2 → 1100 << 2 48
右移 >> 所有位右移n位,左侧补符号位(有符号数) 12 >> 2 → 1100 >> 2 3

8. 什么是 DOM 和 BOM?

  • DOM(文档对象模型) :由W3C制定标准 ,它将网页文档解析为树形结构(节点树) ,使js能够动态访问和修改页面中的元素、内容、结构和样式。‌
    • 顶层对象: document(隶属于window对象)。‌‌
    • 主要功能: 获取、修改、添加或删除页面元素;事件
  • BOM(浏览器对象模型)‌:由浏览器厂商提供。它通过一系列对象(核心是window对象)让js能够控制浏览器窗口、导航、屏幕信息等与网页内容无关的功能。‌‌
    • 顶层对象: window
    • 主要对象与功能 ‌:
      • location:控制URL跳转与刷新。‌‌
      • history:操作浏览器历史记录。‌‌
      • navigator:获取浏览器信息。‌‌
      • screen:获取屏幕尺寸。‌‌
      • alert/confirm:弹出对话框

9. escape、encodeURI、encodeURIComponent 的区别

escape是对字符串(string)进行编码

encodeURI、encodeURIComponent 对URL编码

10. JavaScript为什么要进行变量提升,它导致了什么问题?

总结
变量提升 是JavaScript在代码执行之前对变量和函数声明进行预处理的机制 。它提高了性能和容错性

但也可能导致在变量声明之前使用变量时出现undefined 的情况。

为避免潜在问题,建议在代码中始终先声明变量再使用 ,并使用ES6的let和const来避免变量提升带来的一些隐患。

11. 什么是尾调用,使用尾调用有什么好处?

尾调用 是指一个函数最后一步操作调用另一个函数 。它可以减少内存消耗,提高性能,并且可以优化 编译器

12. use strict是什么意思 ? 使用它区别是什么?

"use strict" :指定代码在严格条件下执行;
目的:

  • 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
  • 消除代码运行的不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫

区别

  • 禁止使用 with 语句。
  • 禁止 this 关键字指向全局对象。
  • 对象不能有重名的属性

13. 如何判断一个对象是否属于某个类?

总结: instanceof运算符、通过constructor属性Object.prototype.toString()instanceof和constructor属性提供了更准确的判断 ,而toString()主要用于获取对象类型,不适用于自定义类。

14. 对AJAX的理解,实现一个AJAX请求

AJAX是一种用于在网页上进行异步数据交互 的技术。它允许网页通过后台与服务器 进行数据交换 ,而不需要刷新整个页面。
Ajax的原理 简单来说通过XmlHttpRequest对象向服务器发异步请求 ,从服务器获得数据 ,然后用JavaScript来操作DOM而更新页面

实现一个基本的AJAX请求可以分为以下几个步骤

  • 创建一个XMLHttpRequest对象 :使用new XMLHttpRequest()创建一个新的XMLHttpRequest对象,该对象用于发送HTTP请求
  • 设置请求参数和回调函数 :使用open()方法设置请求的类型(GET或POST)和URL 。如果有需要,还可以设置其他的请求参数,例如请求头信息等。然后,使用onreadystatechange属性指定一个回调函数 ,该函数将在请求状态发生变化时被调用
  • 发送请求 :使用send()方法发送请求 。对于GET 请求,可以将请求参数附加到URL 中;对于POST 请求,可以将请求参数作为send()方法的参数传递。
  • 处理响应 :在回调函数中,通过检查readyState和status属性来确定请求的状态readyState表示请求的当前状态 ,其中4表示请求已完成并且响应已就绪,status表示服务器返回的HTTP状态码 ,200表示请求成功。可以使用responseText或responseXML属性获取服务器返回的数据。

readyState表示请求的当前状态,包括0(未初始化)、1(已打开)、2(已发送)、3(接收中)和4(已完成)。

javascript 复制代码
// 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest()
// 指定请求的方法、URL和是否为异步请求
xhr.open('POST', 'https://api.example.com/data', true)
// 设置接收到响应时的处理函数
xhr.onload = function () {
  if (xhr.status === 200) {
    // 在这里处理从服务器返回的数据
    var response = JSON.parse(xhr.responseText)
    console.log(response)
  } else {
    console.error('请求失败:' + xhr.status)
  }
}
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json')
// 发送请求到服务器
var data = {
  username: 'John',
  password: 'secret'
}
xhr.send(JSON.stringify(data))

15. ajax、axios、fetch的区别

  • Ajax: 是一种较为早期的技术,基于 XMLHttpRequest 对象发起 HTTP 请求,提供了很大的灵活性,但也带来了复杂性。
  • Fetch: 作为现代 API,它基于 Promise ,提供更简洁和易于理解的接口,使得处理异步请求 变得更加方便。不过在错误处理上有些不足。
  • Axios: 基于 Promise 的 HTTP 客户端,适用于 浏览器 和 Node.js。它封装了 XMLHttpRequest 和 fetch API,提供了简单的 API 进行网络请求。尤其适合需要处理大量 HTTP 请求的项目,但需要依赖库的额外引入。

五、原型与原型链 ??

41. 对原型、原型链的理解

总结:

  • 原型: 每个函数在创建时 都会自动生成一个 prototype 属性 ,它指向一个对象函数.prototype得到的这个对象 ),这个对象称为原型对象 。通过这个原型对象,所有由该函数构造出来的实例对象可以共享方法和属性
  • 原型链:

原型详解

javascript 复制代码
 var a=function (){}
 console.log(a.prototype) 

打印结果如下:从结果来看,函数.prototype得到的是一个对象,包含了constructor和_proto_。有constructor说明是一个构造函数。

构造函数创建实例时,把通用方法写到 prototype 上,让所有实例共享。

javascript 复制代码
function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  return 'Hi, I am ' + this.name;
};
const p1 = new Person('Tom');
const p2 = new Person('Jerry');
console.log(p1.sayHi === p2.sayHi); // true,共享方法

42. 原型修改、重写

43. 原型链指向

44. 原型链的终点是什么?如何打印出原型链的终点?

45. 如何获得对象非原型链上的属性?

六、执行上下文/作用域链/闭包

1. 对闭包的理解

闭包:当一个函数内部定义了另一个函数,并且内部函数引用了外部函数的变量时,就形成了闭包 。这些变量不会因为外部函数执行结束而被垃圾回收机制回收,而是被保留下来。

闭包的原理: "从内存角度 看,正常情况下函数执行完毕后,其执行上下文会被销毁,局部变量会被垃圾回收。闭包 会让被引用的变量保留在内存 中,因为内部函数持有对这些变量的引用。"

闭包的形成条件:

  • 必须有外层函数和内层函数的嵌套关系
  • 内层函数必须访问外层函数作用域中的变量
  • 内层函数被外部使用,也就是说内层函数必须以某种方式被保留(返回、赋值、传递等)

闭包的核心使用场景

  • 数据私有化(模拟私有变量) 外层函数实例拿不到它定义的变量counter.count 结果是 undefined
  • 函数工厂(柯里化)
  • 防抖与节流
  • 循环中的异步问题
javascript 复制代码
function createCounter() {
    let count = 0; // 这个变量本应在函数执行完后被销毁
    let inner =  function() {
        count++; // 但闭包让它"活"了下来
        return count;
    }
    inner()    //只是内部调用,没有返回inner,不形成闭包
    return inner     //内层函数被返回到外部,形成闭包
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

使用场景一、数据封装和私有变量

javascript 复制代码
function createCounter() {
    let count = 0; // 私有变量
    return {
        increment: function() { count++; },
        getCount: function() { return count; }
    };
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined (无法直接访问)

使用场景二、函数工厂(柯里化)

javascript 复制代码
function makeAdder(x) {
    return function(y) {
        return x + y;
    };
}
const add5 = makeAdder(5);
console.log(add5(2));  // 7

使用场景三、循环中的异步问题

javascript 复制代码
// ❌ 错误示例
for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i); // 输出 5 个 5
    }, 1000);
}
 
// ✅ 使用闭包解决
for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(() => {
            console.log(j); // 输出 0, 1, 2, 3, 4
        }, 1000);
    })(i);
}
//  ✅ 使用let解决
for (let i = 1; i <= 5; i++) {
    setTimeout(function() {
        console.log(i); // 1,2,3,4,5
    }, 1000);
}

1.1 .使用场景四、防抖与节流

javascript 复制代码
// 防抖
function debounce(fn, delay) {
    let timer = null; // 闭包保存 timer
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}
// 使用
const handleInput = debounce(() => {
    console.log("搜索请求");
}, 500);

2. 对作用域、作用域链的理解

3. 对执行上下文的理解

七、this/call/apply/bind

1. 对this对象的理解

2. call() 和 apply() 的区别?

3. 实现call、apply 及 bind 函数

连续多个 bind,最后this指向是什么?

八、面向对象

1. 对象创建的方式有哪些?

2. 对象继承的方式有哪些?

九、垃圾回收与内存泄漏

1. 浏览器的垃圾回收机制

2. 哪些情况会导致内存泄漏

相关推荐
学以智用2 小时前
Vue3 状态管理库 Pinia 完整教程
前端·vue.js
这是个栗子2 小时前
前端开发中的常用工具函数(五)
javascript·数据结构·reduce
两万五千个小时2 小时前
学习 Pi Coding Agent:系统提示词与工具设计深度解析
javascript·人工智能·架构
harrain2 小时前
vue3多个watch监听统一等待触发再执行后续逻辑的处理方案
前端·javascript·vue.js
miss2 小时前
Vue3 + AI Agent 前端开发实战:一个 前端开发工程师的转型记录
前端
miss2 小时前
AI Agent 前端开发:一个初级工程师的踩坑成长之路
前端
清水寺小和尚2 小时前
如何用400行代码构建OpenClaw
前端
锦木烁光2 小时前
Flowable 实战:从架构解耦到多状态动态查询的高性能重构方案
前端·后端
子淼8122 小时前
HTML入门指南:构建网页的基石
前端·html