一、js基础
1、JavaScript有哪些数据类型,它们的区别?
JavaScript 的数据类型分为基本数据类型和引用数据类型,它们的主要区别在于存储方式和访问机制。以下是详细介绍:
基本数据类型(Primitive Types)
基本数据类型的值直接存储在栈内存中,并且是不可变的(immutable)。当你修改一个基本类型的值时,实际上是创建了一个新的值。
- undefined:变量已声明但未赋值,或函数没有返回值时,返回 undefined。
- null:表示一个空值,主动将变量赋值为 null。
- boolean:只有两个值:true 和 false。
- number:表示整数和浮点数,包括特殊值如 NaN(Not a Number)、Infinity 和 - Infinity。
- string:表示文本数据,由零个或多个 16 位 Unicode 字符组成。
- symbol(ES6 新增):表示独一无二的值,主要用于创建对象的私有属性或方法。
- bigint(ES2020 新增):表示任意精度的整数,可以安全地存储和操作大整数。
引用数据类型(Reference Types)
引用数据类型的值存储在堆内存中,而变量中存储的是对该值的引用(内存地址)。引用类型是可变的,多个变量可以引用同一个对象,对其中一个变量的修改会影响其他引用同一对象的变量。
- object:最基本的引用类型,可以包含任意数据类型的值,通过键值对的方式存储。
- array:特殊的对象,用于存储有序的数据集合,通过索引访问元素。
- function:特殊的对象,可执行代码块,可作为参数传递或返回值。
- date:用于处理日期和时间。
- regexp:用于处理正则表达式。
- map 和set(ES6 新增):键值对集合和唯一值集合。
主要区别
- 存储位置 :
- 基本类型存储在栈内存中,访问速度快。
- 引用类型存储在堆内存中,访问速度相对较慢。
- 赋值和传递 :
- 基本类型赋值时复制值本身,两个变量互不影响。
- 引用类型赋值时复制引用(内存地址),两个变量指向同一个对象,修改会相互影响。
- 比较方式 :
- 基本类型比较值是否相等。
- 引用类型比较引用是否相等(是否指向同一个对象)。
- 可变性 :
- 基本类型不可变,修改值会创建新值。
- 引用类型可变,可以直接修改对象的属性或方法。
2、数据类型检测的方式有哪些
一、typeof
操作符
作用 :返回一个表示数据类型的字符串。 适用场景 :检测基本数据类型(undefined
、null
除外)和函数。 局限性:
- 对
null
返回"object"
(历史遗留问题)。 - 无法区分对象、数组、正则等引用类型(均返回
"object"
)。
javascript
typeof undefined; // "undefined"
typeof null; // "object"(错误!)
typeof true; // "boolean"
typeof 42; // "number"
typeof "hello"; // "string"
typeof Symbol(); // "symbol"
typeof 123n; // "bigint"
typeof function() {}; // "function"
typeof []; // "object"
typeof {}; // "object"
typeof new Date(); // "object"
二、instanceof
操作符
作用 :判断对象是否是某个构造函数的实例(通过原型链检测)。 适用场景 :检测自定义对象类型或内置对象(如数组、日期等)。 局限性:
- 只能检测对象,对基本类型无效(需装箱成对象)。
- 跨窗口(iframe)检测时可能失效(不同窗口的构造函数不共享)。
javascript
[] instanceof Array; // true
[] instanceof Object; // true(所有对象都是 Object 的实例)
new Date() instanceof Date; // true
new Date() instanceof Object; // true
42 instanceof Number; // false(基本类型)
new Number(42) instanceof Number; // true(装箱对象)
三、Array.isArray()
方法
作用 :专门检测值是否为数组。 适用场景:判断变量是否为数组,跨窗口检测也有效。
javascript
Array.isArray([]); // true
Array.isArray({}); // false
Array.isArray(new Array()); // true
四、Object.prototype.toString.call()
方法
作用 :返回一个包含精确类型信息的字符串(格式为 [object Type]
)。 适用场景 :精确检测所有数据类型(包括内置对象和基本类型)。 优点:最准确、全面的类型检测方式。
javascript
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call("hello"); // "[object String]"
Object.prototype.toString.call(Symbol()); // "[object Symbol]"
Object.prototype.toString.call(123n); // "[object BigInt]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(function() {}); // "[object Function]"
Object.prototype.toString.call(/abc/); // "[object RegExp]"
Object.prototype.toString.call(new Map()); // "[object Map]"
五、constructor
属性
作用 :通过对象的 constructor
属性判断其构造函数。 适用场景 :检测对象的原始类型(如数组、日期等)。 局限性:
null
和undefined
没有constructor
属性。- 可以被修改(如
arr.constructor = Object
),导致检测失效。
javascript
[].constructor === Array; // true
(new Date()).constructor === Date; // true
(42).constructor === Number; // true(基本类型装箱后)
六、各方法对比表
方法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
typeof |
基本类型(除 null )和函数 |
简单快速 | 无法区分对象、数组等 |
instanceof |
自定义对象或内置对象 | 能检测原型链 | 对基本类型无效,跨窗口失效 |
Array.isArray() |
检测数组 | 专门针对数组,跨窗口有效 | 只能检测数组 |
Object.prototype.toString |
精确检测所有类型 | 最全面、准确 | 语法繁琐 |
constructor |
对象的构造函数检测 | 直接关联构造函数 | 可被修改,null/undefined 无效 |
3、null和undefined的区别
特性 | null |
undefined |
---|---|---|
语义 | 表示一个空对象引用,通常用于显式清空变量 | 表示变量已声明但未赋值 |
类型 | typeof null 返回 "object" |
typeof undefined 返回 "undefined" |
产生场景 | 手动赋值:let x = null; |
1. 变量声明但未赋值 2. 函数无返回值 3. 访问不存在的属性 4. 函数参数未传递 |
比较结果 | null === null → true null == undefined → true |
undefined === undefined → true undefined == null → true |
javascript
let a;
console.log(a); // undefined
let b = null;
console.log(b); // null
4、intanceof 操作符的实现原理及实现
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
javascript
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
5、为什么0.1+0.2 ! == 0.3,如何让其相等
0.1 + 0.2 !== 0.3
是因为计算机用二进制存储浮点数时,0.1
和 0.2
无法被精确表示,相加后产生微小误差(实际结果约为 0.30000000000000004
)。
让它们相等的简单方法:
放大倍数转整数计算(如 (0.1*10 + 0.2*10)/10 === 0.3
)。
6、如何获取安全的 undefined 值?
方法 | 代码示例 | 优点 | 适用场景 |
---|---|---|---|
未赋值变量 | let x; console.log(x); |
最直接、语义明确 | 函数内部临时变量 |
函数默认返回值 | const x = (() => {})(); |
无需依赖外部变量 | 立即执行函数表达式 |
void 操作符 |
const x = void 0; |
简洁、不受全局 undefined 污染 |
需显式返回 undefined 的场景 |
全局对象属性 | const x = window.undefined; |
直接引用全局环境的 undefined |
仅浏览器环境,不推荐 |
解构赋值默认值 | const { x = undefined } = {}; |
利用对象解构语法 | 复杂配置对象的默认 |
7、typeof NaN 的结果是什么?
NaN 指"不是一个数字"(not a number),NaN 是一个"警戒值"(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即"执行数学运算没有成功,这是失败后返回的结果"。
javascript
typeof NaN; // "number"
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。
8、isNaN 和 Number.isNaN 函数的区别?
-
函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
-
函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
9、Object.is() 与比较操作符 "==="、"==" 的区别?
- 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
- 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
- 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
10、object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
一、Object.assign()
vs 扩展运算符(...
)
维度 | Object.assign() |
扩展运算符(... ) |
---|---|---|
语法类型 | 函数调用:Object.assign(target, ...sources) |
对象 / 数组字面量:{ ...obj } 或 [...arr] |
目标对象行为 | 修改目标对象(返回修改后的目标对象) | 创建全新对象(不修改原对象) |
合并策略 | 后出现的属性覆盖前面的(相同键名) | 后出现的属性覆盖前面的(相同键名) |
处理嵌套对象 | 浅拷贝(仅复制引用,嵌套对象共享内存) | 浅拷贝(仅复制引用,嵌套对象共享内存) |
对 getter/setter 的处理 |
执行 getter 并赋值 (丢失 getter 定义) |
保留 getter/setter 定义 |
应用场景 | 对象合并(尤其需要修改目标对象时) | 创建新对象 / 数组(保持原数据不变) |
兼容性 | ES6(IE 需 polyfill) | ES6(IE 需转译工具) |
性能 | 略慢(函数调用开销) | 略快(字面量语法优化) |
二、浅拷贝 vs 深拷贝
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
复制层级 | 仅复制对象的一层属性 | 递归复制所有层级的属性 |
引用类型处理 | 复制引用(新旧对象共享子对象) | 创建新的子对象(完全独立) |
修改子对象的影响 | 会同时影响原对象和拷贝对象 | 仅影响当前对象 |
常见实现方式 | Object.assign() 、扩展运算符(... )、Array.slice() |
JSON.parse(JSON.stringify()) 、递归函数、lodash.cloneDeep |
性能 | 高(仅遍历一层) | 低(需递归遍历所有层级) |
适用场景 | 对象结构简单,无嵌套引用类型 | 对象包含深层嵌套的引用类型 |
11、map和Object的区别
维度 | Map |
普通对象(Object ) |
---|---|---|
键的类型 | 任意类型(包括对象、函数、null ) |
仅字符串或 Symbol |
键的顺序 | 按插入顺序迭代(for...of 、forEach ) |
不保证顺序(ES6+ 按创建顺序,但非标准行为) |
支持的遍历方法 | forEach 、for...of 、keys() 、values() |
for...in (需过滤)、Object.keys() + 数组方法 |
默认属性 | 仅内置方法(如 size 、set ) |
原型链上有默认属性(如 toString ) |
长度获取 | 直接通过 size 属性获取 |
需手动计算(如 Object.keys(obj).length ) |
性能(频繁增删) | 优(哈希表优化) | 一般(动态添加属性可能导致性能下降) |
序列化支持 | 不支持(JSON.stringify 会忽略) |
支持(自动序列化为 { key: value } ) |
适用场景 | 频繁增删键值对、需要键的顺序、键为引用类型 | 简单数据存储、需要与 JSON 互转 |
12、对JSON的理解
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,以简洁、易读的文本形式存储和表示数据,广泛用于前后端数据传输、配置文件、数据存储等场景。
一、JSON 的核心特性
-
文本格式 :JSON 本质是字符串,因此可以通过网络传输(如 HTTP 请求)或存储为文件(
.json
)。 -
语言无关
:几乎所有编程语言都提供 JSON 解析(将 JSON 文本转为对应语言的数据结构)和序列化(将数据结构转为 JSON 文本)的 API,例如:
- JavaScript:
JSON.parse()
(解析)、JSON.stringify()
(序列化)。 - Python:
json.loads()
、json.dumps()
。 - Java:
Jackson
、Gson
等库。
- JavaScript:
-
无类型信息 :JSON 仅存储数据值,不包含数据类型元信息(例如,
"2023-10-01"
在 JSON 中是字符串,解析后需手动转为日期类型)。
二、常见使用场景
- 前后端数据交互 :前端通过 AJAX/ Fetch 请求后端接口时,后端返回 JSON 格式数据,前端解析后使用(例如:
{ "code": 200, "data": { "name": "Tom" } }
)。 - 配置文件 :许多工具和框架用 JSON 作为配置文件(如
package.json
、tsconfig.json
)。 - 数据存储 :小型应用可将数据以 JSON 格式存储在本地(如
localStorage
中存储用户信息)。
三、JSON的缺点
-
数据类型少,不支持日期、函数等特殊类型,数字可能丢精度。
-
不能带注释,没法直接表示对象引用或循环关系。
-
文本格式体积大,处理大数据时性能较差。
-
语法严格(如键必须双引号),容易因格式问题报错。
13、JavaScript脚本延迟加载的方式有哪些?
方式 | 原理 | 特性 |
---|---|---|
defer 属性 | 在 <script> 标签中添加 defer ,使脚本与 HTML 并行下载 |
1. 不阻塞解析 2. 按标签顺序执行 3. 在 DOMContentLoaded 前完成 4. 仅适用于外部脚本 |
async 属性 | 在 <script> 标签中添加 async ,使脚本与 HTML 并行下载 |
1. 不阻塞解析 2. 下载完成后立即执行(可能乱序) 3. 执行时机不确定 4. 仅适用于外部脚本 |
动态创建 script | 通过 JavaScript 代码动态创建 script 元素并插入页面 |
1. 完全异步,不阻塞页面 2. 默认执行顺序可能混乱,可强制按顺序执行 3. 可按需触发加载 |
DOMContentLoaded | 监听 DOMContentLoaded 事件,在 HTML 解析完成后再加载脚本 |
1. 不影响页面初始渲染 2. 确保脚本在 DOM 结构完成后执行 3. 适用于需操作 DOM 的脚本 |
window.onload | 监听 window.onload 事件,在页面所有资源(图片、CSS)加载完成后再加载脚本 |
1. 延迟到页面完全渲染后执行 2. 不干扰首屏渲染 3. 可能导致脚本加载延迟较长 |
setTimeout 延迟 | 通过 setTimeout 函数设置延迟时间,在指定时间后加载脚本 |
1. 精确控制加载时机 2. 延迟时间需合理设置 3. 适用于非关键脚本或延迟触发的功能 |
文档底部脚本 | 将脚本放在 <body> 底部(</body> 标签前),确保页面主体内容先渲染 |
1. 兼容性好 2. 主体内容先渲染 3. 脚本执行仍可能阻塞 |
14、ajax、axios、fetch的区别
特性 | AJAX(XMLHttpRequest) | Fetch API | Axios |
---|---|---|---|
API 类型 | 原生 API(基于原生XHR开发) | 原生 API(基于 Promise) | 第三方库(基于 Promise) |
浏览器兼容性 | 高(IE7+) | 中等(IE 不支持,需 polyfill) | 中等(依赖 Promise,需 polyfill) |
Node.js 支持 | 不支持 | 需额外安装(如 node-fetch ) |
支持 |
默认发送 cookies | 是 | 否(需显式设置 credentials ) |
是(默认携带) |
拦截请求 / 响应 | 不支持(需手动实现) | 不支持(需自行封装) | 内置支持 |
取消请求 | 复杂(使用 abort() ) |
需使用 AbortController |
简单(使用 CancelToken ) |
查询文件上传进度 | 支持(通过 XMLHttpRequest.upload ) |
不直接支持(需手动计算) | 支持 |
自动转换 JSON | 否(需手动解析) | 是(但需调用 .json() ) |
是(自动解析响应数据) |
错误处理 | 复杂(需处理多种状态码和错误类型) | 仅网络错误 reject(状态码 404 仍 resolve) | 统一处理网络错误和 HTTP 错误 |
二、ES6
1、ES6新特性
描述 | 示例 | |
---|---|---|
let 和 const | 块级作用域的变量声明,const 声明常量(引用不可变) |
{ let x = 10; const y = 20; } // x 和 y 仅在块内有效 |
箭头函数 | 更简洁的函数语法,绑定词法作用域的 this |
const double = x => x * 2; const sum = (a, b) => a + b; |
解构赋值 | 快速提取数组 / 对象值到变量 | const [a, b] = [1, 2]; const { name, age } = { name: 'Bob' }; |
扩展运算符 | 展开数组 / 对象,或收集剩余参数 | const arr = [...[1, 2], 3]; const { ...rest } = { a: 1, b: 2 }; |
类与继承 | 基于原型的面向对象语法糖 | class Animal { speak() {} } class Dog extends Animal {} |
Promise | 异步编程解决方案,避免回调地狱 | fetchData().then(res => {}).catch(err => {}); |
模块化 | 标准化的模块导入 / 导出 | // 导出: export const add = () => {}; // 导入: import { add } from './math'; |
默认参数值 | 函数参数支持默认值 | function greet(name = 'Guest') { ... } |
模板字符串 | 支持变量插值和多行字符串 | const msg = \ Hello, ${name}!; |
for...of 循环 | 遍历可迭代对象(如数组、字符串) | for (const item of [1, 2, 3]) { ... } |
Map/Set | 键值对集合(Map )和唯一值集合(Set ) |
const map = new Map(); const set = new Set([1, 2, 2]); |
Proxy | 拦截并自定义对象的基本操作(如属性访问、赋值) | const handler = { get: (obj, prop) => ... }; const proxy = new Proxy(target, handler); |
Reflect | 提供对象操作的默认行为,与 Proxy 配合使用 |
Reflect.get(obj, 'prop'); Reflect.set(obj, 'prop', value); |
Symbol | 独一无二的值,用于创建私有属性 | const sym = Symbol('key'); obj[sym] = 'value'; |
Object.assign() | 合并对象属性 | const newObj = Object.assign({}, obj1, obj2); |
2、let、const、var的区别
特性 | let | const | var |
---|---|---|---|
作用域 | 块级作用域({} 内有效) |
块级作用域({} 内有效) |
函数作用域或全局作用域 |
变量提升 | 存在(TDZ 暂时性死区),但未初始化不可访问 | 存在(TDZ 暂时性死区),但未初始化不可访问 | 存在,可在声明前访问(值为 undefined ) |
重复声明 | 不允许(同一作用域) | 不允许(同一作用域) | 允许(会覆盖) |
重新赋值 | 允许 | 不允许(常量引用不可变) | 允许 |
初始值要求 | 不强制要求 | 必须初始化 | 不强制要求 |
3、箭头函数和普通函数的区别
特性 | 箭头函数 | 普通函数 |
---|---|---|
语法 | 更简洁,使用 => |
传统 function 关键字 |
this 指向 |
继承自外层作用域(词法作用域),不可修改 | 动态绑定,取决于调用方式(如 new 、call 、apply 、对象方法等) |
arguments 对象 |
没有自己的 arguments ,使用外层函数的 arguments |
拥有自己的 arguments 对象 |
构造函数 | 不能使用 new 调用,无 prototype 属性 |
可以使用 new 创建实例,有 prototype 属性 |
yield 关键字 |
不能使用(非 Generator 函数) | 可以使用(在 Generator 函数中) |
返回值 | 单行表达式自动返回,多行需显式 return |
必须显式 return (除非函数无内容) |
方法定义 | 不适合作为对象方法(因 this 问题) |
适合作为对象方法,可访问对象属性 |
4、解构赋值
类型 | 语法示例 | 特性说明 |
---|---|---|
数组解构 | const [a, b] = [1, 2]; |
- 按顺序匹配元素 - 可忽略元素:[a, , b] = [1, 2, 3] - 剩余参数:[a, ...rest] = [1, 2, 3] |
对象解构 | const { name, age } = { name: 'Alice', age: 30 }; |
- 按属性名匹配 - 重命名:{ name: userName } = obj - 嵌套解构:{ address: { city } } = user |
默认值 | const [a = 1, b = 2] = [undefined, 4]; const { hobby = 'Reading' } = user; |
- 仅当值为 undefined 时生效 - null 不触发默认值 |
函数参数解构 | function sum([a, b]) { ... } function printUser({ name, age }) { ... } |
- 简化参数处理 - 支持默认值:function greet({ name = 'Guest' } = {}) { ... } |
其他应用 | [a, b] = [b, a]; (交换变量) for (const [key, value] of map) { ... } (遍历 Map) |
- 快速交换变量值 - 解析函数返回的数组 / 对象 |
推荐场景:
场景 | 示例代码 |
---|---|
处理 API 返回数据 | const { code, data } = await fetchData(); |
简化函数参数 | function drawCircle({ x = 0, y = 0, radius = 100 }) { ... } |
提取 URL 参数 | const { protocol, host } = new URL('https://example.com'); |
解析复杂配置对象 | const { plugins: [firstPlugin] } = config; |
5、扩展运算符
类型 | 语法 | 特性说明 | 示例 |
---|---|---|---|
数组展开 | [...iterable] |
将可迭代对象(如数组、字符串)展开为多个元素 | const arr = [...[1, 2], 3]; // [1, 2, 3] const str = [...'hello']; // ['h', 'e', 'l', 'l', 'o'] |
对象展开 | {...object} |
复制对象的所有可枚举属性(ES2018+) | const obj = {...{ a: 1 }, b: 2 }; // { a: 1, b: 2 } |
函数参数收集 | function(...args) |
将剩余参数收集为数组 | function sum(...args) { return args.reduce((a, b) => a + b); } sum(1, 2, 3); // 6 |
解构赋值 | [a, ...rest] |
在解构中收集剩余元素 | const [a, ...res |
推荐场景:
场景 | 示例代码 |
---|---|
合并 / 复制数组 | const merged = [...arr1, 4, 5]; const clone = [...original]; |
传递参数给函数 | const args = [1, 2, 3]; func(...args); |
解构复杂对象 | const { a, ...rest } = obj; |
创建不可变对象 | const newState = {...state, prop: newValue }; |
6、Promise
Promise 是 JavaScript 中用于处理异步操作的一种对象,它代表一个异步操作的**最终完成(或失败)**及其结果值。其核心作用是解决传统异步编程中回调地狱(Callback Hell)的问题,使代码更易读和维护。
一、核心概念
-
三种状态:
- pending:初始状态,既未成功也未失败。
- fulfilled:操作成功完成。
- rejected:操作失败。
状态一旦改变(从
pending
变为fulfilled
或rejected
),就会被锁定,无法再修改。 -
不可变性:
- 状态改变后,Promise 被称为 "已解决"(resolved),此时其值固定不变。
二、基本用法
1. 创建 Promise
javascript
const promise = new Promise((resolve, reject) => {
// 异步操作(如网络请求、定时器)
setTimeout(() => {
const success = true; // 假设操作成功
if (success) {
resolve("操作成功"); // 传递成功结果
} else {
reject(new Error("操作失败")); // 传递失败原因
}
}, 1000);
});
2. 处理结果
通过 .then()
和 .catch()
处理 Promise 的结果:
javascript
promise
.then((result) => {
console.log(result); // 输出:"操作成功"
})
.catch((error) => {
console.error(error); // 处理错误
})
.finally(() => {
console.log("无论成功或失败都会执行");
});
三、关键特性
-
链式调用:
.then()
和.catch()
会返回一个新的 Promise,支持链式操作:jafetchData() .then(parseData) .then(saveData) .catch(handleError);
-
错误冒泡:
错误会跳过后续的
.then()
,直到遇到.catch()
:javascriptpromise .then(() => { throw new Error("Oops!"); }) .then(() => { /* 不会执行 */ }) .catch((error) => { console.error(error); }); // 捕获错误
-
静态方法:
Promise.all(iterable)
:并行处理多个 Promise,全部成功才返回结果。Promise.race(iterable)
:首个完成的 Promise 决定最终结果。Promise.resolve(value)
和Promise.reject(error)
:快速创建已解决的 Promise。
四、使用场景
-
替代回调函数:
javascript// 传统回调 fetchData((error, data) => { if (error) handleError(error); else processData(data); }); // Promise 版本 fetchData() .then(processData) .catch(handleError);
-
并行请求:
javascriptPromise.all([fetchUser(), fetchPosts()]) .then(([user, posts]) => { console.log(user, posts); });
-
超时控制:
javascriptPromise.race([ fetchData(), new Promise((_, reject) => setTimeout(() => reject("超时"), 3000)) ]);
五、与其他异步模式的关系
-
async/await:是 Promise 的语法糖,使异步代码更像同步代码:
javascriptasync function fetchData() { try { const response = await fetch("https://api.example.com"); const data = await response.json(); return data; } catch (error) { console.error(error); } }
-
Generator 函数:早期的异步流程控制方案,需配合 Promise 使用。
7、for in 和 for of的区别
以下是 JavaScript 中 for...in
和 for...of
的核心区别对比:
特性 | for...in |
for...of |
---|---|---|
循环对象 | 遍历对象的可枚举属性(键名) | 遍历可迭代对象的元素(值) |
适用类型 | 对象(Object)、数组(Array)等 | 数组(Array)、字符串(String)、Map、Set 等可迭代对象 |
遍历结果 | 返回属性名(键) | 返回元素值 |
是否遍历原型链 | 是(可能遍历到继承的属性) | 否(只遍历自身元素) |
8、Map、Set、WeekMap、WeekSet的区别
以下是 JavaScript 中 Map/Set/WeakMap/WeakSet 的核心特性对比表:
类型 | 存储结构 | 键的类型 | 是否可重复 | 是否弱引用 | 可迭代性 | 应用场景 |
---|---|---|---|---|---|---|
Map | 键值对集合 | 任意类型(对象、原始值) | 否(键唯一) | 否 | 可迭代 | - 缓存计算结果 - 需要任意类型键的字典 - 保持插入顺序的键值对 |
Set | 值的集合 | 任意类型(对象、原始值) | 否(值唯一) | 否 | 可迭代 | - 数组去重 - 快速查找存在性 - 数学集合操作(交集、并集) |
WeakMap | 弱引用键值对集合 | 仅对象 | 否(键唯一) | 是 | 不可迭代 | - 为 DOM 元素附加元数据 - 避免内存泄漏(键对象被垃圾回收时自动删除记录) |
WeakSet | 弱引用值的集合 | 仅对象 | 否(值唯一) | 是 | 不可迭代 | - 跟踪对象引用(不阻止对象被回收) - 存储临时对象 |
核心区别详解:
-
强/弱引用机制:
-
强引用:当你创建一个对象并赋值给变量时,变量对对象的引用就是「强引用」。只要强引用存在,JavaScript 的垃圾回收器(GC)就不会回收这个对象,即使它在程序中已经不再被使用。
-
弱引用:不影响垃圾回收的引用。如果一个对象仅被弱引用指向,当它没有其他强引用时,垃圾回收器会直接回收这个对象,无视弱引用的存在。
-
-
迭代与遍历:
- Map/Set 支持
for...of
、forEach
,可获取迭代器。 - WeakMap/WeakSet 不可迭代,无
size
属性,仅支持get/set/has/delete
。
- Map/Set 支持
-
键的类型限制:
- WeakMap/WeakSet 的键 / 值必须是对象,不能是原始值。
三、原型与原型链
1、原型
在 JavaScript 中,原型(Prototype) 是对象的一个内置属性(__proto__
),用于实现对象之间的属性和方法共享,是 JavaScript 实现继承的核心机制。
2、原型链
在 JavaScript 中,原型链(Prototype Chain) 是实现继承的核心机制,它允许对象继承其他对象的属性和方法。其本质是多个对象通过 __proto__
属性串联形成的层级结构。
3、new操作符的实现原理
一、new
的核心工作流程
当执行 new Constructor(arg1, arg2)
时,JavaScript 引擎会按以下步骤执行:
- 创建新对象 创建一个空对象,该对象的
[[Prototype]]
指向Constructor.prototype
。 - 绑定
this
将构造函数的this
指向新对象,并执行构造函数代码(为新对象添加属性)。 - 处理返回值
- 若构造函数返回对象类型(如对象、数组、函数等),则返回该对象。
- 否则,返回新创建的对象。
二、手动实现 new
操作符
javascript
function myNew(Constructor, ...args) {
// 1. 创建新对象,将其原型指向构造函数的 prototype
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,绑定 this 到新对象
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回对象类型,则返回该对象;否则返回新创建的对象
return typeof result === 'object' && result !== null ? result : obj;
}
// 使用示例
function Person(name) {
this.name = name;
// 默认返回 undefined,因此 myNew 会返回新创建的对象
}
const alice = myNew(Person, "Alice");
console.log(alice.name); // "Alice"
console.log(alice instanceof Person); // true
四、执行上下文/作用域链/闭包
1、执行上下文
在 JavaScript 中,执行上下文(Execution Context) 是代码执行时的「环境容器」,它定义了变量和函数的作用域,并决定了 this
的指向。执行上下文是 JavaScript 引擎管理代码执行的核心机制,理解它是掌握变量提升、作用域链、闭包等概念的基础。
一、执行上下文的类型
JavaScript 中有三种执行上下文:
- 全局执行上下文
- 是最基础的执行上下文,整个程序中仅存在一个。
- 在浏览器中,全局上下文的
this
指向window
对象,并初始化全局变量(如document
、console
)。 - 程序启动时创建,页面关闭时销毁。
- 函数执行上下文
- 每次调用函数时,都会创建一个新的函数执行上下文。
- 每个函数执行上下文独立管理其内部变量、参数和
this
指向。 - 函数执行完毕后,其执行上下文被销毁(除非存在闭包引用)。
- eval 执行上下文
- 由
eval()
函数执行代码时创建(极少使用,因安全性和性能问题)。
- 由
二、执行上下文的生命周期
每个执行上下文的生命周期分为三个阶段:
- 创建阶段
在代码执行前,JavaScript 引擎会创建执行上下文并初始化以下内容:
- 变量对象(Variable Object) 存储变量、函数声明和参数(
arguments
对象)。- 对于全局上下文,变量对象是
window
。 - 对于函数上下文,变量对象是
活动对象(Activation Object)
。
- 对于全局上下文,变量对象是
- 作用域链(Scope Chain) 由当前执行上下文的变量对象和所有父级执行上下文的变量对象组成,用于变量查找。
- this 指向 根据函数的调用方式确定
this
的值(如全局调用指向window
,对象方法调用指向对象本身)。
- 执行阶段
JavaScript 引擎开始逐行执行代码,主要工作包括:
- 变量赋值(在创建阶段仅声明,未赋值)。
- 函数调用(创建新的执行上下文并压入执行栈)。
- 执行其他语句(如条件判断、循环等)。
- 销毁阶段
执行上下文的变量和内存被释放,执行栈弹出该上下文:
- 全局执行上下文在页面关闭时销毁。
- 函数执行上下文在函数返回后销毁(若存在闭包,则闭包会保留对变量的引用)。
三、执行上下文栈(调用栈)
JavaScript 引擎使用「执行上下文栈」(也称为「调用栈」)来管理执行上下文的顺序:
- 程序启动时,全局执行上下文首先入栈。
- 每次调用函数时,新的函数执行上下文入栈,并成为当前执行上下文。
- 函数执行完毕后,其执行上下文出栈,控制权交回调用它的上下文。
- 执行栈遵循「后进先出(LIFO)」原则。
2、对作用域、作用域链的理解
一、 作用域的类型
作用域类型 | 定义场景 | 可访问范围 | 特点 |
---|---|---|---|
全局作用域 | 最外层代码(不在任何函数 / 块中) | 整个程序(所有函数和代码块内均可访问) | 全局变量在页面生命周期内一直存在,浏览器中挂载在 window 对象上 |
函数作用域 | 函数内部声明的变量 / 函数 | 仅在当前函数内部可访问(包括嵌套函数) | 函数执行时创建,执行完毕后销毁(除非被闭包引用) |
块级作用域 | {} 包裹的代码块(如 if 、for 、switch 或独立块) |
仅在当前代码块内可访问 | 由 let /const 声明的变量才有块级作用域,var 不支持(会 |
二、作用域链
核心定义
当代码在某个作用域(如函数内部、代码块中)访问变量时,JavaScript 引擎会先在当前作用域中查找;若未找到,则自动向上查找其「父级作用域」,再找不到就继续向上,直到全局作用域。这种从当前作用域到全局作用域的层级链条,就是作用域链。
意义
- 隔离与访问平衡:既通过作用域隔离了不同层级的变量(避免冲突),又通过链条保证了合理的跨层级访问(如内层函数访问外层变量)。
- 闭包的基础:当内层函数被外部引用时,作用域链会保留对父级作用域的引用,使得父级变量在函数执行后仍可被访问(这就是闭包的实现原理)。
3、闭包
一、核心定义
闭包是指有权访问另一个函数作用域中的变量的函数,即使该函数已执行完毕,其作用域内的变量也不会被销毁。闭包的本质是函数与其词法环境的引用捆绑,它会捕获并保留函数定义时所处的外部变量。
二、形成条件
- 函数嵌套:内部函数定义在外部函数内部。
- 内部函数引用外部变量:内部函数使用了外部函数作用域中的变量。
- 内部函数被外部引用:内部函数被返回或传递到外部,在其原始作用域之外执行。
三、闭包的关键特性
- 捕获并保留变量
闭包会捕获其定义时所处的外部变量,而非变量的值。这意味着即使外部函数已执行完毕,闭包仍能访问和修改这些变量。
javascript
function outer() {
let count = 0;
return function inner() {
count++; // 闭包捕获并修改外部变量
console.log(count);
};
}
const counter = outer(); // outer 执行完毕,但 count 被闭包保留
counter(); // 1
counter(); // 2(count 持续累加)
- 变量隔离与数据封装
闭包可用于创建私有变量和方法,实现数据封装和模块化。
javascript
function createPerson() {
let name = "Alice"; // 私有变量,外部无法直接访问
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
};
}
const person = createPerson();
console.log(person.getName()); // "Alice"
person.setName("Bob");
console.log(person.getName()); // "Bob"
- 延迟执行的上下文保留
闭包会保留其创建时的整个词法环境,包括所有变量的状态。
javascript
function createTimers() {
const timers = [];
for (var i = 0; i < 3; i++) {
timers.push(function() {
console.log(i); // 闭包捕获的是变量 i 的引用,而非值
});
}
return timers;
}
const [timer1, timer2, timer3] = createTimers();
timer1(); // 3(循环结束时 i 已变为 3)
timer2(); // 3
timer3(); // 3
四、闭包的常见应用场景
-
数据封装与私有变量: 通过闭包实现模块模式,隐藏内部状态,仅暴露公共接口。
javascriptconst counterModule = (function() { let count = 0; return { increment: () => count++, getCount: () => count }; })();
-
事件处理与回调: 在事件处理函数中保留上下文数据。
javascriptfunction setupButton() { const text = "Hello"; document.querySelector("button").addEventListener("click", () => { console.log(text); // 闭包捕获 text }); }
-
函数柯里化(Currying): 将多参数函数转换为一系列单参数函数。
javascriptfunction add(a, b) { return a + b; } const curriedAdd = a => b => a + b; curriedAdd(3)(5); // 8
-
延迟执行与异步操作: 在异步回调中保留变量状态。
javascriptfunction fetchData() { const url = "https://api.example.com/data"; return function() { fetch(url).then(response => response.json()); }; }
-
防抖
javascript
function debounce(func, wait) { let timeout; return function(...args) {
ini
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
php
6. **节流**
```javascript
/**
* 节流函数
* @param {Function} func - 需要节流的函数
* @param {number} wait - 时间间隔(毫秒),表示在这个时间间隔内最多执行一次函数
* @returns {Function} - 返回一个节流后的函数
*/
function throttle(func, wait) {
// 上一次执行函数的时间戳,初始值为 0
let lastTime = 0;
// 返回一个闭包函数,作为节流后的函数
return function (...args) {
// 获取当前时间戳
const now = Date.now();
// 如果当前时间与上一次执行时间的差值大于等于 wait,则执行函数
if (now - lastTime >= wait) {
// 更新上一次执行函数的时间戳
lastTime = now;
// 调用原始函数,并传入参数
func.apply(this, args);
}
};
}
五、this/call/apply/bind
1、this
在 JavaScript 中,this
是一个特殊的关键字,其指向在函数执行时动态确定 ,取决于函数的调用方式,而非定义位置。这使得 this
的行为既灵活又容易混淆,是 JavaScript 的核心难点之一。
一、this
的基本规则
this
的指向由函数的调用方式决定,主要分为以下五种场景:
- 全局作用域中的
this
在全局作用域(任何函数外部)中,this
指向全局对象:
- 在浏览器中,全局对象是
window
。 - 在 Node.js 中,全局对象是
global
。
javascript
console.log(this === window); // true(浏览器环境)
var globalVar = 'hello';
console.log(this.globalVar); // 'hello'(全局变量是全局对象的属性)
- 函数调用中的
this
普通函数直接调用时,this
指向全局对象 (非严格模式)或 undefined
(严格模式)。
示例:
javascript
function showThis() {
console.log(this);
}
showThis(); // window(非严格模式)
// undefined(严格模式,'use strict')
- 方法调用中的
this
当函数作为对象的方法调用时,this
指向调用该方法的对象。
javascript
const obj = {
name: 'Alice',
greet() {
console.log(this.name); // 'Alice'
}
};
obj.greet(); // this 指向 obj
- 构造函数调用中的
this
使用 new
关键字调用函数时,this
指向新创建的实例对象。
javascript
function Person(name) {
this.name = name; // this 指向新实例
}
const p = new Person('Bob');
console.log(p.name); // 'Bob'
- 箭头函数中的
this
箭头函数不绑定自己的 this
,而是捕获其定义时的上下文。
javascript
const obj = {
name: 'Alice',
greet: () => {
console.log(this.name); // undefined(箭头函数的 this 来自全局)
}
};
二、this
的复杂场景
- 嵌套函数的
this
嵌套函数的 this
不会继承外层函数的 this
,默认指向全局对象(非严格模式)。
javascript
const obj = {
name: 'Alice',
nested() {
function inner() {
console.log(this.name); // undefined(this 指向 window)
}
inner();
}
};
obj.nested();
解决方案:
-
使用箭头函数(继承外层
this
):javascriptnested() { const inner = () => { console.log(this.name); // 'Alice' }; inner(); }
-
保存外层this到变量(如
that
或self
):javascriptnested() { const that = this; function inner() { console.log(that.name); // 'Alice' } inner(); }
- 回调函数的
this
回调函数的 this
通常指向全局对象(或 undefined
,严格模式)。
javascript
const obj = {
name: 'Alice',
init() {
document.querySelector('button').addEventListener('click', function() {
console.log(this.name); // undefined(this 指向 DOM 元素)
});
}
};
解决方案:
-
使用箭头函数:
javascriptaddEventListener('click', () => { console.log(this.name); // 'Alice' });
-
使用 bind() 方法:
javascriptaddEventListener('click', function() { console.log(this.name); }.bind(this)); // 绑定 this 到 obj
- 显式修改
this
(call
/apply
/bind
)
通过 Function.prototype
的方法显式指定 this
:
call(thisArg, arg1, arg2, ...)
:立即执行函数,绑定this
并传递参数。apply(thisArg, [argsArray])
:立即执行函数,绑定this
并以数组形式传递参数。bind(thisArg, arg1, arg2, ...)
:返回一个新函数,永久绑定this
和预设参数。
javascript
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: 'Bob' };
greet.call(person, 'Hello'); // 'Hello, Bob'
greet.apply(person, ['Hi']); // 'Hi, Bob'
const greetBob = greet.bind(person, 'Hey');
greetBob(); // 'Hey, Bob'
六、异步编程
1、异步编程的实现方式?
方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
回调函数 | 简单异步操作,Node.js 原生 API | 简单直接 | 回调地狱,错误处理困难 |
Promise | 链式异步操作,现代 JavaScript 库 | 避免回调地狱,统一的错误处理 | 链式调用仍有嵌套,语法相对复杂 |
async/await | 复杂异步流程,需要同步风格的代码 | 代码可读性高,错误处理简单 | 依赖 Promise,调试可能复杂 |
事件监听 | DOM 事件、实时数据流 | 松耦合,适合一对多通信 | 事件管理复杂,可能导致全局状态 |
发布 - 订阅 | 组件间通信,状态管理 | 解耦组件,提高可维护性 | 需要额外的事件总线实现 |
生成器 | 复杂异步流程控制(较少使用) | 灵活的流程控制 | 语法复杂,需要辅助执行器 |
Web Workers | 耗时计算,不阻塞主线程 | 利用多核 CPU | 跨线程通信复杂 |
2、回调函数(Callback)、Promise、async/await
维度 | 回调函数(Callback) | Promise | async/await |
---|---|---|---|
本质 | 函数作为参数传递,异步操作完成后调用 | 状态机对象(pending → fulfilled/rejected) | 基于 Promise 的语法糖 |
错误处理 | 通过回调参数传递错误(如 err ) |
.catch() 捕获错误 |
try...catch 结构 |
链式调用 | 多层嵌套导致回调地狱 | 链式调用(.then().then() ) |
同步风格的顺序执行 |
代码可读性 | 嵌套层级深时极差 | 中等(链式调用仍有嵌套) | 高(类似同步代码) |
执行顺序 | 依赖回调嵌套顺序 | 严格按 .then() 顺序执行 |
按 await 顺序阻塞执行 |
异常捕获范围 | 单个回调内的错误无法跨层级捕获 | 整个 Promise 链的错误可统一捕获 | 单个 try...catch 可捕获多个异步操作 |
七、垃圾回收与内存泄漏
1. 浏览器的垃圾回收机制
(1)垃圾回收的概念
垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
- Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
- 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
(2)垃圾回收的方式
浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。 1)标记清除
- 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量"进入环境",被标记为"进入环境"的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为"离开环境",被标记为"离开环境"的变量会被内存释放。
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
2)引用计数
- 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
- 这种方法会引起循环引用 的问题:例如:
obj1
和obj2
通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1
和obj2
还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
javascript
javascript 体验AI代码助手 代码解读复制代码function fun() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
这种情况下,就要手动释放变量占用的内存:
javascript
javascript 体验AI代码助手 代码解读复制代码obj1.a = null
obj2.a = null
(3)减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
- 对
object
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 - 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
2. 哪些情况会导致内存泄漏
以下四种情况会造成内存的泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
八、浏览器渲染机制
一、渲染的核心步骤
- 解析内容,构建基础结构
- 浏览器首先读取 HTML 文件,按照标签的嵌套关系,构建出一个树形结构(称为DOM 树),就像一本书的目录,记录了所有内容的层级和关系。
- 同时读取 CSS 文件(包括内联样式、外部样式),分析样式规则,构建出CSSOM 树(样式规则树),记录每个元素应该如何显示(如颜色、大小、位置等)。
- 结合结构与样式,生成渲染树
- 浏览器将 DOM 树和 CSSOM 树合并,生成渲染树。
- 渲染树只包含可见元素(比如
display: none
的元素会被排除),并且每个元素都附带了对应的样式规则。
- 计算布局(排版)
- 基于渲染树,浏览器计算每个元素的几何信息:包括位置(在屏幕上的坐标)、大小(宽度、高度)、边距、行距等。
- 这个过程类似排版书籍,确定每个段落、图片的具体位置和占用空间。
- 绘制元素
- 按照布局结果,浏览器将元素 "画" 到屏幕上,包括填充颜色、绘制文字、渲染图片、添加边框或阴影等视觉效果。
- 复杂页面会分成多个 "图层" 分别绘制(比如视频、动画元素单独成层),提高效率。
- 合成图层,展示最终画面
- 把所有绘制好的图层按照正确的顺序叠加、合并,最终形成完整的页面,显示在屏幕上。
二、影响性能的关键概念
- 重排(布局重计算)
- 当元素的位置、大小、结构发生变化时(比如修改窗口大小、添加删除元素),浏览器需要重新计算布局,这个过程称为 "重排"。
- 重排成本较高,可能导致页面卡顿(比如频繁修改元素尺寸时)。
- 重绘(重新上色)
- 当元素的样式变化但不影响布局时(比如修改颜色、背景),浏览器只需重新绘制该元素,称为 "重绘"。
- 重绘成本低于重排,但频繁重绘仍会影响性能(比如快速切换文字颜色)。
- 图层合成
- 浏览器会把复杂页面拆分成多个 "图层"(比如固定定位的导航栏、动画元素),每个图层独立渲染。
- 这样修改某个图层时,只需重新处理该图层,再合并到整体画面,减少对其他部分的影响(比如滑动时,只有内容区图层需要更新)。