前言
其实很多人都不知道怎么去梳理自己得知识体系,而且不太记得住,想要记住,好像是多刷题目,通过做题得这种方式,让自己进行记忆,所以我会总体得看一遍书,然后去刷一些问题,通过问题得方式形成记忆,抓住重点,看看自己是否记得住。同时,这也是常问得面试问题吧。本来之前就想整理得,只是入职新公司,要时间去适应公司。害,分几篇分享。
js
ES6+ 新语法
- ES6(2015)
- 变量:
let/const、块级作用域 - 函数:箭头函数、默认参数、剩余 / 扩展参数
- 字符串:模板字符串(```+
${}) - 数据:数组 / 对象解构、
Map/Set - 面向对象:
class类与继承 - 模块化:
import/export - 异步:
Promise - 其他:
for...of、Symbol、生成器(function*)
- 变量:
- ES7(2016)
- 数组:
includes() - 运算符:指数运算符(
**)
- 数组:
- ES2017(ES8)
- 异步:
async/await - 对象:
Object.values()/entries() - 字符串:
padStart()/padEnd()
- 异步:
- ES2018(ES9)
- 对象:扩展运算符(
...用于对象) - 异步迭代:
for await...of - 正则:命名捕获组、反向断言
- 对象:扩展运算符(
- ES2019(ES10)
- 数组:
flat()/flatMap()(数组扁平化) - 字符串:
trimStart()/trimEnd() - 对象:
fromEntries()(将键值对转为对象)
- 数组:
- ES2020(ES11)
- 数据类型:
BigInt(大整数) - 运算符:可选链(
?.)、空值合并(??) - 模块化:动态
import() - 字符串:
matchAll()
- 数据类型:
- ES2021(ES12)
- 逻辑赋值:
&&=/||=/??= - 数字:分隔符(
1_000_000) - 数组:
at()(支持负索引)
- 逻辑赋值:
- ES2022(ES13)
- 类:私有属性(
#前缀)、静态类字段 - 数组:
findLast()/findLastIndex() Top-level await(模块顶层使用await)
- 类:私有属性(
- 后续版本(2023+)
- 新增
ArrayBuffer扩展、Object方法增强等小特性,以场景化优化为主。
- 新增
整体趋势:ES6 奠定现代语法基础,后续版本逐年迭代,聚焦解决实际开发痛点(如异步简化、安全访问、代码可读性)。
?.与?? 的区别
在 JavaScript(及 TypeScript)中,?.(可选链运算符)和 ??(空值合并运算符)是两个不同用途的语法糖,核心区别在于它们解决的问题和使用场景不同。以下是详细对比:
1. ?.(可选链运算符):安全访问嵌套属性 / 方法
作用 :用于安全地访问对象的嵌套属性、数组元素或调用方法,避免因中间值为 null 或 undefined 而抛出 Cannot read property 'x' of undefined 之类的错误。 逻辑 :如果运算符左侧的值(对象 / 数组)为 null 或 undefined,则整个表达式直接返回 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. ??(空值合并运算符):设置默认值
作用 :当左侧操作数为 null 或 undefined 时,返回右侧的默认值;否则返回左侧操作数。 核心特点 :仅对 null 和 undefined 生效,对其他 "假值"(如 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...in 和 for...of 是两种不同的循环语法,核心区别在于遍历目标、适用场景和遍历结果,具体如下:
1. 遍历目标不同
for...in:用于遍历对象的可枚举属性(包括自身属性和继承的属性) ,本质是遍历 "键名"。 可枚举属性指的是那些enumerable标志为true的属性(如对象自身定义的属性、数组的索引等,默认情况下大部分原生属性不可枚举)。for...of:用于遍历可迭代对象(Iterable Object)的元素值 ,本质是遍历 "值"。 可迭代对象是指实现了[Symbol.iterator]接口的对象,包括:数组、字符串、Map、Set、arguments对象、NodeList(DOM 节点集合)等。
2. 遍历结果不同
for...in遍历的是 "键名" :- 遍历对象时,得到的是对象的属性名(字符串类型);
- 遍历数组时,得到的是数组的索引(字符串类型,而非数字);
- 遍历字符串时,得到的是字符的索引(字符串类型)。
for...of遍历的是 "值" :- 遍历数组时,得到的是数组的元素值;
- 遍历字符串时,得到的是单个字符;
- 遍历
Map时,得到的是[key, value]数组; - 遍历
Set时,得到的是集合中的元素。
3. 适用场景不同
for...in适合遍历 "对象的属性": 主要用于检查对象是否包含某个属性,或遍历对象的键名(需注意过滤继承属性)。 不推荐用于遍历数组(因为可能遍历到非数字索引的属性,且索引是字符串类型)。for...of适合遍历 "可迭代对象的元素" : 主要用于获取数组、字符串、Map、Set等集合中的元素值,更符合 "遍历数据" 的直观需求。
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使用场景
- 作为对象的唯一属性键,避免属性名冲突
- 定义对象的 "私有" 属性(模拟私有成员)
- 定义常量集合(避免魔法字符串)
- 扩展内置对象的方法
- 定义迭代器接口(
Symbol.iterator)
迭代器与生成器
对象、类与面向对象编程
期约与异步函数
函 数
代理与反射
this全面讲解
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 通过陷阱函数实现对数据的拦截,核心步骤是:
- 创建代理对象(
new Proxy(target, handler)); - 在
handler中定义需要拦截的操作(如get、set等陷阱); - 通过代理对象操作数据时,自动触发对应的陷阱函数,执行自定义逻辑。
这种机制广泛用于数据验证、日志记录、响应式系统(如 Vue 3 的响应式原理)等场景,相比 Object.defineProperty 更强大、更灵活。
js 的继承是怎么做的?
JavaScript 的继承机制与传统面向对象语言(如 Java)不同,它基于原型链(Prototype Chain) 实现,而非类的直接继承。随着语言发展,ES6 引入了 class 和 extends 语法糖,简化了继承实现,但底层仍依赖原型链。以下是 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); // 输出:['呼吸', '繁殖'](继承父类实例属性)
缺点:
- 子类实例会共享父类的引用类型属性 (如
features),一个实例修改会影响其他实例。 - 无法在创建子类实例时向父类构造函数传递参数(如
Animal的name参数)。
二、构造函数继承(解决原型链的传参和共享问题)
核心思想 :在子类构造函数中通过 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 引入 class 和 extends 关键字,简化了继承写法,底层仍基于原型链,但更贴近传统面向对象的语法。
实现示例:
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)会被子类继承(通过类名.方法调用)。
六、其他继承方式(了解即可)
-
原型式继承 :通过
Object.create直接创建一个基于现有对象的新对象(适合简单对象的继承)。javascriptconst animal = { name: '动物', eat: () => { } }; const dog = Object.create(animal); // dog 继承 animal 的属性和方法 -
寄生式继承:在原型式继承的基础上,增强新对象(添加属性 / 方法)。
javascriptfunction createDog(animal) { const dog = Object.create(animal); dog.bark = () => console.log('汪汪叫'); // 增强对象 return dog; } -
多重继承:JavaScript 不直接支持多继承,但可通过 "混入(mixin)" 实现(复制多个对象的属性到目标对象)。
总结
JavaScript 继承的核心是原型链,所有继承方式都是围绕原型链的优化:
- 早期通过原型链 + 构造函数组合实现继承。
- 寄生组合继承是 ES5 中最完美的方案。
- ES6 的
class extends是寄生组合继承的语法糖,简化了写法,是目前的推荐方式。
理解原型链的工作原理(对象通过 __proto__ 指向原型,构造函数通过 prototype 关联原型),是掌握 JavaScript 继承的关键。
import()与require()的区别
在 JavaScript 中,import() 和 require() 都是用于模块导入的语法,但它们属于不同的模块系统,存在多方面区别:
- 所属模块系统不同
require()是 CommonJS 模块系统的语法,主要用于 Node.js 环境(早期前端也通过 Webpack 等工具兼容)。import(包括import()动态导入)是 ES6 模块系统(ESM)的语法,是 JavaScript 官方标准化的模块系统,现在浏览器和 Node.js(需配置)均支持。
- 加载时机不同
require()是 运行时加载 :代码执行到require()语句时才会加载模块,属于动态加载。- 静态
import(如import xxx from 'xxx')是 编译时(解析阶段)加载:在代码执行前就会解析模块依赖,属于静态加载(无法在条件语句中使用)。 import()是 ESM 中的动态导入 :虽然语法是import(),但本质是运行时加载,返回一个 Promise,可在条件语句中使用。
- 加载方式与阻塞性
require()是同步加载:会阻塞后续代码执行,直到模块加载完成。- 静态
import是异步加载(浏览器环境):加载模块时不会阻塞页面渲染,模块依赖会并行加载。 import()是异步加载 :返回 Promise,通过.then()或await处理结果,完全非阻塞。
- 返回值性质不同
-
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(实时反映模块内部变化)
- 语法灵活性不同
-
require()可动态生成路径,支持表达式:javascriptconst path = './module-' + Math.random(); const module = require(path); // 合法 -
静态
import路径必须是静态字符串(无法动态拼接):javascriptconst path = './module.js'; import { func } from path; // 报错(路径必须是字面量) -
import()支持动态路径(结合 ESM 的动态加载能力):javascriptconst path = './module-' + Math.random(); import(path).then(module => { /* 使用模块 */ }); // 合法
- 导出语法配合不同
-
require()对应 CommonJS 的导出语法module.exports或exportsjavascript// 导出 module.exports = { name: 'foo' }; exports.age = 18; // 导入 const obj = require('./module'); -
import对应 ESM 的导出语法export或export default:javascript// 导出 export const name = 'foo'; export default { age: 18 }; // 导入 import { name }, defaultObj from './module.js';
- 适用场景不同
require():主要用于 Node.js 环境(默认使用 CommonJS),或需要动态加载且兼容旧系统的场景。- 静态
import:用于前端工程化项目(如 Vue/React)、现代 Node.js 项目(需配置"type": "module"),适合静态分析和 Tree-Shaking 优化。 import():用于按需加载(如路由懒加载)、条件加载场景,兼顾 ESM 特性和动态性。
总结:require() 是 CommonJS 的同步动态加载,import(静态)是 ESM 的编译时静态加载,import() 是 ESM 的异步动态加载,三者在模块系统、加载机制和使用场景上有显著区别。
forEach 跟map得区别以及他们得参数
forEach 和 map 都是 JavaScript 数组的遍历方法,用于对数组中的每个元素执行回调函数,但它们的核心用途 和返回值有本质区别,参数则基本一致。
一、参数对比
两者的参数结构完全相同,都接收两个参数:
- 回调函数(必选) :对数组每个元素执行的函数,包含 3 个参数:
currentValue:当前正在处理的数组元素(必选)index:当前元素的索引(可选)array:调用该方法的原数组(可选)
- thisArg(可选) :执行回调函数时,指定
this的指向(在回调中可通过this访问)。
二、核心区别
| 特性 | forEach |
map |
|---|---|---|
| 返回值 | 返回 undefined(无实际返回值) |
返回一个新数组(由回调函数的返回值组成) |
| 核心用途 | 用于 "执行操作"(如打印、修改外部变量等) | 用于 "转换数组"(根据原数组生成新数组) |
| 是否改变原数组 | 本身不改变,但回调中可手动修改原数组 | 本身不改变原数组,仅返回新数组 |
| 链式调用 | 不能(因返回 undefined) |
可以(因返回新数组,可继续调用其他数组方法) |
三、示例说明
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(无返回值)
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: 数组的push、pop、splice等方法会修改数组长度或元素,但默认不会触发defineProperty定义的set拦截(因为这些操作本质是修改数组的length或索引,而非直接赋值)。 若要拦截数组操作,需手动重写数组原型方法(如 Vue 2 的实现方式),非常繁琐。 -
Proxy: 数组的任何修改操作(包括push、splice等)都会触发Proxy的set或deleteProperty陷阱,无需额外处理。例如:javascriptconst 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: 直接在原对象上修改属性描述符,会改变原对象的结构。例如:javascriptconst obj = {}; Object.defineProperty(obj, 'name', { get() { return 'xxx'; }, set(v) { /* ... */ } }); // obj 本身被修改了,新增了 name 属性的访问器 -
Proxy: 不修改原对象,而是返回一个新的代理对象,所有操作通过代理对象进行。原对象保持不变:javascriptconst obj = { name: 'xxx' }; const proxyObj = new Proxy(obj, { /* 拦截器 */ }); // obj 未被修改,proxyObj 是新的代理层
5. 嵌套对象的处理
-
Object.defineProperty: 只能拦截当前对象的属性,若对象包含嵌套对象(如obj.a.b),需要手动递归 对嵌套对象的属性设置get/set,否则无法拦截嵌套属性的操作。 -
Proxy: 可以在get陷阱中自动递归代理嵌套对象,实现对深层属性的拦截,更简洁:javascriptfunction 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 中,Map 和 Set 都是 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] 数组) |
| 使用场景 | 需要存储 "键 - 值关联" 数据(如字典、缓存、映射关系) | 需要存储 "唯一值"(如去重、集合运算、检查存在性) |
== 做了怎样得转换
转换流程总结
== 的比较步骤可简化为:
- 若两边类型相同,直接比较值(特殊处理 NaN、±0);
- 若类型不同,根据 "类型对"(如数字 vs 字符串、布尔 vs 数字等)执行对应转换;
- 转换为相同类型后,再次比较值是否相等。
注意
== 的转换规则复杂且容易出现反直觉结果(如 "" == 0 → true、[] == false → true),因此实际开发中更推荐使用 ===(严格相等运算符) ,它不进行类型转换,仅当类型和值都相同时才返回 true,避免潜在的逻辑错误。
原生js能发起请求的有哪些API
记忆方法:XHR(API设计繁琐,webworker不支持),fetch(需要手动处理错误),navigator.sendBeacon(页面卸载上报,只能发起post请求),websocket,SSE.
在原生 JavaScript 中,用于发起网络请求的 API 主要有以下几种,它们适用于不同的场景(如 HTTP 请求、实时通信、后台数据上报等):
- 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();
- 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);
}
}
- 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);
});
- 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 的特点
- 优点 :
- 实现简单,兼容性好(支持所有主流浏览器,包括低版本 IE)。
- 不受同源策略限制,可跨域请求数据。
- 缺点 :
- 仅支持 GET 请求 :因为
<script>标签的src属性只能发起 GET 请求,无法用于 POST、PUT 等其他请求方式。 - 安全性风险:服务器返回的是可执行的 JavaScript 代码,如果服务器被恶意攻击,可能返回恶意代码(如 XSS 攻击),导致客户端安全问题。
- 无错误处理机制 :
<script>标签加载失败时,无法通过常规的 error 事件可靠捕获(不同浏览器行为不一致),难以处理请求失败的场景。 - 依赖服务器配合:需要服务器主动支持 JSONP 格式,即在响应中包裹回调函数。
- 仅支持 GET 请求 :因为
四、与 JSON 的区别
- JSON 是一种轻量级的数据交换格式(纯数据),本质是字符串。
- JSONP 是一种跨域请求方式,本质是通过
<script>标签加载并执行包含数据的 JavaScript 代码,其响应内容是「回调函数调用 + JSON 数据」的组合。
总结
JSONP 通过巧妙利用 <script> 标签的跨域特性,以「回调函数包裹数据」的方式实现跨域数据传输,是早期解决跨域问题的重要方案。但由于其局限性(仅支持 GET、安全风险等),现代 Web 开发中更多使用 CORS(跨域资源共享) 方案,JSONP 逐渐成为历史遗留技术。
JSON进行深拷贝会有什么问题
记忆点:
JSON 深拷贝的核心问题是:JSON 格式无法完整映射 JavaScript 的所有数据类型和特性。它仅适用于 简单的纯数据对象(仅包含字符串、数字、布尔、数组、普通对象等基础类型),而对于包含函数、日期、正则、循环引用等复杂场景,会导致数据丢失、失真或报错。
使用 JSON 方法(JSON.stringify() 转字符串 + JSON.parse() 转对象)实现深拷贝是一种简单的方案,但它存在诸多限制和问题,主要源于 JSON 数据格式的局限性 和 JavaScript 数据类型的复杂性 之间的不匹配。具体问题如下:
- 不支持非 JSON 标准数据类型
JSON 仅支持 null、布尔值、数字、字符串、数组、普通对象 这几种基础类型,对于 JavaScript 中的其他特殊类型,会出现 丢失或失真:
-
函数(Function) :
JSON.stringify()会直接忽略函数(包括对象的方法),拷贝后该属性消失。javascriptconst 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 对象。javascriptconst obj = { time: new Date() }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.time instanceof Date); // false(变成字符串) -
正则对象(RegExp) :
JSON.stringify()会将正则转为空对象{},拷贝后完全丢失正则特性。javascriptconst obj = { reg: /abc/g }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.reg); // {}(正则丢失) -
Symbol 类型 :作为对象的键或值时,
JSON.stringify()会直接忽略(键会被跳过,值会被转为null)javascriptconst s = Symbol('key'); const obj = { [s]: 'value', val: s }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy); // {}(Symbol 键和值均丢失) -
其他特殊类型 :如
Map、Set、WeakMap、WeakSet等集合类型,会被转为空对象或数组,失去原有的数据结构和方法。
- 无法处理循环引用
如果对象存在 循环引用 (如 obj.self = obj),JSON.stringify() 会直接 抛出错误,导致拷贝失败。
javascript
const obj = {};
obj.self = obj; // 循环引用:对象引用自身
// 报错:Converting circular structure to JSON
const copy = JSON.parse(JSON.stringify(obj));
- 数值精度与特殊值失真
-
NaN、Infinity、-Infinity :JSON 不支持这些特殊数值,
JSON.stringify()会将它们转为null。javascriptconst 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()会导致精度丢失。javascriptconst obj = { num: 9007199254740993n }; // 大整数 const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.num); // 9007199254740992(精度丢失)
- 忽略对象的特殊属性
-
不可枚举属性 :
JSON.stringify()仅序列化对象的 可枚举自有属性 ,不可枚举属性(如Object.defineProperty定义的enumerable: false属性)会被忽略。 -
原型链属性
:拷贝结果会丢失原对象的原型链,变成一个纯粹的
cssObject实例,无法继承原型上的方法。
javascriptfunction 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(原型方法丢失)
- 性能问题
对于 大型复杂对象 (如嵌套层级极深、包含大量数据),JSON.stringify() 和 JSON.parse() 的序列化 / 反序列化过程会产生额外的性能开销,效率低于专门的深拷贝实现(如递归拷贝 + 类型判断)。
总结
JSON 深拷贝的核心问题是:JSON 格式无法完整映射 JavaScript 的所有数据类型和特性 。它仅适用于 简单的纯数据对象(仅包含字符串、数字、布尔、数组、普通对象等基础类型),而对于包含函数、日期、正则、循环引用等复杂场景,会导致数据丢失、失真或报错。
如需处理复杂类型,建议使用专门的深拷贝方法(如递归遍历 + 类型判断、lodash.cloneDeep 等库函数)。
递归深拷贝怎么处理日期正则
记忆点:使用 Object.prototype.toString.call(target) 而非 instanceof,因为 instanceof 可能受原型链修改影响,而 toString 能稳定返回内置类型标签(如 [object Date])。
在递归深拷贝中处理日期(Date)和正则(RegExp)等特殊引用类型时,需要通过类型判断 和针对性构造新实例的方式,确保拷贝后的对象既保持原始值,又不与原对象共享引用。以下是具体实现思路和代码示例:
一、核心处理思路
- 类型精准判断 : 利用
Object.prototype.toString.call()区分特殊类型(Date、RegExp)与普通对象 / 数组,避免使用typeof(因其对引用类型均返回'object',无法区分)。 - 针对性复制 :
- 日期(
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(引用不同)
四、关键说明
- 类型判断的准确性 : 使用
Object.prototype.toString.call(target)而非instanceof,因为instanceof可能受原型链修改影响,而toString能稳定返回内置类型标签(如[object Date])。 - 正则的完整复制 : 除了
source和flags,正则的lastIndex属性(用于全局匹配时记录下一次匹配位置)也需要手动复制,否则可能影响拷贝后正则的匹配行为。 - 扩展性 : 上述代码可扩展至其他特殊类型(如
Map、Set、Error等),思路类似:先判断类型,再通过对应构造函数创建新实例并复制关键属性。
通过这种方式,递归深拷贝能准确处理日期和正则类型,既保证拷贝后的值与原对象一致,又避免引用共享导致的副作用。
原生的异步方法有那些
在 JavaScript 中,"原生异步方法" 通常指语言本身(ECMAScript 标准)或运行环境(浏览器、Node.js)内置的、无需额外依赖库即可使用的异步机制 / API。以下是常见的原生异步方法及分类:
一、语言层面的异步机制
- 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 结果)。
- 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)、onload、onerror等)均为异步触发(事件队列调度)。
- 所有 DOM 事件回调(如
- 动画与渲染:
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 状态):
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什么函数都生效嘛
call 和 apply 并非对所有函数都生效:
-
对普通函数 (有自己的
this绑定)有效,可改变this并调用。 -
对箭头函数 无效(
this固定,无法修改)。 -
对部分内置函数 可能有限制(若
this类型不符合要求会报错)。
可以说一下操作系统里的堆和栈吗?
记忆点:
-
栈是 "轻量、高效的临时内存",用自动管理和快速访问支持函数调用,适合存储生命周期短、大小固定的数据;
-
堆是 "灵活、动态的长期内存",用手动管理(或自动回收)支持动态数据存储,适合存储生命周期长、大小不确定的数据。
在操作系统中,堆(Heap)和栈(Stack)是进程内存空间中两种核心的动态内存区域,它们承担不同的功能,遵循不同的管理规则,是程序运行的基础。理解操作系统层面的堆和栈,有助于理解程序的内存分配、函数调用机制以及常见的内存问题(如栈溢出、内存泄漏)。
一、栈(Stack):函数调用的 "临时舞台"
操作系统中的栈是一块连续的内存区域 ,主要用于支持函数调用和局部变量存储,其核心特性是 "先进后出(FILO)",类似叠盘子:最后放入的盘子最先被取出。
1. 栈的核心功能
-
存储函数调用上下文
:当一个函数被调用时,操作系统会为其创建一个 "
栈帧(Stack Frame)
",包含:
- 函数的返回地址(调用结束后回到哪里继续执行);
- 函数的参数(按调用约定入栈,如 C 语言的 "从右到左入栈");
- 局部变量(函数内定义的临时变量,如
int a = 10); - 被保存的寄存器状态(函数调用前需要保存寄存器的值,避免覆盖)。
-
支持函数嵌套调用:每个嵌套调用的函数都会生成新的栈帧并 "压栈",函数执行结束后栈帧 "出栈",内存自动释放。
2. 栈的关键特性
- 自动分配与释放:完全由操作系统(或编译器通过生成的机器码)自动管理,无需程序员干预。函数调用时栈帧入栈,函数返回时栈帧出栈,内存立即回收,不会产生碎片。
- 连续内存与固定大小 :栈的内存地址是连续的(从高地址向低地址增长),大小通常在程序启动时由操作系统固定(如 Linux 默认栈大小为 8MB,可通过
ulimit -s调整)。 - 访问速度极快:栈的内存地址连续,且 CPU 会对栈进行缓存优化(栈顶附近的内存大概率被频繁访问),因此读写速度远快于堆。
- 严格的生命周期:栈上的变量生命周期与函数调用绑定,函数执行结束后,局部变量立即失效,无法被外部访问。
- 栈的典型问题
- 栈溢出(Stack Overflow) :若函数嵌套层数过深(如递归无终止条件)或局部变量过大(如定义巨型数组
int arr[1000000]),会耗尽栈空间,触发栈溢出错误(程序崩溃)。
二、堆(Heap):动态内存的 "自由市场"
操作系统中的堆是一块非连续的内存区域 ,用于程序运行时动态分配内存(即 "按需申请、手动释放"),是存储长生命周期数据的主要区域。
- 堆的核心功能
-
动态内存分配
:程序运行时,通过系统调用(如 C 语言的
cmalloc、C++ 的
arduinonew,底层依赖操作系统的
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 中用于简化异步编程的语法糖,其底层基于 Promise 和 Generator 函数 的机制实现,本质是对 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 的本质是:
- 以更友好的语法封装了 Promise 的异步逻辑;
- 借助 Generator 的 "暂停 / 恢复" 机制实现代码的顺序执行感;
- 通过事件循环的微任务队列管理异步回调的执行时机。
这种设计既保留了 Promise 的非阻塞特性,又解决了 Promise 链式调用(.then())可能导致的 "回调地狱" 问题,让异步代码更易读、易维护。
Generator 是如何判断是否可执行?
在 JavaScript 中,Generator(生成器) 的 "可执行性" 主要指其返回的生成器对象(Generator Object)是否还能继续执行并产生值 。这种判断取决于生成器对象的内部状态 和执行上下文,具体逻辑如下:
一、Generator 的核心概念
首先明确两个关键角色:
- 生成器函数(Generator Function) :用
function*定义的函数,调用后不直接执行函数体 ,而是返回一个生成器对象。 - 生成器对象(Generator Object) :既是迭代器(Iterator) (有
next()方法),也是可迭代对象(Iterable) (有Symbol.iterator方法)。它是实际 "可执行" 的主体,负责暂停 / 恢复生成器函数的执行。
二、生成器对象的 "可执行性" 判断依据
生成器对象的可执行性由其内部状态 决定,状态变化与 next()、throw()、return() 等方法的调用紧密相关。主要状态包括:
- 初始状态(suspended start)
生成器函数被调用后,生成器对象刚创建时处于此状态:
- 函数体尚未开始执行(停在函数第一行代码之前)。
- 可执行 :调用
next()会启动执行,直到遇到第一个yield暂停。
- 暂停状态(suspended yield)
执行过程中遇到 yield 表达式时,生成器进入此状态:
- 函数体暂停在
yield处 ,并将yield后的值作为next()返回结果的value。 - 可执行 :再次调用
next()会从暂停处继续执行,直到下一个yield或函数结束。
- 执行中状态(executing)
调用 next()、throw() 等方法后,生成器正在执行函数体时的临时状态:
- 此时无法再次调用
next()(会报错,因为 JavaScript 是单线程,同一生成器对象不能并发执行)。 - 不可执行:需等待当前执行完成(进入暂停或关闭状态)后才能再次操作。
- 关闭状态(closed)
当生成器函数体正常执行完毕 (遇到 return 或函数结尾),或被强制终止 (调用 return()、throw() 且异常未捕获)时,进入此状态:
- 函数体已终止,无法再恢复执行。
- 不可执行 :再次调用
next()会直接返回{ value: undefined, done: true }。
三、如何判断生成器对象是否可执行?
JavaScript 标准并未直接暴露生成器对象的状态属性,但可通过以下方式间接判断:
1. 通过 next() 方法的返回值
生成器对象的 next() 方法返回一个对象 { value: any, done: boolean }:
- 若
done为false:说明生成器处于暂停状态 ,可继续执行 (再次调用next()会恢复)。 - 若
done为true:说明生成器处于关闭状态 ,不可再执行。
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 密集型任务或需要灵活调度的场景。
一、协程的核心特点
- 用户态调度 协程的调度完全由程序自身控制(用户态),而非操作系统内核(内核态)。这意味着协程的创建、暂停、恢复等操作无需经过内核,开销远小于线程或进程。
- 协作式并发 协程之间的调度是 "自愿" 的:一个协程必须主动调用暂停操作(如
yield、await),才能将控制权交给其他协程。而线程是 "抢占式" 的,操作系统可强制剥夺线程的 CPU 使用权。 - 状态保存与恢复 当协程暂停时,其执行状态(如局部变量、程序计数器位置等)会被保存;恢复时,这些状态会被重新加载,程序能从暂停处继续执行,就像从未中断过一样。
- 轻量级 一个进程可以包含多个线程,一个线程可以运行多个协程。协程的内存占用极小(通常仅几 KB),支持创建数十万甚至数百万个协程,而线程的数量受限于系统资源(通常最多几千个)。
二、协程与进程、线程的区别
| 特性 | 进程(Process) | 线程(Thread) | 协程(Coroutine) |
|---|---|---|---|
| 调度者 | 操作系统内核 | 操作系统内核 | 程序自身(用户态) |
| 上下文切换开销 | 大(涉及内存、寄存器等) | 中(比进程小,仍需内核参与) | 极小(仅保存少量状态) |
| 资源占用 | 高(独立内存空间) | 中(共享进程内存) | 极低(共享线程资源) |
| 并发模型 | 抢占式 | 抢占式 | 协作式 |
| 通信方式 | IPC(管道、socket 等) | 共享内存 / 锁 | 直接共享线程内存 |
三、协程的典型应用场景
- I/O 密集型任务 当程序需要频繁等待 I/O 操作(如网络请求、文件读写、数据库查询)时,协程可以在等待期间主动让出 CPU,让其他协程执行,避免资源浪费。例如:
- 网络爬虫:发起多个请求后,在等待响应时切换到其他任务。
- 服务器开发:处理大量客户端连接,每个连接用协程管理,等待数据时不阻塞。
- 复杂流程控制 协程的 "暂停 - 恢复" 特性适合实现需要分步执行、状态保存的逻辑,例如:
- 游戏中的角色 AI:每帧执行一部分逻辑,暂停后下一帧继续。
- 工作流引擎:按步骤执行任务,支持中断后恢复。
- 替代回调地狱 在异步编程中,嵌套回调会导致代码可读性差("回调地狱")。协程可以用同步的代码风格编写异步逻辑,例如 JavaScript 的
async/await(基于 Promise 和生成器实现,本质是协程的简化)。
四、不同语言中的协程实现
协程的概念在不同编程语言中有不同的实现和命名,核心思想一致但细节有差异:
- Python :通过
asyncio模块提供async/await语法,协程需运行在事件循环中。 - JavaScript :
Generator函数(function*)支持暂停 / 恢复,async/await是协程的语法糖(基于 Promise)。 - Go :
goroutine本质是轻量级线程,但调度器结合了协程的协作式特点,由 Go runtime 控制。 - Lua :原生支持协程(
coroutine库),可显式创建、暂停和恢复。 - C++20 :引入
std::coroutine标准库,支持协程语法。
五、总结
协程是一种用户态的轻量级并发组件,通过主动暂停和恢复实现多任务协作。它的优势在于:
- 极低的上下文切换开销,适合大规模并发;
- 用同步代码风格编写异步逻辑,提升可读性;
- 高效利用 CPU,尤其优化 I/O 等待场景。
协程并不替代进程或线程,而是建立在它们之上的更细粒度的调度单位,是现代编程语言处理并发任务的重要工具。
闭包它具体的作用机制是怎么样的?闭包具体是什么东西?可以解释一下吗?
闭包是编程语言中一种重要的语法特性,本质上是一个函数及其捆绑的周边环境状态(lexical environment,词法环境)的组合。简单说,就是当一个函数能记住并访问它创建时所在的作用域,即使这个函数在其创建的作用域之外执行,这种现象就叫闭包。
闭包的核心特征
要形成闭包,需要满足三个条件:
- 函数嵌套:存在内部函数和外部函数(外层函数包含内层函数)。
- 变量引用:内部函数引用了外部函数中的变量(或参数)。
- 外部返回:外部函数将内部函数作为返回值返回,使得内部函数能在外部函数的作用域之外被调用。
作用机制:为什么内部函数能 "记住" 外部变量?
我们通过一个具体例子(以 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
机制拆解:
- 作用域创建 :当
outer()被调用时,会创建一个作用域 (可以理解为一个 "变量存储容器"),里面包含count变量和inner函数。 - 内部函数引用外部变量 :
inner函数引用了outer作用域中的count变量,此时inner会 "记住" 这个变量的位置。 - 外部函数执行完毕但作用域不销毁 :通常,函数执行完毕后,其内部变量会被垃圾回收机制回收(释放内存)。但这里
outer返回了inner函数,而inner还在引用outer中的count,因此 JavaScript 引擎会保留outer的作用域,不让它被销毁。 - 闭包函数在外部调用时的行为 :当
closureFunc()(即inner)在outer作用域之外被调用时,它依然能通过 "作用域链" 找到并操作outer中保留的count变量,因此每次调用都会让count递增。
闭包的本质
闭包的本质是函数对其词法作用域的 "捕获"。无论函数被带到哪里执行,它始终能访问自己创建时所在的作用域中的变量,就像带着一个 "背包",里面装着它需要的外部变量。
闭包的典型用途
-
保存状态:如上面的例子,通过闭包可以在函数调用之间保留变量的状态(类似 "私有变量")。
-
模块化与封装:创建私有变量和方法,避免全局污染。例如:
javascriptfunction createCounter() { let count = 0; // 私有变量,外部无法直接访问 return { increment: () => { count++; }, getCount: () => count }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 1(只能通过暴露的方法访问) -
延迟执行:在定时器、事件监听等场景中,闭包可以记住当时的变量状态。例如:
javascriptfor (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); }
总结
闭包就是 "带着环境的函数",它让函数突破了 "执行完就销毁内部变量" 的限制,能够记住并操作创建时的周边变量。这种特性让它在状态保存、封装、延迟执行等场景中非常有用,但也需要注意过度使用可能导致的内存占用问题(因为闭包会保留作用域,不及时释放可能造成内存泄漏)。
作用域闭包
深浅复制
构造函数、原型和原型链
GC(垃圾回收)的两种类型
GC 是浏览器自动回收不再使用的内存的机制,核心是识别 "垃圾"(不可访问的对象)并释放其占用的内存。常见的两种类型如下:
-
标记 - 清除(Mark-and-Sweep)
-
原理:
- 标记阶段 :从根对象(如
window、全局变量)出发,遍历所有可访问的对象,标记为 "活跃"。 - 清除阶段:未被标记的对象被视为 "垃圾",回收其内存。
- 标记阶段 :从根对象(如
-
优点:实现简单,适用于大多数场景。
-
缺点:
- 清除后会产生内存碎片(空闲内存分散),可能导致后续大对象无法分配连续内存。
- 执行时会暂停 JS 主线程("全停顿"),大型应用可能出现卡顿。
-
-
标记 - 整理(Mark-and-Compact)
-
原理:
- 先执行 "标记 - 清除" 的标记阶段,识别活跃对象。
- 整理阶段:将所有活跃对象向内存一端移动,集中排列,然后清除边界外的所有垃圾内存。
-
优点:解决了内存碎片问题,内存分配更高效。
-
缺点:整理阶段需要移动对象,耗时更长,"全停顿" 时间更久。
-
补充:现代浏览器的 GC 优化
为减少 "全停顿" 影响,现代浏览器(如 Chrome 的 V8 引擎)采用分代回收(结合上述两种类型):
- 新生代(Young Generation):存放短期存活对象(如局部变量),采用 "复制算法"(快速回收,无碎片)。
- 老生代(Old Generation):存放长期存活对象(如全局变量),采用 "标记 - 清除"+"标记 - 整理"(兼顾效率与碎片问题)。
通过分代策略,GC 可针对不同生命周期的对象优化回收频率和方式,平衡性能与内存利用率。
重绘(Repaint)与重排 / 回流(Reflow)的区别
当 DOM 或样式发生变化时,浏览器可能触发重绘或重排,两者的核心区别在于是否影响元素的几何信息:
| 特性 | 重排(Reflow/Layout) | 重绘(Repaint) |
|---|---|---|
| 触发原因 | 元素几何信息改变(位置、尺寸、结构变化等) | 元素样式改变但几何信息不变(颜色、背景、透明度等) |
| 示例 | - 改变 width、height、left、display: none - 窗口大小调整、字体变化 - 新增 / 删除 DOM 元素 |
- 改变 color、background、border-radius - 改变 visibility: hidden(元素仍占据空间) |
| 性能消耗 | 高(需重新计算布局,可能连锁影响父 / 子元素) | 中(无需重新布局,仅重绘像素) |
| 关联关系 | 重排一定会导致重绘(布局变了,样式也需重新绘制) | 重绘不一定导致重排(样式变了,布局可能不变) |
EventLoop
事件循环是一个不停的从 宏任务队列/微任务队列中取出对应任务的**「循环函数」。在一定条件下,你可以将其类比成一个永不停歇的 「永动机」。 它从宏/微任务队列中 「取出」任务并将其 「推送」到「调用栈」**中被执行。
事件循环包含了四个重要的步骤:
- 「执行Script」:以**「同步的方式」**执行script里面的代码,直到调用栈为空才停下来。 其实,在该阶段,JS还会进行一些预编译等操作。(例如,变量提升等)。
- 执行**「一个」宏任务:从宏任务队列中挑选「最老」**的任务并将其推入到调用栈中运行,直到调用栈为空。
- 执行**「所有」微任务:从微任务队列中挑选 「最老」的任务并将其推入到调用栈中运行,直到调用栈为空。 「但是,但是,但是」(转折来了),继续从微任务队列中挑选最老的任务并执行。直到「微任务队列为空」**。
- 「UI渲染」 :渲染UI,然后,「跳到第二步」,继续从宏任务队列中挑选任务执行。(这步只适用浏览器环境,不适用Node环境)
promise原理
Promise 是 JavaScript 中处理异步操作的核心机制,它通过状态管理和回调函数解决了传统回调地狱的问题。下面从实现原理、关键特性到完整代码,深入解析 Promise 的工作机制。
一、核心概念与状态机
- 三种状态
- pending(进行中):初始状态。
- fulfilled(已成功):操作完成且成功。
- rejected(已失败):操作完成但失败。
状态转换规则:
plaintext
pending → fulfilled(不可逆转)
pending → rejected(不可逆转)
- 基本结构
Promise 本质是一个状态机,包含:
-
状态 (state):初始为
pending。 -
结果(value/reason):保存成功值或失败原因。
- 回调队列(then/catch 注册的回调):状态改变时触发。
二、实现 Promise 的核心逻辑
1. 基础框架
javascript
javascriptclass 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) ); }); }); } }三、关键特性解析
- 状态不可变性
状态一旦改变(
pending → fulfilled或pending → rejected),就无法再次修改。这确保了 Promise 的结果唯一性。
2. 异步执行then和catch的回调总是异步执行的,通过setTimeout或queueMicrotask实现。这保证了 Promise 的行为一致性。
3. 链式调用then和catch方法返回新的 Promise,允许链式调用。前一个 Promise 的结果会传递给下一个。
4. 值穿透如果
then或catch没有提供回调函数(如promise.then().then(handleValue)),值会自动传递到下一个回调。
5. 错误冒泡错误会一直向后传递,直到被
catch捕获。四、静态方法实现
1. Promise.all
并行处理多个 Promise,返回一个新 Promise:
- 所有 Promise 成功时,结果为所有值的数组。
-
任何一个 Promise 失败时,立即返回该错误。
- Promise.race
返回一个 Promise,其结果为第一个完成(成功或失败)的 Promise 的结果。
- Promise.resolve/reject
快速创建已解决或已拒绝的 Promise。
五、与 ES6 Promise 的差异
上述实现是简化版,原生 ES6 Promise 还有以下特性:
markdown
1. **微任务队列**:使用 `queueMicrotask` 而非 `setTimeout`,执行优先级更高。
2. **Promise 嵌套处理**:更严格的 `thenable` 对象检测。
3. **更完善的错误处理**:如 `unhandledrejection` 事件。
总结
Promise 通过状态机和回调队列实现了异步操作的同步化表达,核心在于:
- 状态管理:确保结果的不可变性。
- 异步处理:保证回调执行的时机一致性。
- 链式调用:解决回调地狱问题。
理解 Promise 的实现原理,有助于更深入掌握 JavaScript 的异步编程模型,为使用更高级的异步特性(如 async/await)打下坚实基础。
前端必会:Promise 全解析,从原理到实战1. 从 "回调地狱" 到 Promise 在前端开发的异步编程领域,我们 - 掘金
Promise.allSettled和 Promise.any 得区别
Promise.allSettled 和 Promise.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:
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"]
});
- 适用场景不同
Promise.allSettled:适合需要知道所有异步操作的结果(无论成功失败) 的场景。 例如:批量提交表单后,需要统计成功提交了多少、失败了多少,并分别处理。Promise.any:适合只要有一个异步操作成功即可,无需等待所有完成的场景。 例如:从多个镜像服务器加载同一资源,只要有一个服务器返回成功,就使用该资源(优先用最快成功的)。
总结表格
| 特性 | Promise.allSettled |
Promise.any |
|---|---|---|
| 触发条件 | 所有 Promise 都 settle(成功 / 失败) | 第一个成功的 Promise 出现(或所有失败) |
| 返回状态 | 始终成功(fulfilled) | 有成功则成功,全失败则失败(rejected) |
| 结果内容 | 包含所有 Promise 的状态和结果的数组 | 第一个成功的值,或包含所有错误的 AggregateError |
| 核心用途 | 获取所有操作的完整结果 | 快速获取第一个可用的成功结果 |
普通函数和箭头函数的区别
| 场景 | 普通函数 | 箭头函数 |
|---|---|---|
需要动态 this |
✅(如构造函数、对象方法) | ❌(this 无法绑定) |
| 作为构造函数 | ✅(可用 new 调用) |
❌(报错:不是构造函数) |
需要 arguments 对象 |
✅(内置 arguments) |
❌(需用剩余参数 ...args) |
| 简洁的回调函数 | ❌(语法冗余) | ✅(省略 function 和 return) |
| 定义生成器函数 | ✅(function*) |
❌(不能使用 yield) |
需要 prototype |
✅(默认有 prototype) |
❌(无 prototype) |
理解这些差异后,可根据具体场景灵活选择函数类型,避免因 this 指向或语法限制导致的错误。
水平垂直居中
在前端开发中,实现元素的水平垂直居中是一个常见需求。根据不同的场景和元素类型,可以采用多种方法。以下是一些常用的实现方式:
- 行内元素 / 文本(单行)
适用于文本、链接等行内元素,通过 text-align 和 line-height 实现:
css
.parent {
text-align: center;
line-height: 200px; /* 等于容器高度 */
height: 200px;
}
- 行内元素 / 文本(多行)
使用 Flexbox 或 Grid:
css
.parent {
display: flex;
justify-content: center;
align-items: center;
}
- 块级元素(已知宽高)
使用绝对定位和负边距:
css
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
margin-top: -50px; /* 高度的一半 */
margin-left: -50px; /* 宽度的一半 */
}
- 块级元素(未知宽高)
使用绝对定位和 transform:
css
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
- Flexbox(现代方案)
适用于大多数场景,简洁高效:
css
.parent {
display: flex;
justify-content: center;
align-items: center;
}
- Grid(现代方案)
更强大的二维布局:
css
.parent {
display: grid;
place-items: center;
}
- 表格布局
使用 display: table-cell:
css
.parent {
display: table-cell;
text-align: center;
vertical-align: middle;
}
- 绝对定位 + 自适应
使用 top/left/bottom/right 和 margin: 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-),可选用绝对定位方案。
- 响应式 :使用
transform或flex/grid可更好地适应不同尺寸。
根据具体场景选择合适的方法,能有效提升开发效率和布局稳定性。
CSS
常见的选择器有哪些
- 基础选择器
- 元素选择器:通过 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; })
- 元素选择器:通过 HTML 标签名匹配元素,语法:
- 组合选择器
- 后代选择器:匹配父元素内的所有后代元素,语法:
父选择器 后代选择器 { ... }(例:ul li { list-style: none; }) - 子元素选择器:匹配父元素的直接子元素,语法:
父选择器 > 子选择器 { ... }(例:div > p { font-size: 16px; }) - 相邻兄弟选择器:匹配某元素后紧邻的同级元素,语法:
元素 + 相邻元素 { ... }(例:h1 + p { margin-top: 10px; }) - 通用兄弟选择器:匹配某元素后所有同级元素,语法:
元素 ~ 兄弟元素 { ... }(例:h2 ~ span { color: gray; })
- 后代选择器:匹配父元素内的所有后代元素,语法:
- 伪类选择器
- 链接伪类:
:link(未访问链接)、:visited(已访问链接) - 交互伪类:
:hover(鼠标悬停)、:active(元素激活)、:focus(元素获焦) - 结构伪类:
:first-child(第一个子元素)、:last-child(最后一个子元素)、:nth-child(n)(第 n 个子元素)、:only-child(唯一子元素) - 状态伪类:
:checked(选中的表单元素)、:disabled(禁用的表单元素) - 否定伪类:
:not(选择器)(排除匹配选择器的元素,例::not(.active) { opacity: 0.5; })
- 链接伪类:
- 伪元素选择器
::before:在元素内容前插入内容::after:在元素内容后插入内容::first-letter:匹配文本首字母::first-line:匹配文本首行::selection:匹配用户选中的文本(例:::selection { background: yellow; })
选择器的权重
CSS 选择器的权重(Specificity)决定了样式的优先级,权重值越高,优先级越高。权重通常用 "(ID 数,类 / 伪类 / 属性数,元素 / 伪元素数)" 的形式表示,具体对应规则如下:
- 基础选择器
- 元素选择器 (如
p、div):权重(0, 0, 1) - ID 选择器 (如
#header):权重(1, 0, 0)(优先级最高的基础选择器) - 类选择器 (如
.active):权重(0, 1, 0) - 通配符选择器 (
*):权重(0, 0, 0)(无优先级,低于所有具体选择器) - 属性选择器 (如
[type="text"]、[class^="btn"]):权重(0, 1, 0)(与类选择器同级)
- 组合选择器
组合选择器的权重为组成它的所有单个选择器权重之和:
- 后代选择器 (如
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-block、flex); -
触发 BFC(如
overflow: hidden、display: flex); -
用 border 或 padding 隔离 margin;
-
添加隔离元素阻断相邻关系。
BFC
BFC(Block Formatting Context,块级格式化上下文)是 CSS 渲染页面时的一种布局模式,它决定了元素如何对其内容进行布局,以及与其他元素的关系。理解 BFC 是掌握 CSS 布局的关键之一,尤其是解决一些常见的布局问题。
一、基础概念
- 什么是 BFC? BFC 是页面上的一个独立渲染区域,区域内的元素布局不会影响区域外的元素。它有自己的布局规则,例如内部的子元素垂直排列,间距由
margin决定,且不会与外部元素发生布局冲突。 - 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 的原理
- BFC 的布局规则
- 内部的 Box 垂直排列,间距由
margin决定。 - BFC 的区域不会与浮动元素重叠。
- BFC 内外的布局互相独立。
- 计算 BFC 高度时,浮动元素也参与计算。
- 内部的 Box 垂直排列,间距由
- BFC 与文档流的关系 BFC 是页面文档流中的一个独立容器,其内部布局遵循普通流,但与外部的元素隔离。这种隔离性使得 BFC 可以避免外部浮动、外边距折叠等问题。
- BFC 的渲染机制 浏览器在渲染时,会为每个 BFC 分配一个独立的布局上下文。这个上下文决定了元素如何定位、尺寸计算及与其他元素的关系。例如,BFC 容器会阻止浮动元素溢出到容器外部。
四、实际开发中的应用场景
- 避免浮动导致的布局错乱 通过触发父元素的 BFC,确保父容器正确包含浮动子元素。
- 实现复杂布局 结合浮动和 BFC 实现自适应布局,无需依赖现代布局方案(如 Flexbox/Grid)。
- 隔离第三方组件样式 在组件外层创建 BFC,防止组件内外样式相互干扰(如外边距折叠)。
五、注意事项
- BFC 的副作用
- 使用
overflow: hidden可能导致内容被裁剪或出现滚动条。 - 某些触发方式(如
float)会改变元素的显示模式。
- 使用
- BFC 与现代布局方案的对比
- Flexbox 和 Grid 提供了更直观的布局方式,但在某些场景(如清除浮动)中,BFC 仍是简单有效的方案。
六、总结
BFC 是 CSS 布局的底层机制之一,它通过隔离渲染区域解决外边距折叠、浮动塌陷等问题,同时为复杂布局提供基础支持。虽然现代布局方案(Flexbox/Grid)简化了部分场景,但理解 BFC 仍有助于开发者更彻底地掌握 CSS 布局原理,写出更健壮的代码。
flex属性
SS Flexbox 是现代前端开发中用于一维布局的核心技术,其属性分为 容器属性 (控制整体布局)和 项目属性(控制子项行为)。以下是详细的分类和解释,从基础到深入:
一、Flex 容器属性
-
display: flex | inline-flex- 作用:将元素定义为 Flex 容器,子元素成为 Flex 项目。
- 区别 :
flex:容器表现为块级元素。inline-flex:容器表现为行内元素,内部仍为 Flex 布局。
-
flex-direction- 作用:定义主轴方向(项目的排列方向)。
- 值 :
row(默认):水平方向,从左到右。row-reverse:水平方向,从右到左。column:垂直方向,从上到下。column-reverse:垂直方向,从下到上。
-
flex-wrap- 作用:控制项目是否换行。
- 值 :
nowrap(默认):不换行,项目可能溢出。wrap:换行,第一行在上方。wrap-reverse:换行,第一行在下方。
-
flex-flow- 作用 :
flex-direction和flex-wrap的简写。 - 语法 :
flex-flow: <flex-direction> <flex-wrap> - 示例 :
flex-flow: row wrap;
- 作用 :
-
justify-content- 作用:定义项目在主轴上的对齐方式。
- 值 :
flex-start(默认):左对齐。flex-end:右对齐。center:居中。space-between:两端对齐,项目间隔相等。space-around:项目两侧间隔相等。space-evenly:所有间隔完全相等(包括边缘)。
-
align-items- 作用:定义项目在交叉轴上的对齐方式。
- 值 :
stretch(默认):拉伸填满容器高度。flex-start:顶部对齐。flex-end:底部对齐。center:垂直居中。baseline:按基线对齐。
-
align-content- 作用 :多行项目在交叉轴上的对齐方式(需
flex-wrap: wrap)。 - 值 :与
justify-content类似,如flex-start、center、space-between等。
- 作用 :多行项目在交叉轴上的对齐方式(需
二、Flex 项目属性
-
order- 作用:定义项目的排列顺序,数值越小越靠前。
- 值 :整数(默认
0)。 - 示例 :
order: -1;使项目提前。
-
flex-grow- 作用:定义项目的放大比例(当容器有剩余空间时)。
- 值 :非负整数(默认
0,不放大)。 - 示例 :
flex-grow: 2;表示占据剩余空间的比例是其他项目的两倍。
-
flex-shrink- 作用:定义项目的缩小比例(当容器空间不足时)。
- 值 :非负整数(默认
1,允许缩小)。 - 示例 :
flex-shrink: 0;禁止项目缩小。
-
flex-basis- 作用:定义项目在分配多余空间前的初始大小。
- 值 :长度(如
200px)或auto(默认,基于内容计算)。 - 注意 :优先级高于
width。
-
flex- 作用 :
flex-grow、flex-shrink、flex-basis的简写。 - 语法 :
flex: <flex-grow> <flex-shrink> <flex-basis> - 常用简写 :
flex: 1→1 1 0(占满剩余空间)。flex: auto→1 1 auto(基于内容伸缩)。flex: none→0 0 auto(固定大小,不伸缩)。
- 作用 :
-
align-self- 作用 :覆盖容器的
align-items,定义单个项目的交叉轴对齐方式。 - 值 :
auto(默认继承容器)、stretch、flex-start、flex-end、center、baseline。
- 作用 :覆盖容器的
三、深入理解
-
空间分配逻辑
- 剩余空间 :容器大小减去所有项目的
flex-basis或固定大小后的空间。 - 放大 :按
flex-grow比例分配剩余空间。 - 缩小 :按
flex-shrink比例压缩超出容器的空间。
- 剩余空间 :容器大小减去所有项目的
-
flex-basis与width的关系- 当
flex-direction: row时,flex-basis控制宽度; - 当
flex-direction: column时,flex-basis控制高度。 - 若同时设置
flex-basis和width,flex-basis优先级更高。
- 当
-
负空间的压缩
-
若
flex-shrink: 0,项目不会缩小,可能导致溢出。 -
实际压缩量计算公式:
css项目压缩量 = (负空间 * flex-shrink值) / 所有项目的 (flex-shrink值 * flex-basis值) 总和
-
-
浏览器兼容性
- 现代浏览器全面支持 Flexbox,但旧版浏览器(如 IE 10/11)需加前缀
-ms-。 - 使用 Autoprefixer 工具自动处理兼容性。
- 现代浏览器全面支持 Flexbox,但旧版浏览器(如 IE 10/11)需加前缀
四、最佳实践
-
优先使用
flex简写:css.item { flex: 1; /* 等价于 flex: 1 1 0 */`flex-grow`、`flex-shrink`、`flex-basis` } -
固定侧边栏 + 自适应内容布局:
css.sidebar { flex: 0 0 200px; } /* 固定宽度 */ .content { flex: 1; } /* 占满剩余空间 */ -
等高布局:
css.container { display: flex; align-items: stretch; /* 默认值,项目高度一致 */ } -
响应式导航栏:
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属性
一、基础常用值
-
block- 元素生成块级盒子,独占一行,可设置宽高、内外边距。
- 典型元素:
<div>,<p>,<h1>-<h6>。 - 示例:
display: block;
-
inline- 元素生成行内盒子,不独占一行,不可直接设置宽高,大小由内容决定。
- 典型元素:
<span>,<a>,<strong>。 - 示例:
display: inline;
-
inline-block- 混合特性:行内排列,但可设置宽高、内外边距。
- 典型应用:导航按钮、图标与文字混排。
- 示例:
display: inline-block;
-
none- 元素不渲染,完全从文档流中移除,不占据空间。
- 与
visibility: hidden的区别:后者隐藏元素但保留空间。 - 示例:
display: none;
二、布局相关值
-
flex- 启用弹性盒子布局(一维布局),子元素成为弹性项。
- 控制属性:
flex-direction,justify-content,align-items等。 - 示例:
display: flex;
-
grid- 启用网格布局(二维布局),子元素按网格行列排列。
- 控制属性:
grid-template-columns,grid-gap,grid-area等。 - 示例:
display: grid;
-
inline-flex/inline-grid- 行内版本的弹性或网格容器,外部表现为行内元素,内部按弹性/网格布局。
- 示例:
display: inline-flex;
三、传统布局模型
-
table系列- 模拟表格布局(逐渐被 Flex/Grid 替代):
table:行为类似<table>。table-row:类似<tr>。table-cell:类似<td>,常用于垂直居中。
- 示例:
display: table-cell;
- 模拟表格布局(逐渐被 Flex/Grid 替代):
-
list-item- 元素表现为列表项(如
<li>),生成标记(如圆点)。 - 示例:
display: list-item;
- 元素表现为列表项(如
四、特殊场景值
-
contents- 元素自身不生成盒子,子元素直接继承父级布局(类似"溶解"自身)。
- 注意:可能影响可访问性(屏幕阅读器会忽略该元素)。
- 示例:
display: contents;
-
flow-root- 创建块级格式化上下文(BFC),避免外边距合并或浮动塌陷。
- 类似
overflow: hidden但更语义化。 - 示例:
display: flow-root;
-
run-in- 根据上下文决定表现为块级或行内元素(浏览器支持有限)。
- 示例:
display: run-in;
五、实验性或特定用途值
-
ruby系列- 用于东亚文字排版(如注音):
ruby:定义 ruby 容器。ruby-text:定义注音文本。
- 示例:
display: ruby-text;
- 用于东亚文字排版(如注音):
-
subgrid- 网格布局的子网格(CSS Grid Level 2 特性,部分浏览器支持)。
- 允许子网格继承父网格的轨道定义。
- 示例:
display: subgrid;
六、全局值
inherit:继承父元素的display值。initial:重置为默认值(通常是inline)。unset:根据属性是否可继承,表现为inherit或initial。
总结与选择建议
- 基础布局 :优先使用
block、inline、inline-block。 - 现代布局 :首选
flex(一维)和grid(二维)。 - 隐藏元素 :用
none完全移除,或用opacity: 0保留交互。 - 兼容性 :传统布局(如
table)适用于旧项目,新项目推荐 Flex/Grid。
通过灵活组合这些值,可以实现从简单到复杂的响应式布局设计。
postion属性
一、CSS定位基础
CSS的position属性定义了元素的定位模式,主要类型包括:
static:默认值,元素在正常文档流中。relative:相对自身原始位置偏移,不脱离文档流。absolute:相对于最近的定位祖先元素 (非static)绝对定位,脱离文档流。fixed:相对于**视口(viewport)**定位,脱离文档流,不受滚动影响。sticky:混合模式,滚动到阈值前表现为relative,之后表现为fixed。
使用场景:
absolute:悬浮菜单、弹窗。fixed:导航栏、广告。sticky:表格标题、侧边栏。
二、浏览器如何实现定位
浏览器的渲染引擎(如Blink、WebKit)通过以下流程处理定位:
- 布局阶段(Layout/Reflow)
static/relative:参与正常文档流布局,通过盒模型计算位置。absolute/fixed:脱离文档流,单独计算位置,减少父容器回流影响。sticky:动态切换布局模式,需持续监听滚动事件。
- 分层与合成(Composite)
- 提升为合成层 :
fixed、sticky或使用transform的元素会被提升为独立的合成层(Composite Layer),由GPU处理。 - GPU加速 :合成层通过纹理上传到GPU,滚动时仅变换位置(
translate),无需重绘(Repaint)。
- 滚动处理
- 主线程与合成线程协作 :
- 主线程:处理JavaScript、样式计算、布局。
- 合成线程:管理图层合成,直接响应滚动事件,避免阻塞主线程。
三、操作系统底层协作
操作系统通过以下方式支持浏览器渲染:
- 图形接口与硬件加速
- GPU资源管理:操作系统(如Windows的DirectX,macOS的Metal)提供图形API,浏览器通过它们调用GPU进行图层合成。
- 垂直同步(VSync):操作系统协调GPU渲染帧率与显示器刷新率,避免画面撕裂。
- 进程调度
- 多进程架构 :浏览器(如Chrome)将渲染任务分配给渲染进程 ,合成任务由GPU进程处理,操作系统调度这些进程的CPU/GPU资源。
- 输入事件处理:操作系统将滚动、触摸事件传递给浏览器进程,再路由到合成线程。
- 内存管理
- 纹理内存:GPU显存由操作系统分配,浏览器将合成层纹理存储在显存中,提升渲染效率。
四、深度思考:性能优化与挑战
- 层爆炸(Layer Explosion) :
- 过度使用
absolute/fixed可能导致过多合成层,占用GPU内存。 - 优化:使用
will-change谨慎提示浏览器分层。
- 过度使用
- 滚动性能 :
sticky依赖主线程计算阈值,复杂页面可能卡顿。- 优化:使用
position: fixed+ JavaScript手动控制。
- 跨平台差异 :
- 移动端视口受软键盘影响,
fixed定位可能失效。 - 解决方案:使用
position: absolute+ 动态视口高度(vh单位)。
- 移动端视口受软键盘影响,
五、总结
- 前端定位:通过CSS定义元素的布局模式,核心是控制文档流与坐标系。
- 浏览器实现:依赖渲染引擎的分层、合成机制,结合GPU加速提升性能。
- 操作系统角色:提供图形接口、进程调度和硬件资源管理,确保高效渲染。
理解这一链条有助于开发高性能Web应用,尤其是在复杂交互和动画场景中。
CSS三大特性、盒子模型
实现响应式自适应方案
rem与px怎么做转换
rem 和 px 是两种常用的长度单位,它们的转换关系与 根元素(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 个阶段,且通常按顺序执行(现代浏览器会优化为流水线并行处理):
-
HTML 解析与 DOM 树构建
- 浏览器解析 HTML 标签,生成 DOM(文档对象模型)树,每个标签对应树中的一个节点,描述页面的结构和内容。
-
CSS 解析与 CSSOM 树构建
- 解析 CSS 样式(包括内联、嵌入、外部样式),生成 CSSOM(CSS 对象模型)树,记录每个元素的样式规则(如颜色、尺寸、位置等)。
-
渲染树(Render Tree)构建
-
结合 DOM 树和 CSSOM 树,生成
渲染树:
- 只包含可见元素(忽略
display: none的元素、<head>等无视觉效果的节点)。 - 每个节点包含 DOM 信息和对应的样式信息,用于后续布局和绘制。
- 只包含可见元素(忽略
-
-
布局(Layout/Reflow)
-
根据渲染树计算每个元素的
几何信息
(位置、尺寸、大小等),例如:
- 确定元素在页面中的坐标(如
top: 10px)、宽高(如width: 200px)。 - 父元素的布局会影响子元素(如
padding会改变子元素位置)。
- 确定元素在页面中的坐标(如
-
此阶段是计算元素位置和大小的关键步骤,耗时与元素数量正相关。
-
-
绘制(Paint/Repaint)
- 根据布局结果,将元素的样式(如颜色、背景、阴影)绘制到屏幕上,生成像素点。
- 绘制可按 "层" 进行(如
z-index较高的元素单独成层),现代浏览器通过 合成层(Compositing Layers) 优化性能。
-
合成(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) :元素实际显示的内容(文本、图片等),由
width和height属性定义。 - 内边距(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) :元素的内容区宽度包含了
padding和border,但不包含margin。 - 内边距(padding)和边框(border) :包含在
width和height属性内,不会额外增加元素尺寸。 - 外边距(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 属性)
-
标准模型(默认值)
cssbox-sizing: content-box; -
IE 模型(怪异模型)
cssbox-sizing: border-box;
应用场景
- 响应式布局 :使用
border-box可避免因内边距导致布局溢出。 - 统一设计 :所有元素设置
box-sizing: border-box可简化尺寸计算。
css
/* 全局设置所有元素使用IE模型 */
* {
box-sizing: border-box;
}
4. 其他盒子模型相关概念
4.1 行内元素盒子模型
-
内联元素(如
<span>、<a>):
width和height无效,由内容撑开。padding和border水平方向有效(影响布局),垂直方向不影响布局。margin水平方向有效,垂直方向可能无效。
4.2 替换元素(如<img>、<input>)
- 有固有尺寸,
width和height可控制。 - 盒子模型规则与块级元素相同。
4.3 外边距合并(Margin Collapsing)
- 相邻块级元素的垂直外边距会合并为较大的一个。
- 父子元素之间也可能发生外边距合并。
5. 对比总结
| 特性 | 标准盒子模型 (content-box) |
IE 盒子模型 (border-box) |
|---|---|---|
width包含内容 |
仅内容区 | 内容区 + 内边距 + 边框 |
| 尺寸计算 | width + padding + border |
width(已包含) |
| 布局控制 | 需额外计算 padding 和 border | 更直观,不易溢出 |
| 应用场景 | 默认值,适合简单布局 | 响应式设计、复杂布局 |
6. 最佳实践
-
使用 border-box:
css* { box-sizing: border-box; }简化尺寸计算,减少布局错误。
-
避免内外边距混用 : 使用
padding控制元素内部空间,margin控制元素间距离。 -
注意行内元素特性 : 行内元素的
padding和border可能影响布局但不占空间。
理解盒子模型是掌握 CSS 布局的基础,通过合理使用box-sizing和控制内外边距,可以更高效地实现各种复杂布局。
前端不同页面之间怎么通信
对比与选择建议
| 方案 | 数据量 | 跨域 | 实时性 | 实现难度 | 适用场景 |
|---|---|---|---|---|---|
| URL 参数 | 小 | 支持 | 同步 | 简单 | 一次性数据传递 |
| localStorage | 中 | 否 | 需刷新 | 简单 | 持久化数据共享 |
| postMessage | 中 | 支持 | 异步 | 中等 | 跨窗口(源要相同) /iframe 通信 |
| WebSocket | 不限 | 支持 | 实时 | 复杂 | 实时双向通信 |
| IndexedDB | 大 | 否 | 异步 | 复杂 | 大量数据存储与共享 |
| Service Worker | 中 | 否 | 实时 | 复杂 | 离线消息和跨标签通信 |
| Broadcast Channel | 中 | 否 | 实时 | 简单 | 同源页面间广播 |
注意事项
- 安全性:敏感数据需加密处理(如 JWT 签名)。
- 兼容性 :IE 等旧浏览器可能不支持部分 API(如
BroadcastChannel)。 - 性能 :避免频繁操作
localStorage,可能导致页面卡顿。 - 内存管理 :
WebSocket和Service Worker需正确关闭连接,防止内存泄漏。
TS
infer得作用
infer 用于在条件类型中提取类型信息(类似变量声明)。
interface与type的区别
在 TypeScript 中,interface 和 type 都用于描述类型,但它们在语法和功能上存在一些关键区别。以下是它们的主要差异和适用场景:
- 基本语法与定义方式
-
interface:仅用于定义对象类型或类的接口,语法更简洁,专注于结构描述。typescriptinterface 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 }
- 扩展(继承)方式
-
interface:通过extends关键字扩展,支持多继承。typescriptinterface Animal { name: string; } interface Dog extends Animal { bark(): void; } // 多继承 interface Pet extends Animal, Dog { owner: string; } -
type:通过交叉类型(&)实现扩展,不支持extends关键字。typescripttype Animal = { name: string; }; type Dog = Animal & { bark(): void; };
- 合并声明(Declaration Merging)
-
interface:支持同名接口自动合并,属性和方法会被合并为一个接口。typescriptinterface User { name: string; } interface User { age: number; } // 合并后:{ name: string; age: number } const user: User = { name: 'Alice', age: 20 }; -
type:不支持合并,同名type会报错。typescripttype User = { name: string }; type User = { age: number }; // 报错:标识符"User"重复
- 其他关键差异
| 特性 | interface | type | |
|-----------|-------------------------|-----------------------------------------|--------|---|
| 支持的类型 | 仅对象、函数、类接口 | 所有类型(包括基本类型、联合类型等) | |
| 计算属性 | 不支持 | 支持(如 `type Key = 'a' | 'b'`) | |
| 与类的结合 | 可通过 implements 约束类的结构 | 也可通过 implements 使用,但不如 interface 直观 | |
| 声明提升 | 完全支持(类似变量声明提升) | 部分支持(取决于具体场景) | |
- 适用场景建议
- 优先使用
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)
通过遍历已有类型的属性 创建新类型(内置的如 Partial、Readonly 等)。
内置映射类型
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 的核心竞争力,合理使用可以大幅提升代码的健壮性和开发效率。实际开发中,建议结合具体场景(如封装通用组件用泛型、处理复杂类型用映射 / 条件类型)灵活运用。