前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)
前言
其实很多人都不知道怎么去梳理自己得知识体系,而且不太记得住,想要记住,好像是多刷题目,通过做题得这种方式,让自己进行记忆,所以我会总体得看一遍书,然后去刷一些问题,通过问题得方式形成记忆,抓住重点,看看自己是否记得住。同时,这也是常问得面试问题吧。本来之前就想整理得,只是入职新公司,要时间去适应公司。害,分几篇分享。
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
,不再继续访问右侧的属性 / 方法。
示例
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
)不生效(这是它与 ||
运算符的关键区别)。
示例
// 左侧为 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:遍历数组
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:遍历对象
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:遍历字符串
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
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
)
迭代器与生成器
https://www.yuque.com/ergz/web/qeeg7q18va9u2ykc#GRSje
对象、类与面向对象编程
https://www.yuque.com/ergz/web/ozpq9gmwga9tsyg5
期约与异步函数
https://www.yuque.com/ergz/web/cxzlaa1rolmsvbxk#vO4eg
函 数
https://www.yuque.com/ergz/web/vp2ulvbwu3tx5w0m
代理与反射
https://www.yuque.com/ergz/web/xpdllbmu1wil63qe
this全面讲解
https://www.yuque.com/ergz/web/gnncr1
js问题
如何获取url?a=‘11123’
// 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
)指向父类的实例,形成原型链,从而继承父类的属性和方法。
实现示例:
// 父类:动物
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
,从而继承父类的实例属性。
实现示例:
// 父类
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
),导致方法无法复用(每个实例都需单独定义)。
三、组合继承(原型链 + 构造函数,主流方案)
核心思想:结合原型链继承(继承原型方法)和构造函数继承(继承实例属性),取长补短。
实现示例:
// 父类
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
复制父类原型作为子类原型,避免父类构造函数被调用两次,是目前最理想的继承方式。
实现示例:
// 父类
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
关键字,简化了继承写法,底层仍基于原型链,但更贴近传统面向对象的语法。
实现示例:
// 父类
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
直接创建一个基于现有对象的新对象(适合简单对象的继承)。const animal = { name: '动物', eat: () => {} }; const dog = Object.create(animal); // dog 继承 animal 的属性和方法
-
寄生式继承:在原型式继承的基础上,增强新对象(添加属性 / 方法)。
function 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()
返回的是模块导出对象的拷贝:模块内部的变化不会影响已导入的拷贝(除非导出的是引用类型,如对象 / 数组,修改其属性会生效)。// 模块 a.js let count = 1; module.exports = { count };// 主文件 const a = require('./a'); a.count = 2; // 仅修改拷贝,原模块的 count 仍为 1
-
import
导入的是模块导出的引用:模块内部的变化会实时反映到导入处(因为 ESM 是动态绑定)。// 模块 a.js export let count = 1; export const increment = () => { count++ };// 主文件 import { count, increment } from './a.js'; increment(); console.log(count); // 输出 2(实时反映模块内部变化)
- 语法灵活性不同
-
require()
可动态生成路径,支持表达式:const path = './module-' + Math.random(); const module = require(path); // 合法
-
静态
import
路径必须是静态字符串(无法动态拼接):const path = './module.js'; import { func } from path; // 报错(路径必须是字面量)
-
import()
支持动态路径(结合 ESM 的动态加载能力):const path = './module-' + Math.random(); import(path).then(module => { /* 使用模块 */ }); // 合法
- 导出语法配合不同
-
require()
对应 CommonJS 的导出语法module.exports
或exports
// 导出 module.exports = { name: 'foo' }; exports.age = 18;// 导入 const obj = require('./module');
-
import
对应 ESM 的导出语法export
或export default
:// 导出 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
:执行操作,无返回值
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
:转换数组,返回新数组
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
陷阱,无需额外处理。例如: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
:
直接在原对象上修改属性描述符,会改变原对象的结构。例如:const obj = {}; Object.defineProperty(obj, 'name', {get() { return 'xxx'; },set(v) { /* ... */ } }); // obj 本身被修改了,新增了 name 属性的访问器
-
Proxy
:
不修改原对象,而是返回一个新的代理对象,所有操作通过代理对象进行。原对象保持不变:const obj = { name: 'xxx' }; const proxyObj = new Proxy(obj, { /* 拦截器 */ }); // obj 未被修改,proxyObj 是新的代理层
5. 嵌套对象的处理
-
Object.defineProperty
:
只能拦截当前对象的属性,若对象包含嵌套对象(如obj.a.b
),需要手动递归对嵌套对象的属性设置get
/set
,否则无法拦截嵌套属性的操作。 -
Proxy
:
可以在get
陷阱中自动递归代理嵌套对象,实现对深层属性的拦截,更简洁: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 中,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 设计较繁琐。
基本用法示例:
// 创建 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
)。
基本用法示例:
// 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);
- 适用于页面离开时的埋点、日志上报等场景。
基本用法示例:
// 页面关闭时上报数据
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://
协议(后者加密)。
基本用法示例:
// 建立 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. 客户端准备回调函数
客户端在页面中预先定义一个回调函数(名称可自定义),用于接收和处理服务器返回的数据。例如:
// 定义回调函数:参数为服务器返回的数据
function handleResponse(data) {console.log("跨域获取的数据:", data);// 处理数据的逻辑...
}
2. 动态创建 <script>
标签发起跨域请求
客户端通过 JavaScript 动态创建 <script>
标签,并将其 src
属性指向目标服务器的接口 URL。同时,需要在 URL 中通过参数(通常约定为 callback
)告知服务器回调函数的名称(即步骤 1 中定义的函数名)。
示例代码:
// 生成唯一的回调函数名(避免重复)
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" }
,则返回的响应内容为:
handleResponse({ "name": "JSONP", "type": "cross-domain" });
4. 客户端执行回调函数获取数据
当 <script>
标签加载完服务器返回的响应后,浏览器会将其视为 JavaScript 代码并立即执行。此时,客户端预先定义的 handleResponse
函数会被调用,参数就是服务器返回的数据,从而实现跨域数据的获取。
5. 清理资源(可选)
请求完成后,可以移除动态创建的 <script>
标签,避免页面冗余:
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()
会直接忽略函数(包括对象的方法),拷贝后该属性消失。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 对象。const obj = { time: new Date() }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.time instanceof Date); // false(变成字符串)
-
正则对象(RegExp):
JSON.stringify()
会将正则转为空对象{}
,拷贝后完全丢失正则特性。const obj = { reg: /abc/g }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.reg); // {}(正则丢失)
-
Symbol 类型:作为对象的键或值时,
JSON.stringify()
会直接忽略(键会被跳过,值会被转为null
)const 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()
会直接 抛出错误,导致拷贝失败。
const obj = {};
obj.self = obj; // 循环引用:对象引用自身// 报错:Converting circular structure to JSON
const copy = JSON.parse(JSON.stringify(obj));
- 数值精度与特殊值失真
-
NaN、Infinity、-Infinity:JSON 不支持这些特殊数值,
JSON.stringify()
会将它们转为null
。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()
会导致精度丢失。const obj = { num: 9007199254740993n }; // 大整数 const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.num); // 9007199254740992(精度丢失)
- 忽略对象的特殊属性
-
不可枚举属性:
JSON.stringify()
仅序列化对象的 可枚举自有属性,不可枚举属性(如Object.defineProperty
定义的enumerable: false
属性)会被忽略。 -
原型链属性
:拷贝结果会丢失原对象的原型链,变成一个纯粹的
Object
实例,无法继承原型上的方法。
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(原型方法丢失)
- 性能问题
对于 大型复杂对象(如嵌套层级极深、包含大量数据),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)
创建新实例,保持正则特性不变。
- 日期(
二、递归深拷贝实现(含日期和正则处理
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;
}
三、测试验证
// 测试日期拷贝
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()
。
示例:
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 异常的常用方式。
示例
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()
的第二个参数捕获:
示例:
// 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 语言的
malloc
、C++ 的
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 为例)
function fn() {// 基本类型变量:存栈中let num = 100; // 栈中直接存值 100let 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
的执行逻辑,帮助理解其底层实现:
// 模拟 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) // 失败:抛出错误并终止);}}// 启动 Generatorstep('next');});};
}// 用模拟的 async 函数实现一个异步场景
const fetchData = () => new Promise(resolve => {setTimeout(() => resolve('数据加载完成'), 1000);
});// 模拟 async/await 的使用
const mockAsyncFunc = asyncToGenerator(function* () {console.log('开始执行');const data = yield fetchData(); // 模拟 awaitconsole.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
:说明生成器处于关闭状态,不可再执行。
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)
方法,且函数体内未捕获该异常(异常抛出后关闭)。
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 为例)理解其工作机制:
// 外部函数
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
递增。
闭包的本质
闭包的本质是函数对其词法作用域的 “捕获”。无论函数被带到哪里执行,它始终能访问自己创建时所在的作用域中的变量,就像带着一个 “背包”,里面装着它需要的外部变量。
闭包的典型用途
-
保存状态:如上面的例子,通过闭包可以在函数调用之间保留变量的状态(类似 “私有变量”)。
-
模块化与封装:创建私有变量和方法,避免全局污染。例如:
function createCounter() {let count = 0; // 私有变量,外部无法直接访问return {increment: () => { count++; },getCount: () => count}; }const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 1(只能通过暴露的方法访问)
-
延迟执行:在定时器、事件监听等场景中,闭包可以记住当时的变量状态。例如:
for (var i = 0; i < 3; i++) {setTimeout(() => console.log(i), 100); // 若用var,会输出3个3 }// 用闭包解决: for (var i = 0; i < 3; i++) {(function(j) { // 闭包捕获每次循环的jsetTimeout(() => console.log(j), 100); // 输出0、1、2})(i); }
总结
闭包就是 “带着环境的函数”,它让函数突破了 “执行完就销毁内部变量” 的限制,能够记住并操作创建时的周边变量。这种特性让它在状态保存、封装、延迟执行等场景中非常有用,但也需要注意过度使用可能导致的内存占用问题(因为闭包会保留作用域,不及时释放可能造成内存泄漏)。
作用域闭包
https://www.yuque.com/ergz/web/gug7gq
深浅复制
https://www.yuque.com/ergz/web/sl6tdp
构造函数、原型和原型链
https://www.yuque.com/ergz/web/fi0l9t
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(已失败):操作完成但失败。
状态转换规则:
pending → fulfilled(不可逆转)
pending → rejected(不可逆转)
- 基本结构
Promise 本质是一个状态机,包含:
-
状态(state):初始为
pending
。 -
结果(value/reason):保存成功值或失败原因。
- 回调队列(then/catch 注册的回调):状态改变时触发。
二、实现 Promise 的核心逻辑
1. 基础框架
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.resolvestatic resolve(value) {if (value instanceof MyPromise) return value;return new MyPromise(resolve => resolve(value));}// 静态方法:Promise.rejectstatic reject(reason) {return new MyPromise((_, reject) => reject(reason));}// 静态方法:Promise.allstatic 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.racestatic 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 的结果唯一性。- 异步执行
then
和catch
的回调总是异步执行的,通过setTimeout
或queueMicrotask
实现。这保证了 Promise 的行为一致性。- 链式调用
then
和catch
方法返回新的 Promise,允许链式调用。前一个 Promise 的结果会传递给下一个。- 值穿透
如果
then
或catch
没有提供回调函数(如promise.then().then(handleValue)
),值会自动传递到下一个回调。- 错误冒泡
错误会一直向后传递,直到被
catch
捕获。四、静态方法实现
1. Promise.all
并行处理多个 Promise,返回一个新 Promise:
- 所有 Promise 成功时,结果为所有值的数组。
-
任何一个 Promise 失败时,立即返回该错误。
- Promise.race
返回一个 Promise,其结果为第一个完成(成功或失败)的 Promise 的结果。
- Promise.resolve/reject
快速创建已解决或已拒绝的 Promise。
五、与 ES6 Promise 的差异
上述实现是简化版,原生 ES6 Promise 还有以下特性:
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. 代码示例对比
// 定义几个测试用的 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 rejectedconsole.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
实现:
.parent {text-align: center;line-height: 200px; /* 等于容器高度 */height: 200px;
}
- 行内元素 / 文本(多行)
使用 Flexbox 或 Grid:
.parent {display: flex;justify-content: center;align-items: center;
}
- 块级元素(已知宽高)
使用绝对定位和负边距:
.parent {position: relative;
}
.child {position: absolute;top: 50%;left: 50%;width: 100px;height: 100px;margin-top: -50px; /* 高度的一半 */margin-left: -50px; /* 宽度的一半 */
}
- 块级元素(未知宽高)
使用绝对定位和 transform
:
.parent {position: relative;
}
.child {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);
}
- Flexbox(现代方案)
适用于大多数场景,简洁高效:
.parent {display: flex;justify-content: center;align-items: center;
}
- Grid(现代方案)
更强大的二维布局:
.parent {display: grid;place-items: center;
}
- 表格布局
使用 display: table-cell
:
.parent {display: table-cell;text-align: center;vertical-align: middle;
}
- 绝对定位 + 自适应
使用 top/left/bottom/right
和 margin: auto
:
.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 容器中,使其内外边距不再折叠。
<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,使其包裹浮动子元素。
.parent {overflow: hidden; /* 触发 BFC */ }
3. 阻止元素被浮动覆盖
-
问题:浮动元素会脱离文档流,覆盖后续的非浮动元素。
-
BFC 的解决方案:为被覆盖元素创建 BFC,使其与浮动元素隔离。
.non-float-element {overflow: hidden; /* 触发 BFC,不再被浮动元素覆盖 */ }
4. 自适应两栏/三栏布局
-
问题:实现一侧固定宽度、另一侧自适应的布局。
-
BFC 的解决方案:利用 BFC 区域不与浮动元素重叠的特性。
.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
,项目不会缩小,可能导致溢出。 -
实际压缩量计算公式:
项目压缩量 = (负空间 * flex-shrink值) / 所有项目的 (flex-shrink值 * flex-basis值) 总和
-
-
浏览器兼容性
- 现代浏览器全面支持 Flexbox,但旧版浏览器(如 IE 10/11)需加前缀
-ms-
。 - 使用 Autoprefixer 工具自动处理兼容性。
- 现代浏览器全面支持 Flexbox,但旧版浏览器(如 IE 10/11)需加前缀
四、最佳实践
-
优先使用
flex
简写:.item {flex: 1; /* 等价于 flex: 1 1 0 */`flex-grow`、`flex-shrink`、`flex-basis` }
-
固定侧边栏 + 自适应内容布局:
.sidebar { flex: 0 0 200px; } /* 固定宽度 */ .content { flex: 1; } /* 占满剩余空间 */
-
等高布局:
.container {display: flex;align-items: stretch; /* 默认值,项目高度一致 */ }
-
响应式导航栏:
.nav {display: flex;flex-wrap: wrap; /* 小屏幕自动换行 */justify-content: space-between; }
总结
- 核心思想:通过容器和项目的属性组合,实现灵活的空间分配和对齐。
- 适用场景:一维布局(如导航栏、卡片列表、表单控件等)。
- 进阶方向:结合 CSS Grid(二维布局)和媒体查询,构建复杂响应式设计。
掌握这些属性后,可以高效解决大多数布局问题,减少对浮动(float)和定位(position)的依赖。
网格布局
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
值。转换公式如下:
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):元素与其他元素之间的距离,不影响元素自身尺寸,但影响元素在页面中的位置。
宽度计算
总宽度 = width + padding-left + padding-right + border-left + border-right
总高度 = height + padding-top + padding-bottom + border-top + border-bottom
示例代码
.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):与标准模型相同,不影响元素自身尺寸。
宽度计算
总宽度 = width(包含padding和border) + margin-left + margin-right
总高度 = height(包含padding和border) + margin-top + margin-bottom
示例代码
.box {width: 200px; /* 内容区+内边距+边框的总宽度 */padding: 10px; /* 内边距:上下左右各10px */border: 2px solid; /* 边框:宽度2px */margin: 15px; /* 外边距:上下左右各15px */
}/* 内容区实际宽度 = 200 - 10*2 - 2*2 = 176px */
3. 盒子模型切换(box-sizing 属性)
-
标准模型(默认值)
box-sizing: content-box;
-
IE 模型(怪异模型)
box-sizing: border-box;
应用场景
- 响应式布局:使用
border-box
可避免因内边距导致布局溢出。 - 统一设计:所有元素设置
box-sizing: border-box
可简化尺寸计算。
/* 全局设置所有元素使用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:
* {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
:仅用于定义对象类型或类的接口,语法更简洁,专注于结构描述。interface User {name: string;age: number; }// 描述函数类型 interface SayHello {(name: string): string; }
-
type
:可定义任何类型(对象、基本类型、联合类型、交叉类型等),适用范围更广。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
关键字扩展,支持多继承。interface Animal {name: string; }interface Dog extends Animal {bark(): void; }// 多继承 interface Pet extends Animal, Dog {owner: string; }
-
type
:通过交叉类型(&
)实现扩展,不支持extends
关键字。type Animal = {name: string; };type Dog = Animal & {bark(): void; };
- 合并声明(Declaration Merging)
-
interface
:支持同名接口自动合并,属性和方法会被合并为一个接口。interface User {name: string; }interface User {age: number; }// 合并后:{ name: string; age: number } const user: User = { name: 'Alice', age: 20 };
-
type
:不支持合并,同名type
会报错。type 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)
泛型用于创建可复用的组件,支持多种类型而不丢失类型信息。
基本用法
// 泛型函数:接收任意类型并返回相同类型
function identity<T>(arg: T): T {return arg;
}// 自动推断类型
const num = identity(123); // 类型:number
const str = identity("hello"); // 类型:string// 显式指定类型
const bool = identity<boolean>(true); // 类型:boolean
泛型约束(限制泛型范围)
// 约束泛型必须有length属性
interface Lengthwise {length: number;
}function logLength<T extends Lengthwise>(arg: T): T {console.log(arg.length); // 合法,因为T被约束为有lengthreturn arg;
}logLength("abc"); // 正确(string有length)
logLength([1, 2, 3]); // 正确(array有length)
logLength(123); // 错误(number无length)
泛型接口 / 类
// 泛型接口
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)
表示多个类型中的一个,用 |
分隔。
type ID = string | number;function printID(id: ID) {console.log(id);
}printID("abc123"); // 正确
printID(456); // 正确
2. 交叉类型(Intersection Types)
表示合并多个类型,用 &
分隔(需同时满足所有类型)。
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 更精确地推断类型。
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
等)。
内置映射类型
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 }
自定义映射类型
// 将类型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
。
基本用法
// 判断T是否是Array类型
type IsArray<T> = T extends Array<any> ? "yes" : "no";type A = IsArray<number[]>; // "yes"
type B = IsArray<string>; // "no"
提取类型(配合 infer
)
infer
用于在条件类型中提取类型信息(类似变量声明)。
// 提取数组元素类型
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
配置)。
类装饰器
// 装饰器工厂:返回一个装饰器函数
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]"
方法装饰器
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 {@logMethodadd(a: number, b: number) {return a + b;}
}const calc = new Calculator();
calc.add(1, 2);
// 输出:
// "调用方法 add,参数: [1, 2]"
// "方法 add 返回: 3"
六、模块增强(Module Augmentation)
扩展已有模块的类型定义(例如给第三方库添加类型)。
// 扩展内置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博客