前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)

前言

其实很多人都不知道怎么去梳理自己得知识体系,而且不太记得住,想要记住,好像是多刷题目,通过做题得这种方式,让自己进行记忆,所以我会总体得看一遍书,然后去刷一些问题,通过问题得方式形成记忆,抓住重点,看看自己是否记得住。同时,这也是常问得面试问题吧。本来之前就想整理得,只是入职新公司,要时间去适应公司。害,分几篇分享。

js

ES6+ 新语法

  1. ES6(2015)
    • 变量:let/const、块级作用域
    • 函数:箭头函数、默认参数、剩余 / 扩展参数
    • 字符串:模板字符串(```+ ${}
    • 数据:数组 / 对象解构、Map/Set
    • 面向对象:class 类与继承
    • 模块化:import/export
    • 异步:Promise
    • 其他:for...ofSymbol、生成器(function*
  2. ES7(2016)
    • 数组:includes()
    • 运算符:指数运算符(**
  3. ES2017(ES8)
    • 异步:async/await
    • 对象:Object.values()/entries()
    • 字符串:padStart()/padEnd()
  4. ES2018(ES9)
    • 对象:扩展运算符(... 用于对象)
    • 异步迭代:for await...of
    • 正则:命名捕获组、反向断言
  5. ES2019(ES10)
    • 数组:flat()/flatMap()(数组扁平化)
    • 字符串:trimStart()/trimEnd()
    • 对象:fromEntries()(将键值对转为对象)
  6. ES2020(ES11)
    • 数据类型:BigInt(大整数)
    • 运算符:可选链(?.)、空值合并(??
    • 模块化:动态 import()
    • 字符串:matchAll()
  7. ES2021(ES12)
    • 逻辑赋值:&&=/||=/??=
    • 数字:分隔符(1_000_000
    • 数组:at()(支持负索引)
  8. ES2022(ES13)
    • 类:私有属性(# 前缀)、静态类字段
    • 数组:findLast()/findLastIndex()
    • Top-level await(模块顶层使用 await
  9. 后续版本(2023+)
    • 新增 ArrayBuffer 扩展、Object 方法增强等小特性,以场景化优化为主。

整体趋势:ES6 奠定现代语法基础,后续版本逐年迭代,聚焦解决实际开发痛点(如异步简化、安全访问、代码可读性)。

?.与?? 的区别

在 JavaScript(及 TypeScript)中,?.(可选链运算符)和 ??(空值合并运算符)是两个不同用途的语法糖,核心区别在于它们解决的问题和使用场景不同。以下是详细对比:

1. ?.(可选链运算符):安全访问嵌套属性 / 方法

作用 :用于安全地访问对象的嵌套属性、数组元素或调用方法,避免因中间值为 nullundefined 而抛出 Cannot read property 'x' of undefined 之类的错误。 逻辑 :如果运算符左侧的值(对象 / 数组)为 nullundefined,则整个表达式直接返回 undefined,不再继续访问右侧的属性 / 方法。

示例

javascript 复制代码
const user = {
  name: "Alice",
  address: { city: "Beijing" }
};

// 正常访问(无错误)
console.log(user.address.city); // "Beijing"

// 假设 user.address 可能不存在
const user2 = { name: "Bob" };
// 传统方式:需要手动判断,否则报错
console.log(user2.address && user2.address.city); // undefined(无错误)
// 可选链方式:更简洁
console.log(user2.address?.city); // undefined(无错误)


// 访问数组元素
const arr = [1, 2, 3];
console.log(arr[0]); // 1
const arr2 = null;
console.log(arr2?.[0]); // undefined(无错误)


// 调用方法(如果方法不存在,不会报错)
const utils = {
  format: (str) => str.toUpperCase()
};
console.log(utils.format?.("hello")); // "HELLO"
const utils2 = null;
console.log(utils2.format?.("hello")); // undefined(无错误)

2. ??(空值合并运算符):设置默认值

作用 :当左侧操作数为 nullundefined 时,返回右侧的默认值;否则返回左侧操作数。 核心特点 :仅对 nullundefined 生效,对其他 "假值"(如 0''false)不生效(这是它与 || 运算符的关键区别)。

示例

javascript 复制代码
// 左侧为 null/undefined 时,返回右侧
const name = null ?? "Guest"; // "Guest"
const age = undefined ?? 18; // 18

// 左侧为其他"假值"时,返回左侧(与 || 不同)
const score = 0 ?? 60; // 0(若用 || 会返回 60)
const emptyStr = "" ?? "default"; // ""(若用 || 会返回 "default")

核心区别总结

维度 ?.(可选链运算符) ??(空值合并运算符)
用途 安全访问嵌套属性 / 数组 / 方法,避免报错 null/undefined 设置默认值
操作对象 左侧是可能为 null/undefined 的值 左侧是待判断的值,右侧是默认值
返回结果 若左侧有效则返回属性值,否则 undefined 若左侧是 null/undefined 则返回右侧,否则返回左侧
典型场景 处理不确定存在的嵌套数据(如接口返回) 为变量设置默认值(排除 0/'' 等有效假值)

常见组合使用

两者经常结合使用,例如先通过 ?. 安全访问属性,再通过 ?? 设置默认值

for in 与for of得区别

在 JavaScript 中,for...infor...of 是两种不同的循环语法,核心区别在于遍历目标、适用场景和遍历结果,具体如下:

1. 遍历目标不同

  • for...in :用于遍历对象的可枚举属性(包括自身属性和继承的属性) ,本质是遍历 "键名"。 可枚举属性指的是那些 enumerable 标志为 true 的属性(如对象自身定义的属性、数组的索引等,默认情况下大部分原生属性不可枚举)。
  • for...of :用于遍历可迭代对象(Iterable Object)的元素值 ,本质是遍历 "值"。 可迭代对象是指实现了 [Symbol.iterator] 接口的对象,包括:数组、字符串、MapSetarguments 对象、NodeList(DOM 节点集合)等。

2. 遍历结果不同

  • for...in 遍历的是 "键名"
    • 遍历对象时,得到的是对象的属性名(字符串类型);
    • 遍历数组时,得到的是数组的索引(字符串类型,而非数字);
    • 遍历字符串时,得到的是字符的索引(字符串类型)。
  • for...of 遍历的是 "值"
    • 遍历数组时,得到的是数组的元素值
    • 遍历字符串时,得到的是单个字符
    • 遍历 Map 时,得到的是 [key, value] 数组;
    • 遍历 Set 时,得到的是集合中的元素

3. 适用场景不同

  • for...in 适合遍历 "对象的属性": 主要用于检查对象是否包含某个属性,或遍历对象的键名(需注意过滤继承属性)。 不推荐用于遍历数组(因为可能遍历到非数字索引的属性,且索引是字符串类型)。
  • for...of 适合遍历 "可迭代对象的元素" : 主要用于获取数组、字符串、MapSet 等集合中的元素值,更符合 "遍历数据" 的直观需求。

4. 对继承属性的处理不同

  • for...in 会遍历继承的可枚举属性 : 例如,若在 Object.prototype 上添加了自定义属性,for...in 会遍历到这些属性,可能导致意外结果。因此需要用 hasOwnProperty() 过滤自身属性。
  • for...of 只遍历自身元素: 不会涉及原型链上的属性,无需额外过滤。

示例对比

示例 1:遍历数组

javascript 复制代码
const arr = [10, 20, 30];

// for...in:遍历索引(字符串类型)
for (const key in arr) {
  console.log(key, typeof key); // 0 string, 1 string, 2 string
}

// for...of:遍历元素值
for (const value of arr) {
  console.log(value); // 10, 20, 30
}

示例 2:遍历对象

javascript 复制代码
const obj = { name: "foo", age: 18 };

// for...in:遍历属性名(需注意过滤继承属性)
for (const key in obj) {
  // 过滤继承的属性(如 toString 等)
  if (obj.hasOwnProperty(key)) {
    console.log(key, obj[key]); // name foo, age 18
  }
}

// for...of:不能直接遍历普通对象(普通对象不可迭代)
for (const value of obj) { 
  console.log(value); // 报错:obj is not iterable
}

示例 3:遍历字符串

javascript 复制代码
const str = "abc";

// for...in:遍历索引(字符串类型)
for (const key in str) {
  console.log(key, str[key]); // 0 a, 1 b, 2 c
}

// for...of:遍历字符
for (const char of str) {
  console.log(char); // a, b, c
}

示例 4:遍历 Map

javascript 复制代码
const map = new Map();
map.set("name", "bar");
map.set("age", 20);

// for...in:遍历 Map 的属性(非键值对,无意义)
for (const key in map) {
  console.log(key); // 输出 Map 的内置属性(如 size),而非键值对
}

// for...of:遍历 [key, value] 数组
for (const [key, value] of map) {
  console.log(key, value); // name bar, age 20
}

总结对比表

特性 for...in for...of
遍历目标 对象的可枚举属性(键名) 可迭代对象的元素(值)
遍历结果 键名(字符串类型) 元素值
适用对象 所有对象(尤其是普通对象) 可迭代对象(数组、字符串、Map 等)
继承属性处理 会遍历继承的可枚举属性(需过滤) 只遍历自身元素,不涉及原型链
典型用途 检查对象属性、遍历对象键名 获取集合元素值、遍历数据

简单说:for...in 是 "遍历键名的工具",for...of 是 "遍历值的工具" 。实际开发中,遍历对象属性用 for...in(记得过滤继承属性),遍历数组、字符串等集合的元素用 for...of

js基础数据类型

暂时性死锁

symbol使用场景

  1. 作为对象的唯一属性键,避免属性名冲突
  2. 定义对象的 "私有" 属性(模拟私有成员)
  3. 定义常量集合(避免魔法字符串)
  4. 扩展内置对象的方法
  5. 定义迭代器接口(Symbol.iterator

迭代器与生成器

www.yuque.com/ergz/web/qe...

对象、类与面向对象编程

www.yuque.com/ergz/web/oz...

期约与异步函数

www.yuque.com/ergz/web/cx...

函 数

www.yuque.com/ergz/web/vp...

代理与反射

www.yuque.com/ergz/web/xp...

this全面讲解

www.yuque.com/ergz/web/gn...

js问题

如何获取url?a='11123'

javascript 复制代码
// 1. 获取 URL 中的查询部分(即 "?a='11123'&b=456")
const queryString = window.location.search;

// 2. 解析查询字符串
const params = new URLSearchParams(queryString);

// 3. 获取参数 a 的值
const aValue = params.get('a');

proxy如何做数据得拦截

Proxy 通过陷阱函数实现对数据的拦截,核心步骤是:

  1. 创建代理对象(new Proxy(target, handler));
  2. handler 中定义需要拦截的操作(如 getset 等陷阱);
  3. 通过代理对象操作数据时,自动触发对应的陷阱函数,执行自定义逻辑。

这种机制广泛用于数据验证、日志记录、响应式系统(如 Vue 3 的响应式原理)等场景,相比 Object.defineProperty 更强大、更灵活。

js 的继承是怎么做的?

JavaScript 的继承机制与传统面向对象语言(如 Java)不同,它基于原型链(Prototype Chain) 实现,而非类的直接继承。随着语言发展,ES6 引入了 classextends 语法糖,简化了继承实现,但底层仍依赖原型链。以下是 JavaScript 中常见的继承方式及原理:

一、原型链继承(最基础的继承方式)

核心思想 :通过让子类的原型对象(prototype)指向父类的实例,形成原型链,从而继承父类的属性和方法。

实现示例:

javascript 复制代码
// 父类:动物
function Animal(name) {
  this.name = name; // 实例属性
  this.features = ['呼吸', '繁殖']; // 引用类型属性
}

// 父类原型方法
Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`);
};

// 子类:狗
function Dog() {}

// 关键:让子类原型指向父类实例,形成原型链
Dog.prototype = new Animal(); 
// 修复子类构造函数指向(否则 Dog 实例的 constructor 会指向 Animal)
Dog.prototype.constructor = Dog;

// 子类实例
const dog = new Dog();
dog.name = '旺财'; 
dog.eat(); // 输出:"旺财 在吃东西"(继承父类原型方法)
console.log(dog.features); // 输出:['呼吸', '繁殖'](继承父类实例属性)

缺点:

  1. 子类实例会共享父类的引用类型属性 (如 features),一个实例修改会影响其他实例。
  2. 无法在创建子类实例时向父类构造函数传递参数(如 Animalname 参数)。

二、构造函数继承(解决原型链的传参和共享问题)

核心思想 :在子类构造函数中通过 call/apply 调用父类构造函数,强制绑定 this,从而继承父类的实例属性。

实现示例:

javascript 复制代码
// 父类
function Animal(name) {
  this.name = name;
  this.features = ['呼吸', '繁殖'];
}

Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`);
};

// 子类
function Dog(name) {
  // 关键:调用父类构造函数,传递参数
  Animal.call(this, name); 
}

// 子类实例
const dog1 = new Dog('旺财');
const dog2 = new Dog('小白');

dog1.features.push('汪汪叫'); 
console.log(dog1.features); // ['呼吸', '繁殖', '汪汪叫']
console.log(dog2.features); // ['呼吸', '繁殖'](不共享,解决了引用类型共享问题)

dog1.eat(); // 报错!无法继承父类原型方法(构造函数继承只继承实例属性)

缺点:

  • 只能继承父类的实例属性和方法 ,无法继承父类原型上的方法(如 eat),导致方法无法复用(每个实例都需单独定义)。

三、组合继承(原型链 + 构造函数,主流方案)

核心思想:结合原型链继承(继承原型方法)和构造函数继承(继承实例属性),取长补短。

实现示例:

javascript 复制代码
// 父类
function Animal(name) {
  this.name = name;
  this.features = ['呼吸', '繁殖'];
}

Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`);
};

// 子类
function Dog(name) {
  // 1. 构造函数继承:继承实例属性,传递参数
  Animal.call(this, name); 
}

// 2. 原型链继承:继承原型方法
Dog.prototype = new Animal(); 
Dog.prototype.constructor = Dog;

// 子类可添加自己的原型方法
Dog.prototype.bark = function() {
  console.log(`${this.name} 在汪汪叫`);
};

// 测试
const dog = new Dog('旺财');
dog.eat(); // "旺财 在吃东西"(继承原型方法)
dog.bark(); // "旺财 在汪汪叫"(子类自有方法)
console.log(dog.features); // ['呼吸', '繁殖'](继承实例属性)

优点:

  • 既继承了父类的实例属性(不共享),又继承了原型方法(可复用),还能向父类传参。

缺点:

  • 父类构造函数会被调用两次 :一次是 new Animal() 创建子类原型时,一次是 Animal.call(this) 时,造成性能浪费。

四、寄生组合继承(优化组合继承的缺陷)

核心思想 :通过 Object.create 复制父类原型作为子类原型,避免父类构造函数被调用两次,是目前最理想的继承方式。

实现示例:

javascript 复制代码
// 父类
function Animal(name) {
  this.name = name;
  this.features = ['呼吸', '繁殖'];
}

Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`);
};

// 子类
function Dog(name) {
  Animal.call(this, name); // 只调用一次父类构造函数
}

// 关键:复制父类原型作为子类原型(不调用父类构造函数)
Dog.prototype = Object.create(Animal.prototype); 
Dog.prototype.constructor = Dog; // 修复构造函数指向

// 测试
const dog = new Dog('旺财');
dog.eat(); // "旺财 在吃东西"(继承原型方法)
console.log(dog instanceof Animal); // true(正确的原型链关系)

优点:

  • 完美解决组合继承的缺陷(父类构造函数只调用一次)。
  • 保留了原型链继承和构造函数继承的所有优点。 (ES6 的 class extends 本质就是这种方式的语法糖)

五、ES6 class 继承(语法糖,推荐使用)

ES6 引入 classextends 关键字,简化了继承写法,底层仍基于原型链,但更贴近传统面向对象的语法。

实现示例:

javascript 复制代码
// 父类
class Animal {
  // 构造函数(对应 ES5 的构造函数)
  constructor(name) {
    this.name = name;
    this.features = ['呼吸', '繁殖'];
  }

  // 原型方法(自动挂载到 Animal.prototype)
  eat() {
    console.log(`${this.name} 在吃东西`);
  }

  // 静态方法(不会被实例继承,只能通过类调用)
  static isAnimal(obj) {
    return obj instanceof Animal;
  }
}

// 子类:通过 extends 继承父类
class Dog extends Animal {
  constructor(name) {
    // 必须先调用 super(),相当于调用父类构造函数
    super(name); 
  }

  // 子类自有方法
  bark() {
    console.log(`${this.name} 在汪汪叫`);
  }

  // 重写父类方法
  eat() {
    console.log(`${this.name} 爱吃肉`);
  }
}

// 测试
const dog = new Dog('旺财');
dog.eat(); // "旺财 爱吃肉"(重写父类方法)
dog.bark(); // "旺财 在汪汪叫"(子类方法)
console.log(Animal.isAnimal(dog)); // true(调用父类静态方法)

特点:

  • extends 对应原型链继承,super() 对应构造函数继承中的 call
  • 支持重写父类方法(多态)。
  • 静态方法(static)会被子类继承(通过 类名.方法 调用)。

六、其他继承方式(了解即可)

  1. 原型式继承 :通过 Object.create 直接创建一个基于现有对象的新对象(适合简单对象的继承)。

    javascript 复制代码
    const animal = { name: '动物', eat: () => {
        
    } };
    const dog = Object.create(animal); // dog 继承 animal 的属性和方法
  2. 寄生式继承:在原型式继承的基础上,增强新对象(添加属性 / 方法)。

    javascript 复制代码
    function createDog(animal) {
      const dog = Object.create(animal);
      dog.bark = () => console.log('汪汪叫'); // 增强对象
      return dog;
    }
  3. 多重继承:JavaScript 不直接支持多继承,但可通过 "混入(mixin)" 实现(复制多个对象的属性到目标对象)。

总结

JavaScript 继承的核心是原型链,所有继承方式都是围绕原型链的优化:

  • 早期通过原型链 + 构造函数组合实现继承。
  • 寄生组合继承是 ES5 中最完美的方案。
  • ES6 的 class extends 是寄生组合继承的语法糖,简化了写法,是目前的推荐方式。

理解原型链的工作原理(对象通过 __proto__ 指向原型,构造函数通过 prototype 关联原型),是掌握 JavaScript 继承的关键。

import()与require()的区别

在 JavaScript 中,import()require() 都是用于模块导入的语法,但它们属于不同的模块系统,存在多方面区别:

  1. 所属模块系统不同
  • require()CommonJS 模块系统的语法,主要用于 Node.js 环境(早期前端也通过 Webpack 等工具兼容)。
  • import(包括 import() 动态导入)是 ES6 模块系统(ESM)的语法,是 JavaScript 官方标准化的模块系统,现在浏览器和 Node.js(需配置)均支持。
  1. 加载时机不同
  • require()运行时加载 :代码执行到 require() 语句时才会加载模块,属于动态加载。
  • 静态 import(如 import xxx from 'xxx')是 编译时(解析阶段)加载:在代码执行前就会解析模块依赖,属于静态加载(无法在条件语句中使用)。
  • import() 是 ESM 中的动态导入 :虽然语法是 import(),但本质是运行时加载,返回一个 Promise,可在条件语句中使用。
  1. 加载方式与阻塞性
  • require()同步加载:会阻塞后续代码执行,直到模块加载完成。
  • 静态 import异步加载(浏览器环境):加载模块时不会阻塞页面渲染,模块依赖会并行加载。
  • import()异步加载 :返回 Promise,通过 .then()await 处理结果,完全非阻塞。
  1. 返回值性质不同
  • require() 返回的是模块导出对象的拷贝:模块内部的变化不会影响已导入的拷贝(除非导出的是引用类型,如对象 / 数组,修改其属性会生效)。

    javascript 复制代码
    // 模块 a.js
    let count = 1;
    module.exports = { count };
    
    // 主文件
    const a = require('./a');
    a.count = 2; // 仅修改拷贝,原模块的 count 仍为 1
  • import 导入的是模块导出的引用:模块内部的变化会实时反映到导入处(因为 ESM 是动态绑定)。

    javascript 复制代码
    // 模块 a.js
    export let count = 1;
    export const increment = () => { count++ };
    
    // 主文件
    import { count, increment } from './a.js';
    increment(); 
    console.log(count); // 输出 2(实时反映模块内部变化)
  1. 语法灵活性不同
  • require() 可动态生成路径,支持表达式:

    javascript 复制代码
    const path = './module-' + Math.random();
    const module = require(path); // 合法
  • 静态 import 路径必须是静态字符串(无法动态拼接):

    javascript 复制代码
    const path = './module.js';
    import { func } from path; // 报错(路径必须是字面量)
  • import() 支持动态路径(结合 ESM 的动态加载能力):

    javascript 复制代码
    const path = './module-' + Math.random();
    import(path).then(module => { /* 使用模块 */ }); // 合法
  1. 导出语法配合不同
  • require() 对应 CommonJS 的导出语法 module.exportsexports

    javascript 复制代码
    // 导出
    module.exports = { name: 'foo' };
    exports.age = 18;
    
    // 导入
    const obj = require('./module');
  • import 对应 ESM 的导出语法 exportexport default

    javascript 复制代码
    // 导出
    export const name = 'foo';
    export default { age: 18 };
    
    // 导入
    import { name }, defaultObj from './module.js';
  1. 适用场景不同
  • require():主要用于 Node.js 环境(默认使用 CommonJS),或需要动态加载且兼容旧系统的场景。
  • 静态 import:用于前端工程化项目(如 Vue/React)、现代 Node.js 项目(需配置 "type": "module"),适合静态分析和 Tree-Shaking 优化。
  • import():用于按需加载(如路由懒加载)、条件加载场景,兼顾 ESM 特性和动态性。

总结:require() 是 CommonJS 的同步动态加载,import(静态)是 ESM 的编译时静态加载,import() 是 ESM 的异步动态加载,三者在模块系统、加载机制和使用场景上有显著区别。

forEach 跟map得区别以及他们得参数

forEachmap 都是 JavaScript 数组的遍历方法,用于对数组中的每个元素执行回调函数,但它们的核心用途返回值有本质区别,参数则基本一致。

一、参数对比

两者的参数结构完全相同,都接收两个参数:

  1. 回调函数(必选) :对数组每个元素执行的函数,包含 3 个参数:
    • currentValue:当前正在处理的数组元素(必选)
    • index:当前元素的索引(可选)
    • array:调用该方法的原数组(可选)
  2. thisArg(可选) :执行回调函数时,指定 this 的指向(在回调中可通过 this 访问)。

二、核心区别

特性 forEach map
返回值 返回 undefined(无实际返回值) 返回一个新数组(由回调函数的返回值组成)
核心用途 用于 "执行操作"(如打印、修改外部变量等) 用于 "转换数组"(根据原数组生成新数组)
是否改变原数组 本身不改变,但回调中可手动修改原数组 本身不改变原数组,仅返回新数组
链式调用 不能(因返回 undefined 可以(因返回新数组,可继续调用其他数组方法)

三、示例说明

  1. forEach:执行操作,无返回值
javascript 复制代码
const arr = [1, 2, 3];
let sum = 0;

// 遍历数组,累加元素值(执行操作)
arr.forEach((item, index, array) => {
  sum += item;
  console.log(`索引${index}的值:${item}`); // 打印每个元素
});

console.log(sum); // 输出:6
console.log(arr.forEach(...)); // 输出:undefined(无返回值)
  1. map:转换数组,返回新数组
javascript 复制代码
const arr = [1, 2, 3];

// 遍历数组,返回每个元素的2倍组成的新数组(转换操作)
const newArr = arr.map((item, index) => {
  return item * 2; // 回调返回值会被放入新数组
});

console.log(newArr); // 输出:[2, 4, 6](新数组)
console.log(arr); // 输出:[1, 2, 3](原数组不变)

// 支持链式调用(因返回新数组)
const filteredArr = arr.map(item => item * 2).filter(item => item > 3);
console.log(filteredArr); // 输出:[4, 6]

四、使用建议

  • 当需要仅执行操作 (如日志打印、修改外部状态),无需生成新数组时,用 forEach

  • 当需要根据原数组生成新数组 (如数据转换、格式化)时,用 map(更符合 "函数式编程" 思想)。

  • 注意:两者都不能通过 break 中断遍历 (若需中断,可考虑 for 循环或 some/every)。

操作类型 Object.defineProperty 能否拦截 Proxy 能否拦截
读取属性(obj.prop 能(通过 get 能(get 陷阱)
赋值属性(obj.prop = x 能(通过 set 能(set 陷阱)
删除属性(delete obj.prop 不能(需额外处理) 能(deleteProperty 陷阱)
检查属性是否存在(prop in obj 不能 能(has 陷阱)
遍历对象(for...in 不能 能(ownKeys 陷阱)
调用函数(obj.fn() 不能(需单独处理函数属性) 能(apply 陷阱)
数组操作(push/pop 等) 不能(默认不触发 set 能(通过 set 陷阱拦截)
访问原型链(obj.__proto__ 不能 能(getPrototypeOf 陷阱)

简单说:

  • Object.defineProperty 只能拦截单个属性的读写,其他操作(如删除属性、数组方法调用)无法直接拦截。
  • Proxy 可以拦截对象的所有操作(共 13 种陷阱),覆盖更全面。

3. 对数组的支持

Object.defineProperty 对数组的拦截能力很弱,而 Proxy 天然支持数组操作拦截:

  • Object.defineProperty : 数组的 pushpopsplice 等方法会修改数组长度或元素,但默认不会触发 defineProperty 定义的 set 拦截(因为这些操作本质是修改数组的 length 或索引,而非直接赋值)。 若要拦截数组操作,需手动重写数组原型方法(如 Vue 2 的实现方式),非常繁琐。

  • Proxy : 数组的任何修改操作(包括 pushsplice 等)都会触发 ProxysetdeleteProperty 陷阱,无需额外处理。例如:

    javascript 复制代码
    const arr = [1, 2, 3];
    const proxyArr = new Proxy(arr, {
      set(target, prop, value) {
        console.log(`修改了属性 ${prop} 为 ${value}`);
        target[prop] = value;
        return true;
      }
    });
    proxyArr.push(4); // 会触发 set 陷阱(因为 push 会修改索引 3 和 length)

4. 对原对象的影响

  • Object.defineProperty : 直接在原对象上修改属性描述符,会改变原对象的结构。例如:

    javascript 复制代码
    const obj = {};
    Object.defineProperty(obj, 'name', {
      get() { return 'xxx'; },
      set(v) { /* ... */ }
    });
    // obj 本身被修改了,新增了 name 属性的访问器
  • Proxy : 不修改原对象,而是返回一个新的代理对象,所有操作通过代理对象进行。原对象保持不变:

    javascript 复制代码
    const obj = { name: 'xxx' };
    const proxyObj = new Proxy(obj, { /* 拦截器 */ });
    // obj 未被修改,proxyObj 是新的代理层

5. 嵌套对象的处理

  • Object.defineProperty : 只能拦截当前对象的属性,若对象包含嵌套对象(如 obj.a.b),需要手动递归 对嵌套对象的属性设置 get/set,否则无法拦截嵌套属性的操作。

  • Proxy : 可以在 get 陷阱中自动递归代理嵌套对象,实现对深层属性的拦截,更简洁:

    javascript 复制代码
    function createProxy(obj) {
      return new Proxy(obj, {
        get(target, prop) {
          const value = target[prop];
          // 若属性值是对象,递归创建代理
          if (typeof value === 'object' && value !== null) {
            return createProxy(value);
          }
          return value;
        },
        set(target, prop, value) {
          console.log(`设置 ${prop}=${value}`);
          target[prop] = value;
          return true;
        }
      });
    }
    const obj = { a: { b: 1 } };
    const proxy = createProxy(obj);
    proxy.a.b = 2; // 会触发 set 陷阱,拦截成功

6. 兼容性

  • Object.defineProperty: 支持 IE9+(IE8 及以下部分支持),兼容性较好,适合需要兼容旧浏览器的场景。
  • Proxy: 不支持 IE 浏览器,仅支持现代浏览器(Chrome 49+、Firefox 18+ 等),兼容性较差,但功能更强大。

7. 典型应用场景

  • Object.defineProperty: 因兼容性较好,早期常用于实现简单的响应式系统(如 Vue 2 的响应式原理),但需手动处理数组和嵌套对象。
  • Proxy: 因功能全面,现代框架更倾向于使用(如 Vue 3 的响应式原理、MobX 等),能更优雅地处理对象和数组的各种操作。

总结

维度 Object.defineProperty Proxy
拦截范围 仅单个属性的读写 所有对象操作(13 种陷阱)
数组支持 弱(需手动重写方法) 强(天然支持所有数组操作)
原对象影响 直接修改原对象 不修改原对象,返回代理对象
嵌套对象处理 需手动递归 可自动递归代理
兼容性 较好(IE9+) 较差(不支持 IE)
典型应用 Vue 2 响应式、简单属性拦截 Vue 3 响应式、复杂对象代理

简单说:Object.defineProperty 是 "属性级" 的拦截工具,功能有限但兼容性好;Proxy 是 "对象级" 的拦截工具,功能强大但兼容性较差,是更现代的解决方案。

map跟Oject得区别

简单说:Object 是 "传统键值对容器",适合简单场景;Map 是 "更现代的集合类型",在灵活性、性能和功能上更优,尤其适合复杂的键值对管理。

对比维度 Object Map
键的类型 只能是字符串(String)或 Symbol,非字符串键会被自动转为字符串 支持任意类型(基本类型、对象、函数等),键不会被转换
键的顺序 顺序复杂:数字键按数值排序,字符串键按插入顺序,Symbol 键最后 严格按照插入顺序保存,迭代时也按插入顺序返回
大小获取 无内置属性,需通过 Object.keys(obj).length 计算 size 属性,直接返回键值对数量
迭代方式 本身不可迭代,需通过 Object.keys()/Object.entries() 转换后迭代 本身是可迭代对象,支持直接迭代,提供 keys()/values()/entries() 方法
原型链影响 继承原型链属性(如 toString),可能导致键冲突,需用 hasOwnProperty 检查 无原型链干扰,所有键均为自身属性,无默认属性冲突
增删改查操作 通过点语法 / 方括号(obj.key = val),删除用 delete,检查用 in 通过专门方法:set()/get()/delete()/has(),清空用 clear()
性能 适合静态数据,频繁增删属性时性能较差 优化了动态增删操作,大量键值对且频繁变化时性能更优
序列化 可直接用 JSON.stringify() 序列化(会丢失部分类型) 不能直接序列化,需手动转为数组 / 对象后处理
适用场景 静态配置、简单数据结构、需 JSON 序列化的场景 键为非字符串类型、需保证插入顺序、频繁增删或遍历的场景

map跟set得区别

在 JavaScript 中,MapSet 都是 ES6 引入的集合类型 ,用于存储数据,但它们的存储结构核心用途有本质区别。以下通过对比表和示例详细说明:

对比维度 Map Set
存储内容 键值对(key-value),类似 "字典" 唯一的值(value),类似 "无重复元素的列表"
元素唯一性 键(key)唯一,相同键会覆盖旧值 值(value)唯一,重复值会被忽略
主要方法 set(key, value):添加 / 修改键值对 get(key):获取键对应的值 has(key):检查键是否存在 delete(key):删除键值对 clear():清空所有键值对 add(value):添加值(重复值无效) has(value):检查值是否存在 delete(value):删除值 clear():清空所有值
迭代内容 可迭代键(keys())、值(values())、键值对(entries(),默认) 可迭代值(values(),默认)、entries()(返回 [value, value] 数组)
使用场景 需要存储 "键 - 值关联" 数据(如字典、缓存、映射关系) 需要存储 "唯一值"(如去重、集合运算、检查存在性)

== 做了怎样得转换

转换流程总结

== 的比较步骤可简化为:

  1. 若两边类型相同,直接比较值(特殊处理 NaN、±0);
  2. 若类型不同,根据 "类型对"(如数字 vs 字符串、布尔 vs 数字等)执行对应转换;
  3. 转换为相同类型后,再次比较值是否相等。

注意

== 的转换规则复杂且容易出现反直觉结果(如 "" == 0 → true[] == false → true),因此实际开发中更推荐使用 ===(严格相等运算符) ,它不进行类型转换,仅当类型和值都相同时才返回 true,避免潜在的逻辑错误。

原生js能发起请求的有哪些API

记忆方法:XHR(API设计繁琐,webworker不支持),fetch(需要手动处理错误),navigator.sendBeacon(页面卸载上报,只能发起post请求),websocket,SSE.

在原生 JavaScript 中,用于发起网络请求的 API 主要有以下几种,它们适用于不同的场景(如 HTTP 请求、实时通信、后台数据上报等):

  1. XMLHttpRequest(XHR)

最传统的网络请求 API,几乎所有浏览器都支持,可用于发送各种类型的 HTTP 请求(GET、POST 等)。 特点

  • 支持同步 / 异步请求(但同步请求会阻塞线程,不推荐);
  • 可监控请求进度(如上传 / 下载进度);
  • 兼容性极佳(包括 IE6+),但 API 设计较繁琐。

基本用法示例

javascript 复制代码
// 创建 XHR 实例
const xhr = new XMLHttpRequest();

// 配置请求(方法、URL、是否异步)
xhr.open('GET', 'https://api.example.com/data', true);

// 设置请求头(可选)
xhr.setRequestHeader('Content-Type', 'application/json');

// 监听请求状态变化
xhr.onreadystatechange = function() {
  // readyState 4 表示请求完成,status 200 表示成功
  if (xhr.readyState === 4 && xhr.status === 200) {
    const response = JSON.parse(xhr.responseText); // 解析响应数据
    console.log('请求成功:', response);
  } else if (xhr.readyState === 4) {
    console.error('请求失败:', xhr.statusText);
  }
};

// 发送请求(POST 请求可在这里传参:xhr.send(JSON.stringify(data)))
xhr.send();
  1. Fetch API

现代网络请求 API(ES6+ 引入),基于 Promise 设计,语法更简洁,支持链式调用和 async/await,是目前推荐的主流方案。 特点

  • 默认异步,返回 Promise 对象,适合处理异步逻辑;
  • 支持流式处理响应(如大文件下载);
  • 原生支持 Promise,可与 async/await 配合使用,代码更清晰;
  • 不支持同步请求,且错误处理需要手动判断 HTTP 状态码(如 404、500 不会触发 Promise 的 catch)。

基本用法示例

javascript 复制代码
// GET 请求
fetch('https://api.example.com/data')
  .then(response => {
    // 检查 HTTP 状态码(2xx 表示成功)
    if (!response.ok) {
      throw new Error(`HTTP 错误:${response.status}`);
    }
    return response.json(); // 解析 JSON 响应(也可使用 text()、blob() 等)
  })
  .then(data => console.log('请求成功:', data))
  .catch(error => console.error('请求失败:', error));

// POST 请求(带参数)
async function postData() {
  try {
    const response = await fetch('https://api.example.com/submit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: 'test', value: 123 }), // 请求体(需序列化)
    });
    if (!response.ok) throw new Error('提交失败');
    const result = await response.json();
    console.log('提交成功:', result);
  } catch (error) {
    console.error('错误:', error);
  }
}
  1. navigator.sendBeacon()

专门用于后台发送小型数据 (如统计信息、日志上报)的 API,确保页面卸载(如关闭标签页、刷新)时数据能被成功发送,不阻塞页面卸载流程。 特点

  • 异步发送,但不会阻塞当前页面的卸载或导航;
  • 主要用于 POST 请求,数据大小有限制(通常几 KB);
  • 适用于页面离开时的埋点、日志上报等场景。

基本用法示例

javascript 复制代码
// 页面关闭时上报数据
window.addEventListener('unload', () => {
  // 发送数据到服务器(数据会被编码为 form-data 格式)
  const data = { action: 'page_close', time: new Date().getTime() };
  const success = navigator.sendBeacon(
    'https://api.example.com/log',
    JSON.stringify(data) // 数据需序列化(或使用 FormData)
  );
  console.log('上报是否成功:', success);
});
  1. WebSocket

用于建立持久化的全双工通信连接 (基于 TCP),适用于实时通信场景(如聊天、实时数据更新)。与 HTTP 请求不同,WebSocket 是 "长连接",服务器和客户端可双向主动发送数据。 特点

  • 一旦连接建立,客户端和服务器可随时互发消息,无需重复发起请求;

  • 基于帧(frame)传输数据,开销小,实时性高;

  • 使用 ws://wss:// 协议(后者加密)。

基本用法示例

javascript 复制代码
// 建立 WebSocket 连接
const ws = new WebSocket('wss://api.example.com/realtime');

// 连接成功时触发
ws.onopen = () => {
  console.log('WebSocket 连接已建立');
  // 向服务器发送消息
  ws.send(JSON.stringify({ type: 'hello', data: '客户端已连接' }));
};

// 收到服务器消息时触发
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('收到服务器消息:', message);
};

// 连接关闭时触发
ws.onclose = (event) => {
  console.log('连接关闭,代码:', event.code);
};

// 连接出错时触发
ws.onerror = (error) => {
  console.error('WebSocket 错误:', error);
};

总结

API 适用场景 特点
XMLHttpRequest 兼容旧浏览器、需要监控进度的请求 支持同步 / 异步,API 较繁琐
Fetch API 现代 HTTP 请求(GET/POST 等) 基于 Promise,语法简洁,推荐优先使用
navigator.sendBeacon 页面卸载时的后台数据上报 不阻塞页面,确保数据送达
WebSocket 实时通信(聊天、实时更新) 长连接,双向通信,低延迟

兼容性与局限性

API 兼容性 主要局限性
XMLHttpRequest 极佳(IE6+ 及所有现代浏览器) API 设计繁琐,异步处理易嵌套,同步阻塞线程
Fetch API 现代浏览器(IE 完全不支持) 默认不携带 Cookie,错误需手动判断(404/500 不触发 catch)
navigator.sendBeacon() IE 不支持,现代浏览器支持 数据大小有限制,无法获取响应,请求方法固定为 POST
WebSocket 现代浏览器(IE10+) 需服务器支持 WebSocket 协议,不适合简单请求

适用场景总结

  • XMLHttpRequest:需兼容旧浏览器(如 IE)、需要监控上传 / 下载进度的场景(如文件上传)。
  • Fetch API :现代 Web 应用的常规 HTTP 请求(GET/POST 等),优先推荐(配合 async/await 代码清晰)。
  • navigator.sendBeacon():页面离开时的日志上报、用户行为统计(确保数据不丢失)。
  • WebSocket:实时通信场景(如在线聊天、股票行情、多人协作工具)。

通过这些区别可以看出,没有 "万能" 的 API,需根据具体需求(如是否实时、是否跨域、是否需要处理页面卸载)选择最合适的工具。

SSE 是原生 JavaScript 中专门用于 服务器单向持续推送数据 的 API(通过 EventSource 实现),它填补了 "服务器主动推送" 的需求空白,与其他 API 的核心区别在于 单向长连接推送基于 HTTP 协议的轻量实现

如果你的场景是 "客户端只需被动接收服务器更新,无需频繁向服务器发送数据",SSE 比 WebSocket 更简单易用;如果需要双向通信,则优先选 WebSocket。

实际开发中,Fetch API 是处理常规 HTTP 请求的首选;需要兼容旧浏览器时用 XMLHttpRequest;实时场景用 WebSocket;后台上报用 sendBeacon

JSONP实现原理

记忆点:核心原理是利用 <script> 标签的 src 属性不受同源策略限制的特性,实现跨域数据传输。

JSONP 的核心思想是:客户端定义一个回调函数,通过 <script> 标签跨域请求服务器,服务器将数据包裹在回调函数中返回,客户端通过执行回调函数获取数据

JSONP(JSON with Padding)是一种经典的跨域数据请求解决方案,其核心原理是利用 <script> 标签的 src 属性不受同源策略限制的特性,实现跨域数据传输。以下是其详细实现原理:

一、核心背景:同源策略与跨域限制

浏览器的同源策略规定:不同协议、域名、端口的页面之间,默认不允许通过 AJAX(XMLHttpRequest/Fetch)直接进行数据交互 。例如,http://a.com 的页面无法直接通过 AJAX 请求 http://b.com 的接口,否则会被浏览器拦截。

<script><img><link> 等标签的 src 属性是例外 ,它们的请求不受同源策略限制,可以跨域加载资源。JSONP 正是利用了 <script> 标签的这一特性。

二、JSONP 的实现原理

JSONP 的核心思想是:客户端定义一个回调函数,通过 <script> 标签跨域请求服务器,服务器将数据包裹在回调函数中返回,客户端通过执行回调函数获取数据

具体步骤如下:

1. 客户端准备回调函数

客户端在页面中预先定义一个回调函数(名称可自定义),用于接收和处理服务器返回的数据。例如:

javascript 复制代码
// 定义回调函数:参数为服务器返回的数据
function handleResponse(data) {
  console.log("跨域获取的数据:", data);
  // 处理数据的逻辑...
}

2. 动态创建 <script> 标签发起跨域请求

客户端通过 JavaScript 动态创建 <script> 标签,并将其 src 属性指向目标服务器的接口 URL。同时,需要在 URL 中通过参数(通常约定为 callback)告知服务器回调函数的名称(即步骤 1 中定义的函数名)。

示例代码:

javascript 复制代码
// 生成唯一的回调函数名(避免重复)
const callbackName = "handleResponse"; 

// 创建 script 标签
const script = document.createElement("script");
// 设置 src:跨域请求的 URL + 回调函数名参数
script.src = "http://example.com/api?callback=" + callbackName;
// 将 script 插入页面,触发请求
document.body.appendChild(script);

3. 服务器返回「回调函数包裹数据」的响应

服务器接收到请求后,解析 URL 中的 callback 参数(即客户端定义的回调函数名 handleResponse),然后将需要返回的数据包裹在该回调函数中,以 JavaScript 代码的形式返回。

例如,服务器需要返回的数据是 { "name": "JSONP", "type": "cross-domain" },则返回的响应内容为:

javascript 复制代码
handleResponse({ "name": "JSONP", "type": "cross-domain" });

4. 客户端执行回调函数获取数据

<script> 标签加载完服务器返回的响应后,浏览器会将其视为 JavaScript 代码并立即执行。此时,客户端预先定义的 handleResponse 函数会被调用,参数就是服务器返回的数据,从而实现跨域数据的获取。

5. 清理资源(可选)

请求完成后,可以移除动态创建的 <script> 标签,避免页面冗余:

javascript 复制代码
script.onload = function() {
  document.body.removeChild(script); // 加载完成后移除 script 标签
};

三、JSONP 的特点

  1. 优点
    • 实现简单,兼容性好(支持所有主流浏览器,包括低版本 IE)。
    • 不受同源策略限制,可跨域请求数据。
  2. 缺点
    • 仅支持 GET 请求 :因为 <script> 标签的 src 属性只能发起 GET 请求,无法用于 POST、PUT 等其他请求方式。
    • 安全性风险:服务器返回的是可执行的 JavaScript 代码,如果服务器被恶意攻击,可能返回恶意代码(如 XSS 攻击),导致客户端安全问题。
    • 无错误处理机制<script> 标签加载失败时,无法通过常规的 error 事件可靠捕获(不同浏览器行为不一致),难以处理请求失败的场景。
    • 依赖服务器配合:需要服务器主动支持 JSONP 格式,即在响应中包裹回调函数。

四、与 JSON 的区别

  • JSON 是一种轻量级的数据交换格式(纯数据),本质是字符串。
  • JSONP 是一种跨域请求方式,本质是通过 <script> 标签加载并执行包含数据的 JavaScript 代码,其响应内容是「回调函数调用 + JSON 数据」的组合。

总结

JSONP 通过巧妙利用 <script> 标签的跨域特性,以「回调函数包裹数据」的方式实现跨域数据传输,是早期解决跨域问题的重要方案。但由于其局限性(仅支持 GET、安全风险等),现代 Web 开发中更多使用 CORS(跨域资源共享) 方案,JSONP 逐渐成为历史遗留技术。

JSON进行深拷贝会有什么问题

记忆点:

JSON 深拷贝的核心问题是:JSON 格式无法完整映射 JavaScript 的所有数据类型和特性。它仅适用于 简单的纯数据对象(仅包含字符串、数字、布尔、数组、普通对象等基础类型),而对于包含函数、日期、正则、循环引用等复杂场景,会导致数据丢失、失真或报错。

使用 JSON 方法(JSON.stringify() 转字符串 + JSON.parse() 转对象)实现深拷贝是一种简单的方案,但它存在诸多限制和问题,主要源于 JSON 数据格式的局限性JavaScript 数据类型的复杂性 之间的不匹配。具体问题如下:

  1. 不支持非 JSON 标准数据类型

JSON 仅支持 null、布尔值、数字、字符串、数组、普通对象 这几种基础类型,对于 JavaScript 中的其他特殊类型,会出现 丢失或失真

  • 函数(Function)JSON.stringify() 会直接忽略函数(包括对象的方法),拷贝后该属性消失。

    javascript 复制代码
    const obj = { fn: () => 123 };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy.fn); // undefined(函数丢失)
  • 日期对象(Date)JSON.stringify() 会将 Date 转为 ISO 格式字符串(如 "2023-01-01T00:00:00.000Z"),但 JSON.parse() 会将其还原为 字符串 而非 Date 对象。

    javascript 复制代码
    const obj = { time: new Date() };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy.time instanceof Date); // false(变成字符串)
  • 正则对象(RegExp)JSON.stringify() 会将正则转为空对象 {},拷贝后完全丢失正则特性。

    javascript 复制代码
    const obj = { reg: /abc/g };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy.reg); // {}(正则丢失)
  • Symbol 类型 :作为对象的键或值时,JSON.stringify() 会直接忽略(键会被跳过,值会被转为 null

    javascript 复制代码
    const s = Symbol('key');
    const obj = { [s]: 'value', val: s };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy); // {}(Symbol 键和值均丢失)
  • 其他特殊类型 :如 MapSetWeakMapWeakSet 等集合类型,会被转为空对象或数组,失去原有的数据结构和方法。

  1. 无法处理循环引用

如果对象存在 循环引用 (如 obj.self = obj),JSON.stringify() 会直接 抛出错误,导致拷贝失败。

javascript 复制代码
const obj = {};
obj.self = obj; // 循环引用:对象引用自身

// 报错:Converting circular structure to JSON
const copy = JSON.parse(JSON.stringify(obj)); 
  1. 数值精度与特殊值失真
  • NaN、Infinity、-Infinity :JSON 不支持这些特殊数值,JSON.stringify() 会将它们转为 null

    javascript 复制代码
    const obj = { a: NaN, b: Infinity, c: -Infinity };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy); // { a: null, b: null, c: null }(数值失真)
  • 大整数精度丢失 :当数值超过 JavaScript 安全整数范围(2^53 - 1)时,JSON.parse() 会导致精度丢失。

    javascript 复制代码
    const obj = { num: 9007199254740993n }; // 大整数
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy.num); // 9007199254740992(精度丢失)
  1. 忽略对象的特殊属性
  • 不可枚举属性JSON.stringify() 仅序列化对象的 可枚举自有属性 ,不可枚举属性(如 Object.defineProperty 定义的 enumerable: false 属性)会被忽略。

  • 原型链属性

    :拷贝结果会丢失原对象的原型链,变成一个纯粹的

    css 复制代码
    Object

    实例,无法继承原型上的方法。

    javascript 复制代码
    function Person(name) { this.name = name; }
    Person.prototype.sayHi = () => 'hi';
    const p = new Person('Tom');
    
    const copy = JSON.parse(JSON.stringify(p));
    console.log(copy instanceof Person); // false(原型链丢失)
    console.log(copy.sayHi); // undefined(原型方法丢失)
  1. 性能问题

对于 大型复杂对象 (如嵌套层级极深、包含大量数据),JSON.stringify()JSON.parse() 的序列化 / 反序列化过程会产生额外的性能开销,效率低于专门的深拷贝实现(如递归拷贝 + 类型判断)。

总结

JSON 深拷贝的核心问题是:JSON 格式无法完整映射 JavaScript 的所有数据类型和特性 。它仅适用于 简单的纯数据对象(仅包含字符串、数字、布尔、数组、普通对象等基础类型),而对于包含函数、日期、正则、循环引用等复杂场景,会导致数据丢失、失真或报错。

如需处理复杂类型,建议使用专门的深拷贝方法(如递归遍历 + 类型判断、lodash.cloneDeep 等库函数)。

递归深拷贝怎么处理日期正则

记忆点:使用 Object.prototype.toString.call(target) 而非 instanceof,因为 instanceof 可能受原型链修改影响,而 toString 能稳定返回内置类型标签(如 [object Date])。

在递归深拷贝中处理日期(Date)和正则(RegExp)等特殊引用类型时,需要通过类型判断针对性构造新实例的方式,确保拷贝后的对象既保持原始值,又不与原对象共享引用。以下是具体实现思路和代码示例:

一、核心处理思路

  1. 类型精准判断 : 利用 Object.prototype.toString.call() 区分特殊类型(DateRegExp)与普通对象 / 数组,避免使用 typeof(因其对引用类型均返回 'object',无法区分)。
  2. 针对性复制
    • 日期(Date :通过原日期的时间戳创建新 Date 实例(new Date(originalDate.getTime())),确保新对象与原对象时间一致但引用不同。
    • 正则(RegExp :提取原正则的 source(模式)和 flags(修饰符,如 g/i/m),用 new RegExp(source, flags) 创建新实例,保持正则特性不变。

二、递归深拷贝实现(含日期和正则处理

javascript 复制代码
function deepClone(target) {
  // 1. 基本类型直接返回(null 单独处理,因 typeof null 为 'object')
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 处理日期类型
  if (Object.prototype.toString.call(target) === '[object Date]') {
    return new Date(target.getTime()); // 基于原时间戳创建新实例
  }

  // 3. 处理正则类型
  if (Object.prototype.toString.call(target) === '[object RegExp]') {
    // 提取正则的模式(source)和修饰符(flags)
    const reg = new RegExp(target.source, target.flags);
    // 复制 lastIndex 属性(正则.exec() 会用到的匹配位置)
    reg.lastIndex = target.lastIndex;
    return reg;
  }

  // 4. 处理数组(先判断数组,再判断普通对象,因数组也是 object)
  if (Array.isArray(target)) {
    const copy = [];
    for (let i = 0; i < target.length; i++) {
      copy[i] = deepClone(target[i]); // 递归拷贝数组元素
    }
    return copy;
  }

  // 5. 处理普通对象(排除上述特殊类型后)
  if (Object.prototype.toString.call(target) === '[object Object]') {
    const copy = {};
    // 遍历对象自身可枚举属性(不包含原型链)
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        copy[key] = deepClone(target[key]); // 递归拷贝属性值
      }
    }
    return copy;
  }

  // 6. 其他特殊类型(如 Map、Set 等,可按需扩展)
  return target;
}

三、测试验证

javascript 复制代码
// 测试日期拷贝
const originalDate = new Date('2023-01-01');
const clonedDate = deepClone(originalDate);
console.log(clonedDate); // 2023-01-01T00:00:00.000Z(值相同)
console.log(clonedDate === originalDate); // false(引用不同)
clonedDate.setFullYear(2024);
console.log(originalDate.getFullYear()); // 2023(原对象不受影响)

// 测试正则拷贝
const originalReg = /abc/gim;
originalReg.lastIndex = 5; // 设置匹配位置
const clonedReg = deepClone(originalReg);
console.log(clonedReg.source); // 'abc'(模式相同)
console.log(clonedReg.flags); // 'gim'(修饰符相同)
console.log(clonedReg.lastIndex); // 5(lastIndex 被复制)
console.log(clonedReg === originalReg); // false(引用不同)

四、关键说明

  1. 类型判断的准确性 : 使用 Object.prototype.toString.call(target) 而非 instanceof,因为 instanceof 可能受原型链修改影响,而 toString 能稳定返回内置类型标签(如 [object Date])。
  2. 正则的完整复制 : 除了 sourceflags,正则的 lastIndex 属性(用于全局匹配时记录下一次匹配位置)也需要手动复制,否则可能影响拷贝后正则的匹配行为。
  3. 扩展性 : 上述代码可扩展至其他特殊类型(如 MapSetError 等),思路类似:先判断类型,再通过对应构造函数创建新实例并复制关键属性。

通过这种方式,递归深拷贝能准确处理日期和正则类型,既保证拷贝后的值与原对象一致,又避免引用共享导致的副作用。

原生的异步方法有那些

在 JavaScript 中,"原生异步方法" 通常指语言本身(ECMAScript 标准)或运行环境(浏览器、Node.js)内置的、无需额外依赖库即可使用的异步机制 / API。以下是常见的原生异步方法及分类:

一、语言层面的异步机制

  1. Promise 及相关方法

ES6(2015)引入的 Promise 是 JavaScript 异步编程的核心,本身是异步操作的容器,其相关方法均为原生异步:

  • new Promise((resolve, reject) => { ... }):通过构造函数创建异步任务,通过 resolve/reject 控制状态。
  • 静态方法:Promise.resolve()(包装同步值为成功的 Promise)、Promise.reject()(包装同步值为失败的 Promise)、Promise.all()(并行执行多个 Promise,全部成功才返回)、Promise.race()(取第一个完成的 Promise 结果)、Promise.allSettled()(等待所有 Promise 完成,无论成功失败)、Promise.any()(取第一个成功的 Promise 结果)。
  1. async/await

ES2017 引入的语法糖,基于 Promise 实现,简化异步代码逻辑:

  • async function:声明异步函数,返回值自动包装为 Promise。
  • await:在异步函数中暂停执行,等待 Promise 完成后继续,避免回调嵌套。

二、定时器类异步方法

通过延迟或周期性执行回调实现异步,浏览器和 Node.js 均支持:

  • setTimeout(callback, delay, ...args):延迟 delay 毫秒后执行 callback(单次)。
  • setInterval(callback, interval, ...args):每隔 interval 毫秒执行一次 callback(周期性)。
  • 清除方法:clearTimeout(timerId)clearInterval(timerId)(终止定时器)。

三、微任务调度方法

微任务是优先级高于宏任务的异步任务,原生调度方法包括:

  • queueMicrotask(callback):ES2021 引入,将 callback 加入微任务队列,在当前宏任务完成后执行(优先级高于 setTimeout 等宏任务)。
  • Promise 的回调(then/catch/finally):Promise 状态变更后,其回调会作为微任务执行。

四、浏览器环境特有异步 API

浏览器提供的 Web API 中,大量异步方法用于处理网络、DOM、动画等:

  • 网络请求:
    • fetch(url, options):ES2015+ 原生网络请求 API,返回 Promise,替代传统的 XMLHttpRequest(XHR 也可异步,但 fetch 更现代)。
    • XMLHttpRequest:传统异步请求对象(通过 open(method, url, async=true) 开启异步)。
  • DOM 事:
    • 所有 DOM 事件回调(如 addEventListener('click', callback)onloadonerror 等)均为异步触发(事件队列调度)。
  • 动画与渲染:
    • requestAnimationFrame(callback):浏览器重绘前执行回调,用于高性能动画(同步于浏览器刷新频率,异步触发)。
    • requestIdleCallback(callback):在浏览器空闲时执行回调(低优先级异步)。
  • 其他:
    • WebSocket 事件(onopen/onmessage/onclose):WebSocket 通信的异步回调。
    • FileReader:异步读取本地文件(readAsText() 等方法的回调)。

五、Node.js 环境特有异步 API

Node.js 内置模块提供了大量异步方法,核心用于 I/O 操作(非阻塞 I/O 是 Node.js 核心特性):

  • 文件系统(fs 模块):
    • 异步文件操作:fs.readFile(path, callback)fs.writeFile(path, data, callback)fs.mkdir(path, callback) 等(所有不带 Sync 后缀的方法均为异步)。
  • 定时器与调度:
    • 同浏览器的 setTimeout/setInterval,额外支持 setImmediate(callback)(当前事件循环结束后执行,优先级低于微任务)。
  • 微任务与异步调度:
    • process.nextTick(callback):Node 特有微任务,优先级高于 queueMicrotask 和 Promise 微任务(在当前操作完成后立即执行)。
  • 流(Stream
    • 所有流操作(如 fs.createReadStream)的事件(data/end/error)均为异步触发。
  • 其他 I/O 模块:
    • 网络(net/http 模块):如 http.createServer()request 事件回调。
    • 数据库操作(原生 fs 之外,Node 内置模块无数据库 API,但第三方库通常基于原生异步机制实现)。

总结

原生异步方法的核心是通过 "非阻塞" 方式处理耗时操作(如网络请求、文件 I/O、定时器等),避免主线程阻塞。按场景可分为:语言层的 Promise/async-await、定时器、微任务调度,以及环境特有的 Web API(浏览器)或 Node.js 模块 API。

promise除了.catch还有哪些方法能铺获到异常

记忆点:

  • 最常用的是 .catch() 方法,可捕获整个 Promise 链的异常;

  • then() 的第二个参数可捕获当前 Promise 的 reject,但功能有限;

  • try/catch 配合 async/await 是异步代码中处理异常的更直观方式;

  • 静态方法Promise.all()Promise.race()的异常需通过其返回的 Promise 进行捕获。

在 Promise 中,除了 .catch() 方法,还有以下几种方式可以捕获异常(即处理 Promise 的 reject 状态):

  1. then() 方法的第二个参数

Promise.prototype.then() 方法可以接收两个参数:

  • 第一个参数:处理 Promise 成功状态(resolve)的回调函数;
  • 第二个参数:专门处理 Promise 失败状态(reject)的回调函数 ,作用等同于 .catch()

示例:

javascript 复制代码
new Promise((resolve, reject) => {
  reject(new Error("出错了"));
})
.then(
  (data) => { console.log("成功:", data); },  // 第一个参数:处理 resolve
  (err) => { console.log("捕获到错误:", err.message); }  // 第二个参数:处理 rect
);

注意then() 的第二个参数只能捕获当前 Promise 的 reject 错误 ,无法捕获其第一个参数(成功回调)中抛出的错误;而 .catch() 可以捕获整个 Promise 链中所有前置操作的错误(包括 then 回调中抛出的错误)。

2. try/catch(配合 async/await

虽然 try/catch 不是 Promise 自身的方法,但当使用 async/await 语法时,try/catch 可以捕获 await 后面 Promise 的异常(包括 reject 和回调中抛出的错误),是处理 Promise 异常的常用方式。

示例

javascript 复制代码
async function handlePromise() {
  try {
    const result = await new Promise((resolve, reject) => {
      reject(new Error("出错了"));
    });
    console.log("成功:", result);
  } catch (err) {
    console.log("捕获到错误:", err.message);  // 捕获 Promise 的 reject
  }
}

handlePromise();

3. Promise 静态方法的错误传播(间接捕获)

Promise 的静态方法(如 Promise.all()Promise.race() 等)返回的 Promise 会在内部某个 Promise 被 reject 时立即进入 reject 状态,此时可以通过 .catch()then() 的第二个参数捕获:

示例:

javascript 复制代码
// Promise.all() 中某个 Promise reject 时,整体会 reject
Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error("任务2失败")),
  Promise.resolve(3)
])
.catch(err => {
  console.log("捕获到错误:", err.message);  // 输出:"任务2失败"
});

总结

  • 最常用的是 .catch() 方法,可捕获整个 Promise 链的异常;
  • then() 的第二个参数可捕获当前 Promise 的 reject,但功能有限;
  • try/catch 配合 async/await 是异步代码中处理异常的更直观方式;
  • 静态方法的异常需通过其返回的 Promise 进行捕获。

这些方式共同构成了 Promise 异常处理的完整体系,可根据场景选择使用。

call和apply什么函数都生效嘛

callapply 并非对所有函数都生效:

  • 普通函数 (有自己的 this 绑定)有效,可改变 this 并调用。

  • 箭头函数 无效(this 固定,无法修改)。

  • 对部分内置函数 可能有限制(若 this 类型不符合要求会报错)。

可以说一下操作系统里的堆和栈吗?

记忆点:

  • 栈是 "轻量、高效的临时内存",用自动管理和快速访问支持函数调用,适合存储生命周期短、大小固定的数据;

  • 堆是 "灵活、动态的长期内存",用手动管理(或自动回收)支持动态数据存储,适合存储生命周期长、大小不确定的数据。

在操作系统中,堆(Heap)和栈(Stack)是进程内存空间中两种核心的动态内存区域,它们承担不同的功能,遵循不同的管理规则,是程序运行的基础。理解操作系统层面的堆和栈,有助于理解程序的内存分配、函数调用机制以及常见的内存问题(如栈溢出、内存泄漏)。

一、栈(Stack):函数调用的 "临时舞台"

操作系统中的栈是一块连续的内存区域 ,主要用于支持函数调用和局部变量存储,其核心特性是 "先进后出(FILO)",类似叠盘子:最后放入的盘子最先被取出。

1. 栈的核心功能

  • 存储函数调用上下文

    :当一个函数被调用时,操作系统会为其创建一个 "

    栈帧(Stack Frame)

    ",包含:

    • 函数的返回地址(调用结束后回到哪里继续执行);
    • 函数的参数(按调用约定入栈,如 C 语言的 "从右到左入栈");
    • 局部变量(函数内定义的临时变量,如int a = 10);
    • 被保存的寄存器状态(函数调用前需要保存寄存器的值,避免覆盖)。
  • 支持函数嵌套调用:每个嵌套调用的函数都会生成新的栈帧并 "压栈",函数执行结束后栈帧 "出栈",内存自动释放。

2. 栈的关键特性

  • 自动分配与释放:完全由操作系统(或编译器通过生成的机器码)自动管理,无需程序员干预。函数调用时栈帧入栈,函数返回时栈帧出栈,内存立即回收,不会产生碎片。
  • 连续内存与固定大小 :栈的内存地址是连续的(从高地址向低地址增长),大小通常在程序启动时由操作系统固定(如 Linux 默认栈大小为 8MB,可通过ulimit -s调整)。
  • 访问速度极快:栈的内存地址连续,且 CPU 会对栈进行缓存优化(栈顶附近的内存大概率被频繁访问),因此读写速度远快于堆。
  • 严格的生命周期:栈上的变量生命周期与函数调用绑定,函数执行结束后,局部变量立即失效,无法被外部访问。
  1. 栈的典型问题
  • 栈溢出(Stack Overflow) :若函数嵌套层数过深(如递归无终止条件)或局部变量过大(如定义巨型数组int arr[1000000]),会耗尽栈空间,触发栈溢出错误(程序崩溃)。

二、堆(Heap):动态内存的 "自由市场"

操作系统中的堆是一块非连续的内存区域 ,用于程序运行时动态分配内存(即 "按需申请、手动释放"),是存储长生命周期数据的主要区域。

  1. 堆的核心功能
  • 动态内存分配

    :程序运行时,通过系统调用(如 C 语言的

    c 复制代码
    malloc

    、C++ 的

    arduino 复制代码
    new

    ,底层依赖操作系统的

    复制代码
    brk

    复制代码
    sbrk

    复制代码
    mmap

    )向堆申请内存,用于存储大小不确定或生命周期较长的数据,例如:

    • 动态创建的对象(如new Object());
    • 长度动态变化的数组(如vector的动态扩容);
    • 跨函数共享的数据(如函数返回的动态分配指针)。

2. 堆的关键特性

  • 手动管理(或语言层自动管理) :堆内存需要显式申请(如malloc)和释放(如free),若忘记释放会导致 "内存泄漏";部分语言(如 Java、Python)通过垃圾回收器自动管理堆内存,避免手动操作。
  • 非连续内存与动态大小:堆的内存地址通常不连续(因频繁分配 / 释放导致碎片),大小没有固定上限(受限于系统可用内存和地址空间),从低地址向高地址增长。
  • 分配效率较低:堆的分配需要操作系统或内存管理库(如 glibc 的 ptmalloc)通过复杂算法(首次适应、最佳适应、伙伴系统等)查找空闲内存块,且可能涉及内存对齐、元数据记录(如块大小、是否已分配),因此速度远慢于栈。
  • 灵活的生命周期:堆上的数据生命周期不受函数调用限制,只要未被释放,就可以被程序的任意部分访问(通过指针或引用)。

3. 堆的典型问题

  • 内存泄漏:申请的堆内存未释放,导致内存被持续占用,长期运行会耗尽系统内存。
  • 内存碎片:频繁分配和释放不同大小的内存块,会导致堆中产生大量无法利用的小空闲块(碎片),降低内存利用率。
  • 悬空指针:若堆内存被释放后,指针未置空,后续访问该指针会导致 "未定义行为"(可能崩溃或数据错误)。

三、堆和栈在进程内存布局中的位置

在 32 位或 64 位进程的虚拟地址空间中,堆和栈的位置是固定划分的,典型布局如下(从低地址到高地址): 代码段(.text)→ 数据段(.data/.bss)→ 堆(Heap)→ 内存映射区(mmap)→ 栈(Stack)→ 内核空间

  • 栈从高地址向低地址增长(每次压栈,栈顶指针减小);
  • 堆从低地址向高地址增长(每次分配,堆顶指针增大);
  • 中间的 "内存映射区" 用于加载共享库、文件映射等,堆和栈之间有足够的空间避免重叠。

四、堆和栈的核心差异对比

维度 栈(Stack) 堆(Heap)
管理方式 操作系统自动分配 / 释放(函数调用驱动) 手动申请 / 释放(或垃圾回收)
内存地址 连续,从高地址向低地址增长 非连续,从低地址向高地址增长
大小限制 固定且较小(通常 MB 级) 动态且较大(可达 GB 级,受系统内存限制)
访问速度 极快(CPU 缓存优化,连续地址) 较慢(需查找空闲块,非连续地址)
用途 函数调用上下文、局部变量 动态数据、长生命周期对象
典型问题 栈溢出 内存泄漏、碎片、悬空指针
分配粒度 小(栈帧大小固定或可预测) 灵活(大小可变,按需分配)

五、总结

操作系统中的堆和栈是内存管理的 "两大支柱",分工明确:

  • 是 "轻量、高效的临时内存",用自动管理和快速访问支持函数调用,适合存储生命周期短、大小固定的数据;
  • 是 "灵活、动态的长期内存",用手动管理(或自动回收)支持动态数据存储,适合存储生命周期长、大小不确定的数据。

两者的配合让程序既能高效处理函数调用和临时数据,又能灵活管理动态变化的复杂数据,是现代程序运行的基础内存模型。

为什么变量放到栈里,对象放堆里 ?

记忆点:

  • 栈用高效的自动管理和快速访问,适配大小固定、生命周期明确的基本类型变量;
  • 堆用动态分配和灵活的内存管理,适配大小可变、生命周期不确定的对象。

基本数据类型确定大小,生命周期确定。

在编程语言(如 JavaScript、Java、C# 等)的内存管理中,基本类型变量存放在栈(Stack)中,对象(引用类型)存放在堆(Heap)中,这一设计主要源于栈和堆的内存特性差异,以及变量与对象的自身特点的适配性。核心原因可以从内存分配机制、数据特性和访问效率三个维度来解释:

1. 栈和堆的内存特性差异

栈和堆是计算机内存中两种不同的存储区域,它们的分配 / 释放方式功能定位截然不同:

特性 栈(Stack) 堆(Heap)
分配方式 自动分配、自动释放(由编译器 / 解释器管理) 动态分配、手动或垃圾回收释放(需显式管理或语言自动回收)
空间大小 空间较小(通常几 MB),大小固定 空间较大(可至 GB 级),大小动态扩展
访问速度 速度极快(内存地址连续,CPU 缓存友好) 速度较慢(内存地址分散,需通过指针访问)
数据结构 线性结构(先进后出,FILO) 树形 / 图结构(无固定顺序)

2. 基本类型变量与栈的适配性

基本类型变量(如数字、布尔值、null、undefined、短字符串等)的特点是:

  • 大小固定 :例如 JavaScript 中 Number 固定为 8 字节,Boolean 为 1 字节,编译期即可确定占用空间。
  • 生命周期明确:通常与函数作用域绑定(如局部变量),函数执行时创建,执行结束后销毁,生命周期短且可预测。

这些特点完美匹配栈的特性:

  • 栈的自动分配 / 释放机制可以高效处理这类变量:函数调用时,局部变量随 "栈帧"(Stack Frame)入栈;函数执行完毕,栈帧出栈,变量内存自动释放,无需手动管理,效率极高。
  • 栈的连续内存快速访问特性,让基本类型的读写速度更快(直接操作值,无需间接寻址)。

3. 对象(引用类型)与堆的适配性

对象(引用类型)(如对象、数组、函数等)的特点是:

  • 大小不固定 :对象可以动态添加属性(如 obj.newProp = 1),数组可以动态扩容(如 arr.push(2)),编译期无法确定最终大小。
  • 生命周期不确定 :对象可能被多个变量引用(如 let a = obj; let b = a),其生命周期不局限于某个函数作用域,无法随栈帧释放。

这些特点更适合堆的特性:

  • 堆的动态分配能力可以满足对象大小可变的需求:内存按需分配,即使后续扩容也能通过动态调整实现(代价是可能产生内存碎片,但灵活性更高)。
  • 堆的手动 / 垃圾回收释放机制适配对象的长生命周期:当对象不再被任何变量引用时,由垃圾回收器(如 JS 的标记 - 清除算法)识别并释放内存,避免了栈内存 "生命周期固定" 的限制。
  • 引用传递节省内存:对象在栈中只存储堆内存的地址(指针),而非对象本身。多个变量可以通过共享指针引用同一个对象,避免重复存储大数据,节省内存空间。

4. 典型场景举例(以 JavaScript 为例)

javascript 复制代码
function fn() {
  // 基本类型变量:存栈中
  let num = 100; // 栈中直接存值 100
  let str = "hello"; // 栈中存字符串值(短字符串通常优化为栈存储)

  // 对象:栈中存地址,堆中存实际数据
  let obj = { name: "foo" }; // 栈中存地址(如 0x123),堆中存 { name: "foo" }
  let arr = [1, 2, 3]; // 栈中存地址(如 0x456),堆中存数组数据
}

fn(); 
// 函数执行结束后,栈帧释放:num、str、obj/arr 的地址被清除
// 堆中的对象/数组若不再被引用,后续由垃圾回收器释放
  • 当访问 num 时,直接从栈中读取值;
  • 当访问 obj.name 时,先从栈中读取 obj 存储的地址(如 0x123),再通过地址到堆中找到对象,读取 name 属性。

总结

栈和堆的分工本质是 **"效率" 与 "灵活性" 的权衡 **:

  • 栈用高效的自动管理和快速访问,适配大小固定、生命周期明确的基本类型变量;
  • 堆用动态分配和灵活的内存管理,适配大小可变、生命周期不确定的对象。

这种设计既保证了基本类型的快速读写和内存高效利用,又满足了对象动态扩展和复杂引用关系的需求,是编程语言内存管理的经典优化方案。

async/await 实现原理

async/await 是 JavaScript 中用于简化异步编程的语法糖,其底层基于 PromiseGenerator 函数 的机制实现,本质是对 Promise 异步模式的封装,让异步代码的写法更接近同步代码的直观性。

核心原理拆解

1. async 函数的本质

async 关键字用于声明一个异步函数,其核心特性是:

  • 返回值自动包装为 Promise :无论函数内部 return 什么值(非 Promise 类型),都会被自动包装成一个 resolved 状态的 Promise;如果函数内部抛出错误,则会被包装成 rejected 状态的 Promise。
  • 内部支持 await 关键字 :只有在 async 函数内部才能使用 await,用于 "等待" 一个 Promise 完成。

2. await 的工作机制

await 关键字的作用是 "暂停" 当前 async 函数的执行,等待其后的 Promise 完成(resolved 或 rejected),然后恢复执行并获取结果。其底层逻辑可拆解为:

  • 将后续代码转为回调await 后面的表达式会被优先执行,若结果不是 Promise,则会被自动包装成 resolved 状态的 Promise(如 await 123 等价于 await Promise.resolve(123))。
  • 暂停与恢复 :当遇到 await 时,JavaScript 引擎会暂停当前 async 函数的执行,将函数的后续代码(await 之后的部分)封装成一个 "回调函数",并将这个回调函数注册到 await 对应的 Promise 的 then 方法中。
  • 控制权移交 :暂停执行后,控制权会交还给调用者(如事件循环),直到 await 的 Promise 完成,再通过之前注册的回调函数恢复 async 函数的执行。

3. 与 Generator 函数的关联

async/await 的实现借鉴了 Generator 函数(带 * 的函数,配合 yield 使用)的 "暂停 / 恢复" 特性,但做了关键优化:

  • Generator 函数需要手动调用 next() 方法恢复执行,而 async 函数会自动根据 Promise 的状态恢复执行(无需手动干预)。
  • Generator 函数本身不与 Promise 强绑定,而 async/await 天然与 Promise 结合,更适合异步场景。

可以简单理解:async 函数相当于一个 "自动执行的 Generator 函数",其内部通过类似 Generator 的状态机管理执行流程,而 await 相当于增强版的 yield(自动处理 Promise 状态)。

4. 事件循环中的执行时机

await 后面的代码会被放入 微任务队列 (与 Promise.then 的回调一致),等待当前同步代码执行完毕后,再按照微任务队列的顺序执行。这也是为什么 await 能 "暂停" 却不阻塞整个线程的原因(JavaScript 是单线程,通过事件循环实现非阻塞)。

代码示例:模拟 async/await 原理

下面用 Generator 函数配合 Promise 模拟 async/await 的执行逻辑,帮助理解其底层实现:

javascript 复制代码
// 模拟 async 函数(自动执行的 Generator)
function asyncToGenerator(generatorFunc) {
  return function() {
    const gen = generatorFunc.apply(this, arguments);
    
    // 返回一个 Promise,对应 async 函数的返回值
    return new Promise((resolve, reject) => {
      // 递归执行 Generator 的 next()
      function step(key, arg) {
        let info;
        try {
          info = gen[key](arg); // 执行 next() 或 throw()
        } catch (error) {
          reject(error); // 捕获 Generator 内部抛出的错误
          return;
        }
        
        const { value, done } = info;
        if (done) {
          // Generator 执行完毕,resolve 最终结果
          resolve(value);
        } else {
          // 将 yield 的结果包装为 Promise,等待其完成后继续执行
          Promise.resolve(value).then(
            (val) => step('next', val), // 成功:继续执行 next()
            (err) => step('throw', err) // 失败:抛出错误并终止
          );
        }
      }
      
      // 启动 Generator
      step('next');
    });
  };
}

// 用模拟的 async 函数实现一个异步场景
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('数据加载完成'), 1000);
});

// 模拟 async/await 的使用
const mockAsyncFunc = asyncToGenerator(function* () {
  console.log('开始执行');
  const data = yield fetchData(); // 模拟 await
  console.log(data); // 1秒后输出:数据加载完成
  return '执行结束';
});

// 调用模拟的 async 函数
mockAsyncFunc().then(result => {
  console.log(result); // 输出:执行结束
});

总结

async/await 的本质是:

  1. 以更友好的语法封装了 Promise 的异步逻辑;
  2. 借助 Generator 的 "暂停 / 恢复" 机制实现代码的顺序执行感;
  3. 通过事件循环的微任务队列管理异步回调的执行时机。

这种设计既保留了 Promise 的非阻塞特性,又解决了 Promise 链式调用(.then())可能导致的 "回调地狱" 问题,让异步代码更易读、易维护。

Generator 是如何判断是否可执行?

在 JavaScript 中,Generator(生成器) 的 "可执行性" 主要指其返回的生成器对象(Generator Object)是否还能继续执行并产生值 。这种判断取决于生成器对象的内部状态执行上下文,具体逻辑如下:

一、Generator 的核心概念

首先明确两个关键角色:

  • 生成器函数(Generator Function) :用 function* 定义的函数,调用后不直接执行函数体 ,而是返回一个生成器对象
  • 生成器对象(Generator Object) :既是迭代器(Iterator) (有 next() 方法),也是可迭代对象(Iterable) (有 Symbol.iterator 方法)。它是实际 "可执行" 的主体,负责暂停 / 恢复生成器函数的执行。

二、生成器对象的 "可执行性" 判断依据

生成器对象的可执行性由其内部状态 决定,状态变化与 next()throw()return() 等方法的调用紧密相关。主要状态包括:

  1. 初始状态(suspended start)

生成器函数被调用后,生成器对象刚创建时处于此状态:

  • 函数体尚未开始执行(停在函数第一行代码之前)。
  • 可执行 :调用 next() 会启动执行,直到遇到第一个 yield 暂停。
  1. 暂停状态(suspended yield)

执行过程中遇到 yield 表达式时,生成器进入此状态:

  • 函数体暂停在 yield ,并将 yield 后的值作为 next() 返回结果的 value
  • 可执行 :再次调用 next() 会从暂停处继续执行,直到下一个 yield 或函数结束。
  1. 执行中状态(executing)

调用 next()throw() 等方法后,生成器正在执行函数体时的临时状态:

  • 此时无法再次调用 next()(会报错,因为 JavaScript 是单线程,同一生成器对象不能并发执行)。
  • 不可执行:需等待当前执行完成(进入暂停或关闭状态)后才能再次操作。
  1. 关闭状态(closed)

当生成器函数体正常执行完毕 (遇到 return 或函数结尾),或被强制终止 (调用 return()throw() 且异常未捕获)时,进入此状态:

  • 函数体已终止,无法再恢复执行。
  • 不可执行 :再次调用 next() 会直接返回 { value: undefined, done: true }

三、如何判断生成器对象是否可执行?

JavaScript 标准并未直接暴露生成器对象的状态属性,但可通过以下方式间接判断:

1. 通过 next() 方法的返回值

生成器对象的 next() 方法返回一个对象 { value: any, done: boolean }

  • donefalse:说明生成器处于暂停状态可继续执行 (再次调用 next() 会恢复)。
  • donetrue:说明生成器处于关闭状态不可再执行
javascript 复制代码
function* gen() {
  yield 1;
  yield 2;
}

const g = gen(); // 生成器对象(初始状态)

console.log(g.next()); // { value: 1, done: false } → 可继续执行
console.log(g.next()); // { value: 2, done: false } → 可继续执行
console.log(g.next()); // { value: undefined, done: true } → 不可执行

2. 检查是否已被强制关闭

以下操作会导致生成器进入关闭状态 ,后续调用 next() 均返回 done: true

  • 生成器函数执行到结尾(无 return 时,隐式返回 undefined)。
  • 调用生成器对象的 return(value) 方法(强制返回指定值并关闭)。
  • 调用生成器对象的 throw(error) 方法,且函数体内未捕获该异常(异常抛出后关闭)。
javascript 复制代码
function* gen() {
  yield 1;
  try {
    yield 2;
  } catch (e) {
    console.log(e); // 捕获异常,生成器不会关闭
  }
  yield 3; // 仍可执行
}

const g = gen();
g.next(); // { value: 1, done: false }
g.throw(new Error("中断")); // 抛出异常被捕获
g.next(); // { value: 3, done: false } → 仍可执行
g.return("结束"); // 强制关闭,返回 { value: "结束", done: true }
g.next(); // { value: undefined, done: true } → 不可执行

四、总结

Generator 的 "可执行性" 本质是生成器对象是否处于可恢复执行的状态(初始状态或暂停状态)。判断方式主要是:

  • 调用 next() 方法,通过返回值的 done 属性判断:done: false 表示可继续执行,done: true 表示已关闭(不可执行)。
  • 注意:生成器对象一旦进入关闭状态,无论何种原因,都无法再恢复执行。

这种设计使得 Generator 能够实现 "暂停 - 恢复" 的协作式多任务,是 JavaScript 中处理异步逻辑(如早期的 co 库)和迭代场景的重要工具。

什么是协程?

在计算机科学中,协程(Coroutine) 是一种轻量级的程序组件,用于实现多任务之间的协作式调度。它允许程序在执行过程中主动暂停自身 ,将控制权转移给其他协程,之后再从暂停处恢复执行。这种 "暂停 - 恢复" 的特性让协程能够高效地实现并发逻辑,尤其适合处理 I/O 密集型任务或需要灵活调度的场景。

一、协程的核心特点

  1. 用户态调度 协程的调度完全由程序自身控制(用户态),而非操作系统内核(内核态)。这意味着协程的创建、暂停、恢复等操作无需经过内核,开销远小于线程或进程。
  2. 协作式并发 协程之间的调度是 "自愿" 的:一个协程必须主动调用暂停操作(如 yieldawait),才能将控制权交给其他协程。而线程是 "抢占式" 的,操作系统可强制剥夺线程的 CPU 使用权。
  3. 状态保存与恢复 当协程暂停时,其执行状态(如局部变量、程序计数器位置等)会被保存;恢复时,这些状态会被重新加载,程序能从暂停处继续执行,就像从未中断过一样。
  4. 轻量级 一个进程可以包含多个线程,一个线程可以运行多个协程。协程的内存占用极小(通常仅几 KB),支持创建数十万甚至数百万个协程,而线程的数量受限于系统资源(通常最多几千个)。

二、协程与进程、线程的区别

特性 进程(Process) 线程(Thread) 协程(Coroutine)
调度者 操作系统内核 操作系统内核 程序自身(用户态)
上下文切换开销 大(涉及内存、寄存器等) 中(比进程小,仍需内核参与) 极小(仅保存少量状态)
资源占用 高(独立内存空间) 中(共享进程内存) 极低(共享线程资源)
并发模型 抢占式 抢占式 协作式
通信方式 IPC(管道、socket 等) 共享内存 / 锁 直接共享线程内存

三、协程的典型应用场景

  1. I/O 密集型任务 当程序需要频繁等待 I/O 操作(如网络请求、文件读写、数据库查询)时,协程可以在等待期间主动让出 CPU,让其他协程执行,避免资源浪费。例如:
    • 网络爬虫:发起多个请求后,在等待响应时切换到其他任务。
    • 服务器开发:处理大量客户端连接,每个连接用协程管理,等待数据时不阻塞。
  2. 复杂流程控制 协程的 "暂停 - 恢复" 特性适合实现需要分步执行、状态保存的逻辑,例如:
    • 游戏中的角色 AI:每帧执行一部分逻辑,暂停后下一帧继续。
    • 工作流引擎:按步骤执行任务,支持中断后恢复。
  3. 替代回调地狱 在异步编程中,嵌套回调会导致代码可读性差("回调地狱")。协程可以用同步的代码风格编写异步逻辑,例如 JavaScript 的 async/await(基于 Promise 和生成器实现,本质是协程的简化)。

四、不同语言中的协程实现

协程的概念在不同编程语言中有不同的实现和命名,核心思想一致但细节有差异:

  • Python :通过 asyncio 模块提供 async/await 语法,协程需运行在事件循环中。
  • JavaScriptGenerator 函数(function*)支持暂停 / 恢复,async/await 是协程的语法糖(基于 Promise)。
  • Gogoroutine 本质是轻量级线程,但调度器结合了协程的协作式特点,由 Go runtime 控制。
  • Lua :原生支持协程(coroutine 库),可显式创建、暂停和恢复。
  • C++20 :引入 std::coroutine 标准库,支持协程语法。

五、总结

协程是一种用户态的轻量级并发组件,通过主动暂停和恢复实现多任务协作。它的优势在于:

  • 极低的上下文切换开销,适合大规模并发;
  • 用同步代码风格编写异步逻辑,提升可读性;
  • 高效利用 CPU,尤其优化 I/O 等待场景。

协程并不替代进程或线程,而是建立在它们之上的更细粒度的调度单位,是现代编程语言处理并发任务的重要工具。

闭包它具体的作用机制是怎么样的?闭包具体是什么东西?可以解释一下吗?

闭包是编程语言中一种重要的语法特性,本质上是一个函数及其捆绑的周边环境状态(lexical environment,词法环境)的组合。简单说,就是当一个函数能记住并访问它创建时所在的作用域,即使这个函数在其创建的作用域之外执行,这种现象就叫闭包。

闭包的核心特征

要形成闭包,需要满足三个条件:

  1. 函数嵌套:存在内部函数和外部函数(外层函数包含内层函数)。
  2. 变量引用:内部函数引用了外部函数中的变量(或参数)。
  3. 外部返回:外部函数将内部函数作为返回值返回,使得内部函数能在外部函数的作用域之外被调用。

作用机制:为什么内部函数能 "记住" 外部变量?

我们通过一个具体例子(以 JavaScript 为例)理解其工作机制:

javascript 复制代码
// 外部函数
function outer() {
  // 外部函数的局部变量
  let count = 0;
  
  // 内部函数(闭包)
  function inner() {
    count++; // 引用外部函数的变量
    return count;
  }
  
  // 返回内部函数
  return inner;
}

// 调用外部函数,得到内部函数(此时outer已经执行完毕)
const closureFunc = outer();

// 在outer作用域之外调用内部函数
console.log(closureFunc()); // 输出1
console.log(closureFunc()); // 输出2
console.log(closureFunc()); // 输出3

机制拆解:

  1. 作用域创建 :当outer()被调用时,会创建一个作用域 (可以理解为一个 "变量存储容器"),里面包含count变量和inner函数。
  2. 内部函数引用外部变量inner函数引用了outer作用域中的count变量,此时inner会 "记住" 这个变量的位置。
  3. 外部函数执行完毕但作用域不销毁 :通常,函数执行完毕后,其内部变量会被垃圾回收机制回收(释放内存)。但这里outer返回了inner函数,而inner还在引用outer中的count,因此 JavaScript 引擎会保留outer的作用域,不让它被销毁。
  4. 闭包函数在外部调用时的行为 :当closureFunc()(即inner)在outer作用域之外被调用时,它依然能通过 "作用域链" 找到并操作outer中保留的count变量,因此每次调用都会让count递增。

闭包的本质

闭包的本质是函数对其词法作用域的 "捕获"。无论函数被带到哪里执行,它始终能访问自己创建时所在的作用域中的变量,就像带着一个 "背包",里面装着它需要的外部变量。

闭包的典型用途

  1. 保存状态:如上面的例子,通过闭包可以在函数调用之间保留变量的状态(类似 "私有变量")。

  2. 模块化与封装:创建私有变量和方法,避免全局污染。例如:

    javascript 复制代码
    function createCounter() {
      let count = 0; // 私有变量,外部无法直接访问
      return {
        increment: () => { count++; },
        getCount: () => count
      };
    }
    
    const counter = createCounter();
    counter.increment();
    console.log(counter.getCount()); // 1(只能通过暴露的方法访问)
  3. 延迟执行:在定时器、事件监听等场景中,闭包可以记住当时的变量状态。例如:

    javascript 复制代码
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 100); // 若用var,会输出3个3
    }
    
    // 用闭包解决:
    for (var i = 0; i < 3; i++) {
      (function(j) { // 闭包捕获每次循环的j
        setTimeout(() => console.log(j), 100); // 输出0、1、2
      })(i);
    }

总结

闭包就是 "带着环境的函数",它让函数突破了 "执行完就销毁内部变量" 的限制,能够记住并操作创建时的周边变量。这种特性让它在状态保存、封装、延迟执行等场景中非常有用,但也需要注意过度使用可能导致的内存占用问题(因为闭包会保留作用域,不及时释放可能造成内存泄漏)。

作用域闭包

www.yuque.com/ergz/web/gu...

深浅复制

www.yuque.com/ergz/web/sl...

构造函数、原型和原型链

www.yuque.com/ergz/web/fi...

GC(垃圾回收)的两种类型

GC 是浏览器自动回收不再使用的内存的机制,核心是识别 "垃圾"(不可访问的对象)并释放其占用的内存。常见的两种类型如下:

  1. 标记 - 清除(Mark-and-Sweep)

    • 原理:

      1. 标记阶段 :从根对象(如 window、全局变量)出发,遍历所有可访问的对象,标记为 "活跃"。
      2. 清除阶段:未被标记的对象被视为 "垃圾",回收其内存。
    • 优点:实现简单,适用于大多数场景。

    • 缺点:

      • 清除后会产生内存碎片(空闲内存分散),可能导致后续大对象无法分配连续内存。
      • 执行时会暂停 JS 主线程("全停顿"),大型应用可能出现卡顿。
  2. 标记 - 整理(Mark-and-Compact)

    • 原理:

      1. 先执行 "标记 - 清除" 的标记阶段,识别活跃对象。
      2. 整理阶段:将所有活跃对象向内存一端移动,集中排列,然后清除边界外的所有垃圾内存。
    • 优点:解决了内存碎片问题,内存分配更高效。

    • 缺点:整理阶段需要移动对象,耗时更长,"全停顿" 时间更久。

补充:现代浏览器的 GC 优化

为减少 "全停顿" 影响,现代浏览器(如 Chrome 的 V8 引擎)采用分代回收(结合上述两种类型):

  • 新生代(Young Generation):存放短期存活对象(如局部变量),采用 "复制算法"(快速回收,无碎片)。
  • 老生代(Old Generation):存放长期存活对象(如全局变量),采用 "标记 - 清除"+"标记 - 整理"(兼顾效率与碎片问题)。

通过分代策略,GC 可针对不同生命周期的对象优化回收频率和方式,平衡性能与内存利用率。

重绘(Repaint)与重排 / 回流(Reflow)的区别

当 DOM 或样式发生变化时,浏览器可能触发重绘或重排,两者的核心区别在于是否影响元素的几何信息:

特性 重排(Reflow/Layout) 重绘(Repaint)
触发原因 元素几何信息改变(位置、尺寸、结构变化等) 元素样式改变但几何信息不变(颜色、背景、透明度等)
示例 - 改变 widthheightleftdisplay: none - 窗口大小调整、字体变化 - 新增 / 删除 DOM 元素 - 改变 colorbackgroundborder-radius - 改变 visibility: hidden(元素仍占据空间)
性能消耗 高(需重新计算布局,可能连锁影响父 / 子元素) 中(无需重新布局,仅重绘像素)
关联关系 重排一定会导致重绘(布局变了,样式也需重新绘制) 重绘不一定导致重排(样式变了,布局可能不变)

EventLoop

事件循环是一个不停的从 宏任务队列/微任务队列中取出对应任务的**「循环函数」。在一定条件下,你可以将其类比成一个永不停歇的 「永动机」。 它从宏/微任务队列中 「取出」任务并将其 「推送」「调用栈」**中被执行。

事件循环包含了四个重要的步骤:

  1. 「执行Script」:以**「同步的方式」**执行script里面的代码,直到调用栈为空才停下来。 其实,在该阶段,JS还会进行一些预编译等操作。(例如,变量提升等)。
  2. 执行**「一个」宏任务:从宏任务队列中挑选「最老」**的任务并将其推入到调用栈中运行,直到调用栈为空。
  3. 执行**「所有」微任务:从微任务队列中挑选 「最老」的任务并将其推入到调用栈中运行,直到调用栈为空。 「但是,但是,但是」(转折来了),继续从微任务队列中挑选最老的任务并执行。直到「微任务队列为空」**。
  4. 「UI渲染」 :渲染UI,然后,「跳到第二步」,继续从宏任务队列中挑选任务执行。(这步只适用浏览器环境,不适用Node环境)

promise原理

Promise 是 JavaScript 中处理异步操作的核心机制,它通过状态管理和回调函数解决了传统回调地狱的问题。下面从实现原理、关键特性到完整代码,深入解析 Promise 的工作机制。

一、核心概念与状态机

  1. 三种状态
  • pending(进行中):初始状态。
  • fulfilled(已成功):操作完成且成功。
  • rejected(已失败):操作完成但失败。

状态转换规则

plaintext 复制代码
pending → fulfilled(不可逆转)
pending → rejected(不可逆转)
  1. 基本结构

Promise 本质是一个状态机,包含:

  • 状态 (state):初始为 pending

  • 结果(value/reason):保存成功值或失败原因。

    • 回调队列(then/catch 注册的回调):状态改变时触发。

    二、实现 Promise 的核心逻辑

    1. 基础框架

    javascript

    javascript 复制代码
    class MyPromise {
      constructor(executor) {
     // 初始状态与结果
        this.state = 'pending';
     this.value = undefined;
        this.reason = undefined;
     
        // 存储成功和失败的回调函数
     this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
        
        // 成功回调
        const resolve = (value) => {
          if (this.state === 'pending') {
            this.state = 'fulfilled';
            this.value = value;
            // 执行所有成功回调
            this.onFulfilledCallbacks.forEach(callback => callback());
          }
        };
        
        // 失败回调
        const reject = (reason) => {
          if (this.state === 'pending') {
            this.state = 'rejected';
            this.reason = reason;
            // 执行所有失败回调
            this.onRejectedCallbacks.forEach(callback => callback());
          }
        };
        
        // 执行 executor 函数
        try {
          executor(resolve, reject);
        } catch (error) {
          reject(error);
        }
      }
      
      // then 方法实现
      then(onFulfilled, onRejected) {
        // 参数校验与默认值
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error; };
        
        // 创建新 Promise 用于链式调用
        const newPromise = new MyPromise((resolve, reject) => {
          // 处理已成功的情况
          const handleFulfilled = () => {
            try {
              const result = onFulfilled(this.value);
              resolve(result); // 直接传递结果
            } catch (error) {
              reject(error);
            }
          };
          
          // 处理已失败的情况
          const handleRejected = () => {
            try {
              const result = onRejected(this.reason);
              resolve(result); // 失败回调的结果仍传递给下一个 Promise
            } catch (error) {
              reject(error);
            }
          };
          
          // 根据当前状态执行回调
          if (this.state === 'fulfilled') {
            setTimeout(handleFulfilled, 0); // 确保异步执行
          } else if (this.state === 'rejected') {
            setTimeout(handleRejected, 0);
          } else {
            // 状态为 pending 时,存储回调
            this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0));
            this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0));
          }
        });
        
        return newPromise;
      }
      
      // catch 方法实现
      catch(onRejected) {
        return this.then(null, onRejected);
      }
      
      // finally 方法实现
      finally(callback) {
        return this.then(
          value => MyPromise.resolve(callback()).then(() => value),
          error => MyPromise.resolve(callback()).then(() => { throw error; })
        );
      }
      
      // 静态方法:Promise.resolve
      static resolve(value) {
        if (value instanceof MyPromise) return value;
        return new MyPromise(resolve => resolve(value));
      }
      
      // 静态方法:Promise.reject
      static reject(reason) {
        return new MyPromise((_, reject) => reject(reason));
      }
      
      // 静态方法:Promise.all
      static all(promises) {
        return new MyPromise((resolve, reject) => {
          const results = [];
          let completed = 0;
          
          if (promises.length === 0) {
            resolve(results);
            return;
          }
          
          promises.forEach((promise, index) => {
            MyPromise.resolve(promise).then(
              value => {
                results[index] = value;
                completed++;
                if (completed === promises.length) resolve(results);
              },
              error => reject(error)
            );
          });
        });
      }
      
      // 静态方法:Promise.race
      static race(promises) {
        return new MyPromise((resolve, reject) => {
          promises.forEach(promise => {
            MyPromise.resolve(promise).then(
              value => resolve(value),
              error => reject(error)
            );
          });
        });
      }
    }

    三、关键特性解析

    1. 状态不可变性

    状态一旦改变(pending → fulfilledpending → rejected),就无法再次修改。这确保了 Promise 的结果唯一性。
    2. 异步执行

    thencatch 的回调总是异步执行的,通过 setTimeoutqueueMicrotask 实现。这保证了 Promise 的行为一致性。
    3. 链式调用

    thencatch 方法返回新的 Promise,允许链式调用。前一个 Promise 的结果会传递给下一个。
    4. 值穿透

    如果 thencatch 没有提供回调函数(如 promise.then().then(handleValue)),值会自动传递到下一个回调。
    5. 错误冒泡

    错误会一直向后传递,直到被 catch 捕获。

    四、静态方法实现

    1. Promise.all

    并行处理多个 Promise,返回一个新 Promise:

    • 所有 Promise 成功时,结果为所有值的数组。
  • 任何一个 Promise 失败时,立即返回该错误。

  1. Promise.race

返回一个 Promise,其结果为第一个完成(成功或失败)的 Promise 的结果。

  1. Promise.resolve/reject

快速创建已解决或已拒绝的 Promise。

五、与 ES6 Promise 的差异

上述实现是简化版,原生 ES6 Promise 还有以下特性:

markdown 复制代码
  1. **微任务队列**:使用 `queueMicrotask` 而非 `setTimeout`,执行优先级更高。

  2. **Promise 嵌套处理**:更严格的 `thenable` 对象检测。
  3. **更完善的错误处理**:如 `unhandledrejection` 事件。

总结

Promise 通过状态机和回调队列实现了异步操作的同步化表达,核心在于:

  1. 状态管理:确保结果的不可变性。
  2. 异步处理:保证回调执行的时机一致性。
  3. 链式调用:解决回调地狱问题。

理解 Promise 的实现原理,有助于更深入掌握 JavaScript 的异步编程模型,为使用更高级的异步特性(如 async/await)打下坚实基础。

前端必会:Promise 全解析,从原理到实战1. 从 "回调地狱" 到 Promise 在前端开发的异步编程领域,我们 - 掘金

Promise.allSettled和 Promise.any 得区别

Promise.allSettledPromise.any 是 ES2020 新增的两个 Promise 静态方法,它们的核心区别在于处理多个 Promise 时的触发条件、返回结果和适用场景

1. 触发条件不同

  • Promise.allSettled(promises) 等待所有传入的 Promise 都 "settle" (即所有 Promise 都完成,无论成功 fulfilled 还是失败 rejected),才会返回一个成功的 Promise。 它不会因为任何一个 Promise 失败而提前结束,必须等全部完成。
  • Promise.any(promises) 只要有一个传入的 Promise 成功 fulfilled ,就会立即返回这个成功的结果(只返回第一个成功的值)。 只有当所有传入的 Promise 都失败 rejected 时,才会返回一个失败的 Promise(包含所有错误信息)。

2. 返回结果不同

  • Promise.allSettled 的返回值 始终返回一个成功的 Promise ,其结果是一个数组,数组中的每个元素对应传入的每个 Promise 的 "结算信息":
    • 对于成功的 Promise:{ status: "fulfilled", value: 成功值 }
    • 对于失败的 Promise:{ status: "rejected", reason: 错误原因 }
  • Promise.any 的返回值
    • 若有一个 Promise 成功:返回一个成功的 Promise,结果是 "第一个成功的 Promise 的值"。
    • 若所有 Promise 失败:返回一个失败的 Promise ,结果是 AggregateError 实例(包含所有失败的错误信息)。

3. 代码示例对比

javascript 复制代码
// 定义几个测试用的 Promise
const p1 = Promise.resolve("成功1");
const p2 = Promise.reject("失败2");
const p3 = Promise.resolve("成功3");
const p4 = Promise.reject("失败4");


// 测试 Promise.allSettled
Promise.allSettled([p1, p2, p3, p4]).then(result => {
  console.log("allSettled 结果:", result);
  // 输出:
  // [
  //   { status: "fulfilled", value: "成功1" },
  //   { status: "rejected", reason: "失败2" },
  //   { status: "fulfilled", value: "成功3" },
  //   { status: "rejected", reason: "失败4" }
  // ]
});


// 测试 Promise.any
Promise.any([p1, p2, p3, p4]).then(
  value => console.log("any 成功结果:", value), // 输出:"any 成功结果:成功1"(第一个成功的p1)
  error => console.log("any 失败结果:", error)
);


// 测试所有 Promise 都失败的情况
Promise.any([p2, p4]).catch(error => {
  console.log("all rejected 时 any 的结果:", error); 
  // 输出:AggregateError: All promises were rejected
  console.log("错误列表:", error.errors); // 输出:["失败2", "失败4"]
});
  1. 适用场景不同
  • Promise.allSettled :适合需要知道所有异步操作的结果(无论成功失败) 的场景。 例如:批量提交表单后,需要统计成功提交了多少、失败了多少,并分别处理。
  • Promise.any :适合只要有一个异步操作成功即可,无需等待所有完成的场景。 例如:从多个镜像服务器加载同一资源,只要有一个服务器返回成功,就使用该资源(优先用最快成功的)。

总结表格

特性 Promise.allSettled Promise.any
触发条件 所有 Promise 都 settle(成功 / 失败) 第一个成功的 Promise 出现(或所有失败)
返回状态 始终成功(fulfilled) 有成功则成功,全失败则失败(rejected)
结果内容 包含所有 Promise 的状态和结果的数组 第一个成功的值,或包含所有错误的 AggregateError
核心用途 获取所有操作的完整结果 快速获取第一个可用的成功结果

普通函数和箭头函数的区别

场景 普通函数 箭头函数
需要动态 this ✅(如构造函数、对象方法) ❌(this 无法绑定)
作为构造函数 ✅(可用 new 调用) ❌(报错:不是构造函数)
需要 arguments 对象 ✅(内置 arguments ❌(需用剩余参数 ...args
简洁的回调函数 ❌(语法冗余) ✅(省略 functionreturn
定义生成器函数 ✅(function* ❌(不能使用 yield
需要 prototype ✅(默认有 prototype ❌(无 prototype

理解这些差异后,可根据具体场景灵活选择函数类型,避免因 this 指向或语法限制导致的错误。

水平垂直居中

在前端开发中,实现元素的水平垂直居中是一个常见需求。根据不同的场景和元素类型,可以采用多种方法。以下是一些常用的实现方式:

  1. 行内元素 / 文本(单行)

适用于文本、链接等行内元素,通过 text-alignline-height 实现:

css 复制代码
.parent {
  text-align: center;
  line-height: 200px; /* 等于容器高度 */
  height: 200px;
}
  1. 行内元素 / 文本(多行)

使用 Flexbox 或 Grid:

css 复制代码
.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}
  1. 块级元素(已知宽高)

使用绝对定位和负边距:

css 复制代码
.parent {
  position: relative;
}
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100px;
  height: 100px;
  margin-top: -50px; /* 高度的一半 */
  margin-left: -50px; /* 宽度的一半 */
}
  1. 块级元素(未知宽高)

使用绝对定位和 transform

css 复制代码
.parent {
  position: relative;
}
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
  1. Flexbox(现代方案)

适用于大多数场景,简洁高效:

css 复制代码
.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}
  1. Grid(现代方案)

更强大的二维布局:

css 复制代码
.parent {
  display: grid;
  place-items: center;
}
  1. 表格布局

使用 display: table-cell

css 复制代码
.parent {
  display: table-cell;
  text-align: center;
  vertical-align: middle;
}
  1. 绝对定位 + 自适应

使用 top/left/bottom/rightmargin: auto

css 复制代码
.parent {
  position: relative;
}
.child {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto; /* 关键 */
  width: 200px; /* 需指定宽高 */
  height: 100px;
}

总结

  • 推荐方案:优先使用 Flexbox 或 Grid,代码简洁且兼容性良好。
  • 兼容性:若需支持旧版浏览器(如 IE9-),可选用绝对定位方案。
  • 响应式 :使用 transformflex/grid 可更好地适应不同尺寸。

根据具体场景选择合适的方法,能有效提升开发效率和布局稳定性。

CSS

常见的选择器有哪些

  1. 基础选择器
    • 元素选择器:通过 HTML 标签名匹配元素,语法:标签名 { ... }(例:p { color: red; }
    • ID 选择器:通过元素的id属性匹配唯一元素,语法:#id值 { ... }(例:#header { width: 100%; }
    • 类选择器:通过元素的class属性匹配多个元素,语法:.类名 { ... }(例:.active { background: blue; }
    • 通配符选择器:匹配所有元素,语法:* { ... }(例:* { margin: 0; padding: 0; }
    • 属性选择器:通过元素属性匹配,常见形式:[attr](含 attr 属性)、[attr=value](attr 值为 value)、[attr^=value](attr 值以 value 开头)等(例:[type="text"] { border: 1px solid; }
  2. 组合选择器
    • 后代选择器:匹配父元素内的所有后代元素,语法:父选择器 后代选择器 { ... }(例:ul li { list-style: none; }
    • 子元素选择器:匹配父元素的直接子元素,语法:父选择器 > 子选择器 { ... }(例:div > p { font-size: 16px; }
    • 相邻兄弟选择器:匹配某元素后紧邻的同级元素,语法:元素 + 相邻元素 { ... }(例:h1 + p { margin-top: 10px; }
    • 通用兄弟选择器:匹配某元素后所有同级元素,语法:元素 ~ 兄弟元素 { ... }(例:h2 ~ span { color: gray; }
  3. 伪类选择器
    • 链接伪类::link(未访问链接)、:visited(已访问链接)
    • 交互伪类::hover(鼠标悬停)、:active(元素激活)、:focus(元素获焦)
    • 结构伪类::first-child(第一个子元素)、:last-child(最后一个子元素)、:nth-child(n)(第 n 个子元素)、:only-child(唯一子元素)
    • 状态伪类::checked(选中的表单元素)、:disabled(禁用的表单元素)
    • 否定伪类::not(选择器)(排除匹配选择器的元素,例::not(.active) { opacity: 0.5; }
  4. 伪元素选择器
    • ::before:在元素内容前插入内容
    • ::after:在元素内容后插入内容
    • ::first-letter:匹配文本首字母
    • ::first-line:匹配文本首行
    • ::selection:匹配用户选中的文本(例:::selection { background: yellow; }

选择器的权重

CSS 选择器的权重(Specificity)决定了样式的优先级,权重值越高,优先级越高。权重通常用 "(ID 数,类 / 伪类 / 属性数,元素 / 伪元素数)" 的形式表示,具体对应规则如下:

  1. 基础选择器
  • 元素选择器 (如 pdiv):权重 (0, 0, 1)
  • ID 选择器 (如 #header):权重 (1, 0, 0)(优先级最高的基础选择器)
  • 类选择器 (如 .active):权重 (0, 1, 0)
  • 通配符选择器*):权重 (0, 0, 0)(无优先级,低于所有具体选择器)
  • 属性选择器 (如 [type="text"][class^="btn"]):权重 (0, 1, 0)(与类选择器同级)
  1. 组合选择器

组合选择器的权重为组成它的所有单个选择器权重之和

  • 后代选择器 (如 ul li):元素选择器 + 元素选择器(0, 0, 1+1) = (0, 0, 2)
  • 子元素选择器 (如 div > p):元素 + 元素(0, 0, 2)
  • 相邻兄弟选择器 (如 h1 + p):元素 + 元素(0, 0, 2)
  • 通用兄弟选择器 (如 h2 ~ span):元素 + 元素(0, 0, 2)
  • 复杂组合示例(如 #nav .item > a):ID + 类 + 元素(1, 1, 1)

3. 伪类选择器

所有伪类选择器权重与类选择器一致:

  • :hover:first-child:checked:not(...) 等:权重 (0, 1, 0) 示例:a:hover元素 + 伪类(0, 1, 1)

4. 伪元素选择器

所有伪元素选择器权重与元素选择器一致:

  • ::before::first-letter::selection 等:权重 (0, 0, 1) 示例:p::first-line元素 + 伪元素(0, 0, 2)

补充说明

  • 权重计算只累加对应类别数量,不进位(如 11个类选择器 权重为 (0, 11, 0),仍低于 1个ID选择器 (1, 0, 0))。

  • 内联样式(style="...")权重为 (10, 0, 0)(即 1000),高于所有选择器;!important 优先级最高(强制覆盖,除非另一个 !important 权重更高)。

flex:1表示什么?

flex-grow: 1****flex-shrink: 1 flex-basis: 0%

margin 0 0 0 0表示什么等价margin 0 0 0 自动补充

margin: 0 0 0 0 的含义是:将元素四个方向的外边距都设置为 0,即元素与周围元素 / 容器在上下左右四个方向上都没有额外的间距。

margin 合并问题如何解决

  • 改变元素的 display 类型(如 inline-blockflex);

  • 触发 BFC(如 overflow: hiddendisplay: flex);

  • 用 border 或 padding 隔离 margin;

  • 添加隔离元素阻断相邻关系。

BFC

BFC(Block Formatting Context,块级格式化上下文)是 CSS 渲染页面时的一种布局模式,它决定了元素如何对其内容进行布局,以及与其他元素的关系。理解 BFC 是掌握 CSS 布局的关键之一,尤其是解决一些常见的布局问题。

一、基础概念

  1. 什么是 BFC? BFC 是页面上的一个独立渲染区域,区域内的元素布局不会影响区域外的元素。它有自己的布局规则,例如内部的子元素垂直排列,间距由 margin 决定,且不会与外部元素发生布局冲突。
  2. BFC 的创建条件 满足以下任一条件即可触发 BFC:
    • 根元素 <html>
    • 浮动元素(float 不为 none)。
    • 绝对定位元素(position: absolute/fixed)。
    • display: inline-block/table-cell/flex/grid 等。
    • overflow 不为 visible 的块元素(如 overflow: hidden/auto)。

二、BFC 的核心作用

1. 解决外边距折叠(Margin Collapse)

  • 问题:相邻块级元素的上下外边距会折叠(合并为一个值)。

  • BFC 的解决方案:将其中一个元素包裹在 BFC 容器中,使其内外边距不再折叠。

    html 复制代码
    <div class="container">
      <div class="child"></div>
    </div>
    <div class="bfc-container" style="overflow: hidden;">
      <div class="child"></div>
    </div>

2. 清除浮动(Containing Floats)

  • 问题:父元素高度塌陷(子元素浮动后脱离文档流,父元素高度为 0)。

  • BFC 的解决方案:触发父元素的 BFC,使其包裹浮动子元素。

    css 复制代码
    .parent {
      overflow: hidden; /* 触发 BFC */
    }

3. 阻止元素被浮动覆盖

  • 问题:浮动元素会脱离文档流,覆盖后续的非浮动元素。

  • BFC 的解决方案:为被覆盖元素创建 BFC,使其与浮动元素隔离。

    css 复制代码
    .non-float-element {
      overflow: hidden; /* 触发 BFC,不再被浮动元素覆盖 */
    }

4. 自适应两栏/三栏布局

  • 问题:实现一侧固定宽度、另一侧自适应的布局。

  • BFC 的解决方案:利用 BFC 区域不与浮动元素重叠的特性。

    css 复制代码
    .left {
      float: left;
      width: 200px;
    }
    .right {
      overflow: hidden; /* 触发 BFC,自适应剩余宽度 */
    }

三、深度解析 BFC 的原理

  1. BFC 的布局规则
    • 内部的 Box 垂直排列,间距由 margin 决定。
    • BFC 的区域不会与浮动元素重叠。
    • BFC 内外的布局互相独立。
    • 计算 BFC 高度时,浮动元素也参与计算。
  2. BFC 与文档流的关系 BFC 是页面文档流中的一个独立容器,其内部布局遵循普通流,但与外部的元素隔离。这种隔离性使得 BFC 可以避免外部浮动、外边距折叠等问题。
  3. BFC 的渲染机制 浏览器在渲染时,会为每个 BFC 分配一个独立的布局上下文。这个上下文决定了元素如何定位、尺寸计算及与其他元素的关系。例如,BFC 容器会阻止浮动元素溢出到容器外部。

四、实际开发中的应用场景

  1. 避免浮动导致的布局错乱 通过触发父元素的 BFC,确保父容器正确包含浮动子元素。
  2. 实现复杂布局 结合浮动和 BFC 实现自适应布局,无需依赖现代布局方案(如 Flexbox/Grid)。
  3. 隔离第三方组件样式 在组件外层创建 BFC,防止组件内外样式相互干扰(如外边距折叠)。

五、注意事项

  1. BFC 的副作用
    • 使用 overflow: hidden 可能导致内容被裁剪或出现滚动条。
    • 某些触发方式(如 float)会改变元素的显示模式。
  2. BFC 与现代布局方案的对比
    • Flexbox 和 Grid 提供了更直观的布局方式,但在某些场景(如清除浮动)中,BFC 仍是简单有效的方案。

六、总结

BFC 是 CSS 布局的底层机制之一,它通过隔离渲染区域解决外边距折叠、浮动塌陷等问题,同时为复杂布局提供基础支持。虽然现代布局方案(Flexbox/Grid)简化了部分场景,但理解 BFC 仍有助于开发者更彻底地掌握 CSS 布局原理,写出更健壮的代码。

flex属性

SS Flexbox 是现代前端开发中用于一维布局的核心技术,其属性分为 容器属性 (控制整体布局)和 项目属性(控制子项行为)。以下是详细的分类和解释,从基础到深入:

一、Flex 容器属性

  1. display: flex | inline-flex

    • 作用:将元素定义为 Flex 容器,子元素成为 Flex 项目。
    • 区别
      • flex:容器表现为块级元素。
      • inline-flex:容器表现为行内元素,内部仍为 Flex 布局。
  2. flex-direction

    • 作用:定义主轴方向(项目的排列方向)。
      • row(默认):水平方向,从左到右。
      • row-reverse:水平方向,从右到左。
      • column:垂直方向,从上到下。
      • column-reverse:垂直方向,从下到上。
  3. flex-wrap

    • 作用:控制项目是否换行。
      • nowrap(默认):不换行,项目可能溢出。
      • wrap:换行,第一行在上方。
      • wrap-reverse:换行,第一行在下方。
  4. flex-flow

    • 作用flex-directionflex-wrap 的简写。
    • 语法flex-flow: <flex-direction> <flex-wrap>
    • 示例flex-flow: row wrap;
  5. justify-content

    • 作用:定义项目在主轴上的对齐方式。
      • flex-start(默认):左对齐。
      • flex-end:右对齐。
      • center:居中。
      • space-between:两端对齐,项目间隔相等。
      • space-around:项目两侧间隔相等。
      • space-evenly:所有间隔完全相等(包括边缘)。
  6. align-items

    • 作用:定义项目在交叉轴上的对齐方式。
      • stretch(默认):拉伸填满容器高度。
      • flex-start:顶部对齐。
      • flex-end:底部对齐。
      • center:垂直居中。
      • baseline:按基线对齐。
  7. align-content

    • 作用 :多行项目在交叉轴上的对齐方式(需 flex-wrap: wrap)。
    • :与 justify-content 类似,如 flex-startcenterspace-between 等。

二、Flex 项目属性

  1. order

    • 作用:定义项目的排列顺序,数值越小越靠前。
    • :整数(默认 0)。
    • 示例order: -1; 使项目提前。
  2. flex-grow

    • 作用:定义项目的放大比例(当容器有剩余空间时)。
    • :非负整数(默认 0,不放大)。
    • 示例flex-grow: 2; 表示占据剩余空间的比例是其他项目的两倍。
  3. flex-shrink

    • 作用:定义项目的缩小比例(当容器空间不足时)。
    • :非负整数(默认 1,允许缩小)。
    • 示例flex-shrink: 0; 禁止项目缩小。
  4. flex-basis

    • 作用:定义项目在分配多余空间前的初始大小。
    • :长度(如 200px)或 auto(默认,基于内容计算)。
    • 注意 :优先级高于 width
  5. flex

    • 作用flex-growflex-shrinkflex-basis 的简写。
    • 语法flex: <flex-grow> <flex-shrink> <flex-basis>
    • 常用简写
      • flex: 11 1 0(占满剩余空间)。
      • flex: auto1 1 auto(基于内容伸缩)。
      • flex: none0 0 auto(固定大小,不伸缩)。
  6. align-self

    • 作用 :覆盖容器的 align-items,定义单个项目的交叉轴对齐方式。
    • auto(默认继承容器)、stretchflex-startflex-endcenterbaseline

三、深入理解

  1. 空间分配逻辑

    • 剩余空间 :容器大小减去所有项目的 flex-basis 或固定大小后的空间。
    • 放大 :按 flex-grow 比例分配剩余空间。
    • 缩小 :按 flex-shrink 比例压缩超出容器的空间。
  2. flex-basiswidth 的关系

    • flex-direction: row 时,flex-basis 控制宽度;
    • flex-direction: column 时,flex-basis 控制高度。
    • 若同时设置 flex-basiswidthflex-basis 优先级更高。
  3. 负空间的压缩

    • flex-shrink: 0,项目不会缩小,可能导致溢出。

    • 实际压缩量计算公式:

      css 复制代码
      项目压缩量 = (负空间 * flex-shrink值) / 所有项目的 (flex-shrink值 * flex-basis值) 总和  
  4. 浏览器兼容性

    • 现代浏览器全面支持 Flexbox,但旧版浏览器(如 IE 10/11)需加前缀 -ms-
    • 使用 Autoprefixer 工具自动处理兼容性。

四、最佳实践

  1. 优先使用 flex 简写

    css 复制代码
    .item {
      flex: 1; /* 等价于 flex: 1 1 0 */`flex-grow`、`flex-shrink`、`flex-basis`
    }
  2. 固定侧边栏 + 自适应内容布局

    css 复制代码
    .sidebar { flex: 0 0 200px; }  /* 固定宽度 */
    .content { flex: 1; }          /* 占满剩余空间 */
  3. 等高布局

    css 复制代码
    .container {
      display: flex;
      align-items: stretch; /* 默认值,项目高度一致 */
    }
  4. 响应式导航栏

    css 复制代码
    .nav {
      display: flex;
      flex-wrap: wrap;      /* 小屏幕自动换行 */
      justify-content: space-between;
    }

总结

  • 核心思想:通过容器和项目的属性组合,实现灵活的空间分配和对齐。
  • 适用场景:一维布局(如导航栏、卡片列表、表单控件等)。
  • 进阶方向:结合 CSS Grid(二维布局)和媒体查询,构建复杂响应式设计。

掌握这些属性后,可以高效解决大多数布局问题,减少对浮动(float)和定位(position)的依赖。

网格布局

css 复制代码
 grid-template-columns: repeat(3, 1fr); /* 三列等分 */

1fr 代表网格容器中 "剩余空间的一份"。当为网格的列(grid-template-columns)或行(grid-template-rows)设置 fr 单位时,浏览器会先扣除固定尺寸(如 px% 等)占用的空间,再将剩余的可用空间按 fr 的比例分配给对应轨道。

display属性

一、基础常用值

  1. block

    • 元素生成块级盒子,独占一行,可设置宽高、内外边距。
    • 典型元素:<div>, <p>, <h1>-<h6>
    • 示例:display: block;
  2. inline

    • 元素生成行内盒子,不独占一行,不可直接设置宽高,大小由内容决定。
    • 典型元素:<span>, <a>, <strong>
    • 示例:display: inline;
  3. inline-block

    • 混合特性:行内排列,但可设置宽高、内外边距。
    • 典型应用:导航按钮、图标与文字混排。
    • 示例:display: inline-block;
  4. none

    • 元素不渲染,完全从文档流中移除,不占据空间。
    • visibility: hidden 的区别:后者隐藏元素但保留空间。
    • 示例:display: none;

二、布局相关值

  1. flex

    • 启用弹性盒子布局(一维布局),子元素成为弹性项。
    • 控制属性:flex-direction, justify-content, align-items 等。
    • 示例:display: flex;
  2. grid

    • 启用网格布局(二维布局),子元素按网格行列排列。
    • 控制属性:grid-template-columns, grid-gap, grid-area 等。
    • 示例:display: grid;
  3. inline-flex / inline-grid

    • 行内版本的弹性或网格容器,外部表现为行内元素,内部按弹性/网格布局。
    • 示例:display: inline-flex;

三、传统布局模型

  1. table 系列

    • 模拟表格布局(逐渐被 Flex/Grid 替代):
      • table:行为类似 <table>
      • table-row:类似 <tr>
      • table-cell:类似 <td>,常用于垂直居中。
    • 示例:display: table-cell;
  2. list-item

    • 元素表现为列表项(如 <li>),生成标记(如圆点)。
    • 示例:display: list-item;

四、特殊场景值

  1. contents

    • 元素自身不生成盒子,子元素直接继承父级布局(类似"溶解"自身)。
    • 注意:可能影响可访问性(屏幕阅读器会忽略该元素)。
    • 示例:display: contents;
  2. flow-root

    • 创建块级格式化上下文(BFC),避免外边距合并或浮动塌陷。
    • 类似 overflow: hidden 但更语义化。
    • 示例:display: flow-root;
  3. run-in

    • 根据上下文决定表现为块级或行内元素(浏览器支持有限)。
    • 示例:display: run-in;

五、实验性或特定用途值

  1. ruby 系列

    • 用于东亚文字排版(如注音):
      • ruby:定义 ruby 容器。
      • ruby-text:定义注音文本。
    • 示例:display: ruby-text;
  2. subgrid

    • 网格布局的子网格(CSS Grid Level 2 特性,部分浏览器支持)。
    • 允许子网格继承父网格的轨道定义。
    • 示例:display: subgrid;

六、全局值

  • inherit :继承父元素的 display 值。
  • initial :重置为默认值(通常是 inline)。
  • unset :根据属性是否可继承,表现为 inheritinitial

总结与选择建议

  • 基础布局 :优先使用 blockinlineinline-block
  • 现代布局 :首选 flex(一维)和 grid(二维)。
  • 隐藏元素 :用 none 完全移除,或用 opacity: 0 保留交互。
  • 兼容性 :传统布局(如 table)适用于旧项目,新项目推荐 Flex/Grid。

通过灵活组合这些值,可以实现从简单到复杂的响应式布局设计。

postion属性

一、CSS定位基础

CSS的position属性定义了元素的定位模式,主要类型包括:

  1. static:默认值,元素在正常文档流中。
  2. relative:相对自身原始位置偏移,不脱离文档流。
  3. absolute :相对于最近的定位祖先元素 (非static)绝对定位,脱离文档流。
  4. fixed:相对于**视口(viewport)**定位,脱离文档流,不受滚动影响。
  5. sticky :混合模式,滚动到阈值前表现为relative,之后表现为fixed

使用场景

  • absolute:悬浮菜单、弹窗。
  • fixed:导航栏、广告。
  • sticky:表格标题、侧边栏。

二、浏览器如何实现定位

浏览器的渲染引擎(如Blink、WebKit)通过以下流程处理定位:

  1. 布局阶段(Layout/Reflow)
  • static/relative:参与正常文档流布局,通过盒模型计算位置。
  • absolute/fixed:脱离文档流,单独计算位置,减少父容器回流影响。
  • sticky:动态切换布局模式,需持续监听滚动事件。
  1. 分层与合成(Composite)
  • 提升为合成层fixedsticky或使用transform的元素会被提升为独立的合成层(Composite Layer),由GPU处理。
  • GPU加速 :合成层通过纹理上传到GPU,滚动时仅变换位置(translate),无需重绘(Repaint)。
  1. 滚动处理
  • 主线程与合成线程协作
    • 主线程:处理JavaScript、样式计算、布局。
    • 合成线程:管理图层合成,直接响应滚动事件,避免阻塞主线程。

三、操作系统底层协作

操作系统通过以下方式支持浏览器渲染:

  1. 图形接口与硬件加速
  • GPU资源管理:操作系统(如Windows的DirectX,macOS的Metal)提供图形API,浏览器通过它们调用GPU进行图层合成。
  • 垂直同步(VSync):操作系统协调GPU渲染帧率与显示器刷新率,避免画面撕裂。
  1. 进程调度
  • 多进程架构 :浏览器(如Chrome)将渲染任务分配给渲染进程 ,合成任务由GPU进程处理,操作系统调度这些进程的CPU/GPU资源。
  • 输入事件处理:操作系统将滚动、触摸事件传递给浏览器进程,再路由到合成线程。
  1. 内存管理
  • 纹理内存:GPU显存由操作系统分配,浏览器将合成层纹理存储在显存中,提升渲染效率。

四、深度思考:性能优化与挑战

  1. 层爆炸(Layer Explosion)
    • 过度使用absolute/fixed可能导致过多合成层,占用GPU内存。
    • 优化:使用will-change谨慎提示浏览器分层。
  2. 滚动性能
    • sticky依赖主线程计算阈值,复杂页面可能卡顿。
    • 优化:使用position: fixed + JavaScript手动控制。
  3. 跨平台差异
    • 移动端视口受软键盘影响,fixed定位可能失效。
    • 解决方案:使用position: absolute + 动态视口高度(vh单位)。

五、总结

  • 前端定位:通过CSS定义元素的布局模式,核心是控制文档流与坐标系。
  • 浏览器实现:依赖渲染引擎的分层、合成机制,结合GPU加速提升性能。
  • 操作系统角色:提供图形接口、进程调度和硬件资源管理,确保高效渲染。

理解这一链条有助于开发高性能Web应用,尤其是在复杂交互和动画场景中。

CSS三大特性、盒子模型

实现响应式自适应方案

rem与px怎么做转换

rempx 是两种常用的长度单位,它们的转换关系与 根元素(html 标签)的字体大小 直接相关。核心转换公式

rem(Root EM)是相对单位,其基准值为 根元素(html)的 font-size。转换公式如下:

plaintext 复制代码
1rem = 根元素的 font-size(单位:px)
目标 px 值 = 目标 rem 值 × 根元素 font-size(px)
目标 rem 值 = 目标 px 值 ÷ 根元素 font-size(px

浏览器渲染原理

记忆法:HTML 解析与 DOM 树构建=>CSS 解析与 CSSOM 树构建=>渲染树(Render Tree)构建(合成)=>布局=>绘制=>合成(GPU加速渲染)=>GUI线程

浏览器渲染页面是一个将 HTML、CSS、JavaScript 转化为可视化界面的过程,核心分为以下 5 个阶段,且通常按顺序执行(现代浏览器会优化为流水线并行处理):

  1. HTML 解析与 DOM 树构建

    • 浏览器解析 HTML 标签,生成 DOM(文档对象模型)树,每个标签对应树中的一个节点,描述页面的结构和内容。
  2. CSS 解析与 CSSOM 树构建

    • 解析 CSS 样式(包括内联、嵌入、外部样式),生成 CSSOM(CSS 对象模型)树,记录每个元素的样式规则(如颜色、尺寸、位置等)。
  3. 渲染树(Render Tree)构建

    • 结合 DOM 树和 CSSOM 树,生成

      渲染树:

      • 只包含可见元素(忽略 display: none 的元素、<head> 等无视觉效果的节点)。
      • 每个节点包含 DOM 信息和对应的样式信息,用于后续布局和绘制。
  4. 布局(Layout/Reflow)

    • 根据渲染树计算每个元素的

      几何信息

      (位置、尺寸、大小等),例如:

      • 确定元素在页面中的坐标(如 top: 10px)、宽高(如 width: 200px)。
      • 父元素的布局会影响子元素(如 padding 会改变子元素位置)。
    • 此阶段是计算元素位置和大小的关键步骤,耗时与元素数量正相关。

  5. 绘制(Paint/Repaint)

    • 根据布局结果,将元素的样式(如颜色、背景、阴影)绘制到屏幕上,生成像素点。
    • 绘制可按 "层" 进行(如 z-index 较高的元素单独成层),现代浏览器通过 合成层(Compositing Layers) 优化性能。
  6. 合成(Compositing)

    • 将多个绘制层合并为最终屏幕图像,处理层间关系(如重叠、透明度),并交给 GPU 加速渲染(避免 CPU 瓶颈)。

html

Html和html5的区别

总结对比表

特性 HTML HTML5
语义化标签 缺乏,依赖<div> 丰富的语义标签(<header>等)
多媒体支持 依赖插件(如 Flash) 原生<video><audio>
本地存储 Cookie(4KB 限制) localStorage/sessionStorage
表单增强 基本输入类型 丰富的输入类型和属性
绘图能力 依赖图片或插件 Canvas/SVG
离线支持 AppCache/Service Worker
文档类型声明 复杂 <!DOCTYPE html>
浏览器兼容性 全兼容 现代浏览器(IE9 + 部分支持)

何时使用 HTML5?

  • 新项目或重构旧项目时
  • 无需支持 IE9 及以下版本
  • 需要使用多媒体、离线应用、地理定位等现代功能
  • 注重代码可维护性和 SEO 优化

HTML5 是现代 Web 开发的标准,提供了更强大、更简洁的功能,推荐优先使用。对于需要兼容旧浏览器的场景,可以结合 Polyfill 和降级方案使用。

盒子模型有哪些,特点

在前端开发中,盒子模型(Box Model)是布局的基础概念,它描述了元素在页面中所占空间的计算方式。以下是关于盒子模型的详细介绍:

1. 标准盒子模型(W3C 盒子模型)

特点

  • 内容区(content) :元素实际显示的内容(文本、图片等),由widthheight属性定义。
  • 内边距(padding):内容区与边框之间的距离,会增加元素的整体尺寸。
  • 边框(border) :围绕内容区和内边距的线条,宽度由border-width定义。
  • 外边距(margin):元素与其他元素之间的距离,不影响元素自身尺寸,但影响元素在页面中的位置。

宽度计算

plaintext 复制代码
总宽度 = width + padding-left + padding-right + border-left + border-right
总高度 = height + padding-top + padding-bottom + border-top + border-bottom

示例代码

css 复制代码
.box {
  width: 200px;       /* 内容区宽度 */
  padding: 10px;      /* 内边距:上下左右各10px */
  border: 2px solid;  /* 边框:宽度2px */
  margin: 15px;       /* 外边距:上下左右各15px */
}

/* 实际占用宽度 = 200 + 10*2 + 2*2 = 224px */

2. IE 盒子模型(怪异盒子模型)

特点

  • 内容区(content) :元素的内容区宽度包含了paddingborder,但不包含margin
  • 内边距(padding)和边框(border) :包含在widthheight属性内,不会额外增加元素尺寸。
  • 外边距(margin):与标准模型相同,不影响元素自身尺寸。

宽度计算

plaintext 复制代码
总宽度 = width(包含padding和border) + margin-left + margin-right
总高度 = height(包含padding和border) + margin-top + margin-bottom

示例代码

css 复制代码
.box {
  width: 200px;       /* 内容区+内边距+边框的总宽度 */
  padding: 10px;      /* 内边距:上下左右各10px */
  border: 2px solid;  /* 边框:宽度2px */
  margin: 15px;       /* 外边距:上下左右各15px */
}

/* 内容区实际宽度 = 200 - 10*2 - 2*2 = 176px */

3. 盒子模型切换(box-sizing 属性)

  • 标准模型(默认值)

    css 复制代码
    box-sizing: content-box;
  • IE 模型(怪异模型)

    css 复制代码
    box-sizing: border-box;

应用场景

  • 响应式布局 :使用border-box可避免因内边距导致布局溢出。
  • 统一设计 :所有元素设置box-sizing: border-box可简化尺寸计算。
css 复制代码
/* 全局设置所有元素使用IE模型 */
* {
  box-sizing: border-box;
}

4. 其他盒子模型相关概念

4.1 行内元素盒子模型

  • 内联元素(如<span><a>

    • widthheight无效,由内容撑开。
    • paddingborder水平方向有效(影响布局),垂直方向不影响布局。
    • margin水平方向有效,垂直方向可能无效。

4.2 替换元素(如<img><input>

  • 有固有尺寸,widthheight可控制。
  • 盒子模型规则与块级元素相同。

4.3 外边距合并(Margin Collapsing)

  • 相邻块级元素的垂直外边距会合并为较大的一个。
  • 父子元素之间也可能发生外边距合并。

5. 对比总结

特性 标准盒子模型 (content-box) IE 盒子模型 (border-box)
width包含内容 仅内容区 内容区 + 内边距 + 边框
尺寸计算 width + padding + border width(已包含)
布局控制 需额外计算 padding 和 border 更直观,不易溢出
应用场景 默认值,适合简单布局 响应式设计、复杂布局

6. 最佳实践

  1. 使用 border-box

    css 复制代码
    * {
      box-sizing: border-box;
    }

    简化尺寸计算,减少布局错误。

  2. 避免内外边距混用 : 使用padding控制元素内部空间,margin控制元素间距离。

  3. 注意行内元素特性 : 行内元素的paddingborder可能影响布局但不占空间。

理解盒子模型是掌握 CSS 布局的基础,通过合理使用box-sizing和控制内外边距,可以更高效地实现各种复杂布局。

前端不同页面之间怎么通信

对比与选择建议

方案 数据量 跨域 实时性 实现难度 适用场景
URL 参数 支持 同步 简单 一次性数据传递
localStorage 需刷新 简单 持久化数据共享
postMessage 支持 异步 中等 跨窗口(源要相同) /iframe 通信
WebSocket 不限 支持 实时 复杂 实时双向通信
IndexedDB 异步 复杂 大量数据存储与共享
Service Worker 实时 复杂 离线消息和跨标签通信
Broadcast Channel 实时 简单 同源页面间广播

注意事项

  1. 安全性:敏感数据需加密处理(如 JWT 签名)。
  2. 兼容性 :IE 等旧浏览器可能不支持部分 API(如BroadcastChannel)。
  3. 性能 :避免频繁操作localStorage,可能导致页面卡顿。
  4. 内存管理WebSocketService Worker需正确关闭连接,防止内存泄漏。

TS

infer得作用

infer 用于在条件类型中提取类型信息(类似变量声明)。

interface与type的区别

在 TypeScript 中,interfacetype 都用于描述类型,但它们在语法和功能上存在一些关键区别。以下是它们的主要差异和适用场景:

  1. 基本语法与定义方式
  • interface:仅用于定义对象类型或类的接口,语法更简洁,专注于结构描述。

    typescript 复制代码
    interface User {
      name: string;
      age: number;
    }
    
    // 描述函数类型
    interface SayHello {
      (name: string): string;
    }
  • type:可定义任何类型(对象、基本类型、联合类型、交叉类型等),适用范围更广。

    typescript

    typescript 复制代码
    // 对象类型
    type User = {
      name: string;
      age: number;
    };
    
    // 基本类型别名
    type ID = string | number;
    
    // 联合类型
    type Status = 'active' | 'inactive' | 'pending';
    
    // 交叉类型
    type A = { x: number };
    type B = { y: string };
    type C = A & B; // { x: number; y: string }
  1. 扩展(继承)方式
  • interface :通过 extends 关键字扩展,支持多继承。

    typescript 复制代码
    interface Animal {
      name: string;
    }
    
    interface Dog extends Animal {
      bark(): void;
    }
    
    // 多继承
    interface Pet extends Animal, Dog {
      owner: string;
    }
  • type :通过交叉类型(&)实现扩展,不支持 extends 关键字。

    typescript 复制代码
    type Animal = {
      name: string;
    };
    
    type Dog = Animal & {
      bark(): void;
    };
  1. 合并声明(Declaration Merging)
  • interface:支持同名接口自动合并,属性和方法会被合并为一个接口。

    typescript 复制代码
    interface User {
      name: string;
    }
    
    interface User {
      age: number;
    }
    
    // 合并后:{ name: string; age: number }
    const user: User = { name: 'Alice', age: 20 };
  • type :不支持合并,同名 type 会报错。

    typescript 复制代码
    type User = { name: string };
    type User = { age: number }; // 报错:标识符"User"重复
  1. 其他关键差异

| 特性 | interface | type | |
|-----------|-------------------------|-----------------------------------------|--------|---|
| 支持的类型 | 仅对象、函数、类接口 | 所有类型(包括基本类型、联合类型等) | |
| 计算属性 | 不支持 | 支持(如 `type Key = 'a' | 'b'`) | |
| 与类的结合 | 可通过 implements 约束类的结构 | 也可通过 implements 使用,但不如 interface 直观 | |
| 声明提升 | 完全支持(类似变量声明提升) | 部分支持(取决于具体场景) | |

  1. 适用场景建议
  • 优先使用 interface
    • 定义对象的结构(如 API 响应、配置项)。
    • 需要继承或被继承的类型。
    • 希望支持声明合并(如扩展第三方库的类型)。
    • 描述类的接口(配合 implements)。
  • 优先使用 type
    • 定义基本类型别名(如 type ID = string)。
    • 定义联合类型(type Status = 'on' | 'off')或交叉类型。
    • 使用映射类型(type Readonly<T> = { readonly [P in keyof T]: T[P] })。
    • 定义元组类型(type Point = [number, number])。

总结

interface 更适合描述对象结构和接口继承,强调 "契约";type 更灵活,可描述任何类型,适合复杂类型组合。实际开发中两者可以混用,但保持一致性更重要。

如何交叉

&

如何取值

根据不同场景选择合适的方式:

  • 从对象类型中挑选属性Pick

  • 从对象类型中排除属性Omit

  • 从联合类型中保留特定类型Extract

  • 从联合类型中排除特定类型Exclude

  • 从数组 / 元组中提取元素类型 → 索引访问(T[number]

  • 根据不同场景选择合适的方式:

    • 从对象类型中挑选属性Pick

    • 从对象类型中排除属性Omit

    • 从联合类型中保留特定类型Extract

    • 从联合类型中排除特定类型Exclude

    • 从数组 / 元组中提取元素类型 → 索引访问(T[number]

ts 高级

TypeScript(TS)的高级特性是提升代码类型安全性、可维护性和复用性的关键。以下是一些核心高级特性的解析与示例:

一、泛型(Generics)

泛型用于创建可复用的组件,支持多种类型而不丢失类型信息。

基本用法

typescript 复制代码
// 泛型函数:接收任意类型并返回相同类型
function identity<T>(arg: T): T {
  return arg;
}

// 自动推断类型
const num = identity(123); // 类型:number
const str = identity("hello"); // 类型:string

// 显式指定类型
const bool = identity<boolean>(true); // 类型:boolean

泛型约束(限制泛型范围)

typescript 复制代码
// 约束泛型必须有length属性
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 合法,因为T被约束为有length
  return arg;
}

logLength("abc"); // 正确(string有length)
logLength([1, 2, 3]); // 正确(array有length)
logLength(123); // 错误(number无length)

泛型接口 / 类

typescript 复制代码
// 泛型接口
interface Container<T> {
  value: T;
  getValue: () => T;
}

const stringContainer: Container<string> = {
  value: "test",
  getValue: () => "test",
};

// 泛型类
class Queue<T> {
  private data: T[] = [];
  push(item: T) { this.data.push(item); }
  pop(): T | undefined { return this.data.shift(); }
}

const numberQueue = new Queue<number>();
numberQueue.push(1);

二、高级类型

1. 联合类型(Union Types)

表示多个类型中的一个 ,用 | 分隔。

typescript 复制代码
type ID = string | number;

function printID(id: ID) {
  console.log(id);
}

printID("abc123"); // 正确
printID(456); // 正确

2. 交叉类型(Intersection Types)

表示合并多个类型 ,用 & 分隔(需同时满足所有类型)。

typescript 复制代码
type User = { name: string };
type Contact = { phone: string };

// 同时拥有User和Contact的属性
type UserWithContact = User & Contact;

const user: UserWithContact = {
  name: "Alice",
  phone: "123456"
};

3. 类型守卫(Type Guards)

缩小联合类型的范围,让 TS 更精确地推断类型。

typescript 复制代码
type Dog = { type: "dog"; bark: () => void };
type Cat = { type: "cat"; meow: () => void };
type Pet = Dog | Cat;

// 自定义类型守卫:判断是否为Dog
function isDog(pet: Pet): pet is Dog {
  return pet.type === "dog";
}

function makeSound(pet: Pet) {
  if (isDog(pet)) {
    pet.bark(); // 正确,TS知道此时pet是Dog
  } else {
    pet.meow(); // 正确,TS知道此时pet是Cat
  }
}

三、映射类型(Mapped Types)

通过遍历已有类型的属性 创建新类型(内置的如 PartialReadonly 等)。

内置映射类型

typescript 复制代码
interface Todo {
  title: string;
  content: string;
}

// Partial<T>:将所有属性变为可选
type PartialTodo = Partial<Todo>; 
// { title?: string; content?: string }

// Readonly<T>:将所有属性变为只读
type ReadonlyTodo = Readonly<Todo>;
// { readonly title: string; readonly content: string }

自定义映射类型

typescript 复制代码
// 将类型T的所有属性变为number类型
type ToNumber<T> = {
  [K in keyof T]: number; // K遍历T的所有属性(keyof T获取属性名联合类型)
};

interface Data {
  a: string;
  b: boolean;
}

type NumberData = ToNumber<Data>; 
// { a: number; b: number }

四、条件类型(Conditional Types)

类似三元表达式,根据条件返回不同类型,语法:T extends U ? X : Y

基本用法

typescript 复制代码
// 判断T是否是Array类型
type IsArray<T> = T extends Array<any> ? "yes" : "no";

type A = IsArray<number[]>; // "yes"
type B = IsArray<string>; // "no"

提取类型(配合 infer

infer 用于在条件类型中提取类型信息(类似变量声明)。

typescript 复制代码
// 提取数组元素类型
type ElementType<T> = T extends Array<infer E> ? E : T;

type Arr = number[];
type El = ElementType<Arr>; // number

// 提取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = () => string;
type FnReturn = ReturnType<Fn>; // string

五、装饰器(Decorators)

用于修改类、方法、属性的行为 (实验性特性,需开启 experimentalDecorators 配置)。

类装饰器

typescript 复制代码
// 装饰器工厂:返回一个装饰器函数
function logClass(prefix: string) {
  return function (target: any) {
    console.log(`${prefix}: 类被定义了`, target);
  };
}

@logClass("INFO") // 应用装饰器
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
// 输出:"INFO: 类被定义了 [class User]"

方法装饰器

typescript 复制代码
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  // 重写方法
  descriptor.value = function (...args: any[]) {
    console.log(`调用方法 ${propertyKey},参数:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`方法 ${propertyKey} 返回:`, result);
    return result;
  };
}

class Calculator {
  @logMethod
  add(a: number, b: number) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(1, 2); 
// 输出:
// "调用方法 add,参数: [1, 2]"
// "方法 add 返回: 3"

六、模块增强(Module Augmentation)

扩展已有模块的类型定义(例如给第三方库添加类型)。

typescript 复制代码
// 扩展内置Array类型
declare global {
  interface Array<T> {
    // 添加自定义方法
    sum(): number;
  }
}

// 实现方法
Array.prototype.sum = function () {
  return this.reduce((acc, val) => acc + (val as number), 0);
};

// 使用
const nums = [1, 2, 3];
console.log(nums.sum()); // 6

这些高级特性是 TS 的核心竞争力,合理使用可以大幅提升代码的健壮性和开发效率。实际开发中,建议结合具体场景(如封装通用组件用泛型、处理复杂类型用映射 / 条件类型)灵活运用。

相关内容

写下自己求职记录也给正在求职得一些建议-CSDN博客

从初中级如何迈入中高级-其实技术只是"入门卷"-CSDN博客

相关推荐
trsoliu2 小时前
前端基于 TypeScript 使用 Mastra 来开发一个 AI 应用 / AI 代理(Agent)
前端·人工智能
鸡吃丸子2 小时前
前端权限控制:深入理解与实现RBAC模型
前端
Larry_zhang双栖2 小时前
低版本Chrome 内核兼容性问题的优美解决
前端·chrome
良木林2 小时前
浅谈原型。
开发语言·javascript·原型模式
qq_12498707533 小时前
基于node.js+vue的医院陪诊系统的设计与实现(源码+论文+部署+安装)
前端·vue.js·node.js·毕业设计
袁煦丞3 小时前
9.12 Halo的“傻瓜建站魔法”:cpolar内网穿透实验室第637个成功挑战
前端·程序员·远程工作
围巾哥萧尘3 小时前
大型语言模型语境学习中的演示位置偏置(DPP Bias)研究🧣
面试
universe_013 小时前
day27|前端框架学习
前端·笔记
沙尘暴炒饭3 小时前
前端vue使用canvas封装图片标注功能,鼠标画矩形框,标注文字 包含下载标注之后的图片
前端·vue.js·计算机外设