题目总数:60
JavaScript 高频面试题的总结
一、数据类型与类型检测
-
基本数据类型
string、number、boolean、undefined、null(typeof null返回"object",历史遗留问题)、symbol(ES6,唯一标识符)、bigint(ES10,大整数)。 -
引用数据类型
Object(包含数组、函数、日期等)。 -
类型检测方法
typeof:检测基本类型,无法区分null与对象(如typeof []返回"object")。instanceof:基于原型链判断(如[] instanceof Array返回true)。Object.prototype.toString.call():精确检测类型(如返回[object Array])。
二、变量声明与作用域
-
var、let、const 区别
- var:函数作用域,允许重复声明,存在变量提升。
- let/const:块级作用域,不可重复声明,无变量提升;
const声明后不可重新赋值。
-
变量提升
var声明的变量在代码执行前被提升至作用域顶部(值为undefined)。函数声明整体提升,优先于变量声明。
-
作用域链
函数嵌套时,内部函数通过作用域链访问外层变量,形成链式结构(如闭包)。
三、闭包
-
定义与作用
- 函数嵌套函数,内部函数引用外部函数的变量,形成闭包,
延长变量生命周期。 - 作用:
封装私有变量、实现柯里化、模块化开发(如工厂函数)。
- 函数嵌套函数,内部函数引用外部函数的变量,形成闭包,
-
内存泄漏风险
闭包可能导致外部函数变量无法被垃圾回收,需手动解除引用,使用弱引用(WeakMap)优化使用弱引用(WeakMap)优化。
示例:
function outer() {
let count = 0;
return function() { return ++count; };
}
const counter = outer();
console.log(counter()); // 1
四、原型与原型链
-
原型对象
- 每个函数都有
prototype属性,用于共享方法。 - 实例对象的
__proto__指向构造函数的prototype(如obj.__proto__ === Object.prototype)。
- 每个函数都有
-
原型链
对象通过__proto__链式查找属性,终点为null(如obj → Object.prototype → null)。
五、this 指向与箭头函数
-
this 动态绑定
- 普通函数:由调用方式决定(如
obj.fn()中this指向obj)。 - 箭头函数:继承外层作用域的
this,无法通过call/apply修改。
- 普通函数:由调用方式决定(如
-
修改 this 的方法
call、apply:立即执行函数并指定this(参数传递方式不同)。bind:返回绑定this的新函数,支持柯里化。
六、异步与事件循环
- 宏任务与微任务
- 宏任务:
setTimeout、setInterval、I/O 操作。 - 微任务:
Promise.then、MutationObserver。 - 执行顺序:同步代码 → 微任务 → 宏任务。
- 宏任务:
示例:
setTimeout(() => console.log(1), 0);
Promise.resolve().then(() => console.log(2));
// 输出顺序:2 → 1
七、深拷贝与浅拷贝
-
浅拷贝
- 仅复制第一层属性(如
Object.assign、展开运算符...)。 - 引用类型属性共享地址,修改影响原对象。
- 仅复制第一层属性(如
-
深拷贝
- 递归复制所有层级(需处理循环引用)。
- 方法:
JSON.parse(JSON.stringify(obj))(忽略函数/Symbol)、lodash.cloneDeep。
八、垃圾回收机制
- 标记清除(主流)
标记不再被引用的对象并清除。
- 分代回收:V8 引擎将内存分为新生代(Scavenge 算法)和老生代(标记-整理)。
- 引用计数(已淘汰)
循环引用时无法回收,导致内存泄漏。
九、ES6+ 新特性
-
模块化
- ES6 Module:静态导入/导出(
import/export),支持 Tree Shaking。 - CommonJS:动态加载(
require),适用于 Node.js。
- ES6 Module:静态导入/导出(
-
其他特性
Promise:解决回调地狱。async/await:同步化异步代码。
以上为 JavaScript 高频面试题的精选总结,更多细节可参考相关来源。
刷新页面JS请求一般会有哪些地方有缓存处理
- 浏览器缓存(HTTP 缓存)
- CDN 或反向代理缓存
- DNS缓存
- Service Worker 缓存
- 代码层的缓存处理
- 第三方库或框架的缓存
在 Web 开发中,刷新页面时 JavaScript 请求的缓存处理可能存在于以下多个环节。理解这些缓存机制,可以帮助你优化性能或避免缓存导致的数据不一致问题:
1. 浏览器缓存(HTTP 缓存)
浏览器会根据 HTTP 协议的缓存策略自动缓存资源,JS 请求的响应结果可能被缓存:
-
强缓存
- 通过
Cache-Control(优先级更高)或Expires响应头控制。 - 示例:
Cache-Control: max-age=3600 # 缓存 1 小时 Expires: Wed, 21 Oct 2023 07:28:00 GMT - 表现:刷新页面时,直接从本地缓存读取,不发送请求(状态码
200 (from disk cache))。“from cache” :意为该响应内容来自缓存,而非服务器重新生成并返回。说明浏览器或代理服务器在本地缓存中找到了与请求匹配的资源,直接从缓存中获取并返回给客户端,没有向原始服务器发起新的请求
- 通过
-
协商缓存
- 通过
ETag(响应头)和If-None-Match(请求头) 或Last-Modified(响应头)和If-Modified-Since(请求头)控制。 - 表现:刷新页面时,浏览器会向服务器发起请求,若资源未修改,返回
304 Not Modified,使用本地缓存。
- 通过
2. 代码层的缓存处理 mark
开发者可能在代码中手动缓存请求结果,需检查以下场景:
-
全局变量或内存缓存
let cachedData; // 全局变量缓存 async function fetchData() { if (!cachedData) { cachedData = await fetch('/api/data').then(res => res.json()); } return cachedData; } -
闭包或模块缓存
const cache = new Map(); // 模块级缓存 function getData(url) { if (cache.has(url)) return cache.get(url); const data = await fetch(url); cache.set(url, data); return data; } -
Promise 复用(防止重复请求)
const pendingRequests = new Map(); function fetchData(url) { if (pendingRequests.has(url)) { return pendingRequests.get(url); // 返回已存在的 Promise } const promise = fetch(url).finally(() => pendingRequests.delete(url)); pendingRequests.set(url, promise); return promise; }
3. 第三方库或框架的缓存 mark
-
Axios 缓存适配器
使用axios-cache-adapter等库时,可能自动缓存 GET 请求:import { setupCache } from 'axios-cache-adapter'; const api = axios.create({ adapter: setupCache({ maxAge: 15 * 60 * 1000 }).adapter, }); -
React Query / SWR
前端框架的请求库默认会缓存数据:// React Query 示例 const { data } = useQuery('todos', fetchTodoList); // 自动缓存
4. Service Worker 缓存
若页面注册了 Service Worker,可能通过 Cache API 拦截并缓存请求:
// Service Worker 代码
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request); // 优先返回缓存
})
);
});
5. CDN 或反向代理缓存
- CDN 缓存
静态资源(如 JS 文件)可能被 CDN 缓存,通过 URL 添加哈希避免缓存问题:<script src="/app.js?v=abcd1234"></script> - 反向代理缓存(如 Nginx)
代理服务器可能缓存 API 响应,需设置Cache-Control: private或no-cache避免敏感数据被缓存。
6. 浏览器开发者工具的干扰
- 禁用缓存选项:
若浏览器开发者工具中勾选了 Disable Cache,可能导致缓存行为与实际用户不一致。
总结
| 缓存位置 | 控制方式 | 典型场景 |
|---|---|---|
| 浏览器 HTTP 缓存 | Cache-Control、ETag 等响应头 | 静态资源、重复 API 响应 |
| 代码内存缓存 | 变量、Map、Promise 复用 | SPA 全局状态管理 |
| 请求库/框架 | Axios 适配器、React Query | 自动化数据缓存 |
| Service Worker | Cache API | PWA 离线可用性 |
| CDN/代理 | URL 哈希、响应头配置 | 静态资源加速 |
根据实际需求选择缓存策略,必要时通过 URL 随机参数 或 HTTP 头 强制绕过缓存。
浏览器架构
1.用户界面
2.浏览器引擎(负责窗口管理、Tab进程管理等)
3.渲染引擎(又叫内核,负责HTML解析、页面渲染)
4.JS引擎(JS解释器,如Chrome和Nodejs采用的V8)
浏览器架构是指浏览器的组成部分及其相互关系,一般由以下主要部分构成:
一、用户界面(Shell)
- 组成元素:包括地址栏、前进 / 后退按钮、书签菜单、工具栏、智能下载处理、首选项、打印等除浏览器主窗口显示页面外的部分。用户通过这些界面元素与浏览器进行交互,比如在地址栏输入网址,使用前进 / 后退按钮切换浏览历史记录。
- 功能作用:负责接收用户输入,然后将输入传输到浏览器引擎进行处理。部分浏览器还支持通过扩展和主题来自定义用户界面,提升用户个性化体验。
二、浏览器引擎
- 功能定位:作为用户界面和渲染引擎之间的桥梁,提供了二者交互的 API 。它主要负责处理用户输入以及渲染请求。
- 工作流程:当用户在 UI 上输入 URL 时,浏览器引擎会向网络层发出请求,以获取指定 URL 的内容。在获取内容后,协调渲染引擎进行页面渲染相关工作。
三、渲染引擎
- 核心职责:是浏览器的核心组件之一,负责将浏览器引擎请求到的内容(如 HTML、XML、图像等网页资源)渲染成用户界面。具体包括解析 HTML 构建 DOM 树,解析 CSS 形成 CSSOM 树,结合二者进行布局计算,以及将渲染树转化为最终的像素画面展示在屏幕上。
- 涉及技术:渲染引擎能够处理 HTML、CSS、JavaScript 等内容。例如,通过 HTML 解析器将 HTML 文档解析成文档对象模型(DOM)树;使用 CSS 对象模型(CSSOM)表达应用于 HTML 元素的样式,并考虑样式特殊性和级联性;执行 JavaScript 代码来动态修改 DOM 和 CSSOM,实现页面交互和动态更新。不同浏览器的渲染引擎不同,如 Firefox 浏览器使用 Geoko(Mozilla 自主研发),Safari 和早期 Chrome 使用 webkit (后来 Chrome 推出了 Blink) 。
四、JavaScript 解释器
- 功能:能够解释并执行嵌入在网页中的 JavaScript(又称 ECMAScript)代码。网页中的 JavaScript 代码可以实现页面交互、数据获取及界面更新等功能,JavaScript 解释器确保这些代码能够正确执行。
- 举例:例如 Chrome 浏览器中的 V8 引擎,它可以高效地解析和执行 JavaScript 代码,并且具备即时编译(JIT)等优化技术,提升代码执行性能。
五、XML 解析器
- 作用:可以将 XML 文档解析成文档对象模型(DOM)树。在浏览器中,很多地方可能会用到 XML 格式的数据,XML 解析器将其解析以便浏览器进行后续处理。
- 特点:是浏览器架构中复用最多的子系统之一,多数浏览器实现都利用现有的 XML 解析器,而非重新开发。
六、网络层
- 通信任务:负责在浏览器引擎请求内容时和服务器进行通信。使用 HTTP、FTP、TCP、UDP 等各种协议与服务器通信,处理 Cookie、SSL/TLS 等相关功能。
- 工作过程:在用户输入 URL 后,网络层进行域名解析,将主机名转换为 IP 地址,然后建立与服务器的连接,发送 HTTP 请求获取网页资源,并接收服务器返回的响应数据。同时,网络层还可以实现最近检索资源的缓存功能,在不同字符集之间进行转换,为文件解析 MIME 媒体类型。
七、数据存储
- 功能:将与浏览会话相关联的各种数据存储在硬盘上,比如浏览器缓存、Cookie、本地存储(localStorage)、会话存储(sessionStorage)等。
- 用途:浏览器缓存可以存储网页资源,加快后续访问速度;Cookie 用于存储网站相关信息,如用户登录状态等;localStorage 用于长期存储数据,在浏览器关闭后数据依然存在;sessionStorage 用于在当前会话中存储数据,会话结束(如关闭标签页)后数据消失。
八、其他组件
- GPU 进程:最初是为了实现 3D CSS 效果,后来扩展到绘制网页和浏览器 UI 界面。通过独立的 GPU 进程进行图形绘制操作,可利用 GPU 的并行计算能力提高渲染性能,减轻 CPU 负担。
- 插件进程:负责对插件(如早期的 Flash、Java 插件等)的运行和管理,不过随着技术发展,很多插件逐渐被淘汰。插件可以扩展浏览器功能,但也可能存在稳定性和安全性问题。
- 音频 / 视频进程:处理网页中的音频和视频播放相关任务,确保音视频能够正常播放,并提供相关的控制功能。
不同浏览器在架构实现上会有差异,且随着技术发展,浏览器架构也在不断演进,从早期的单进程架构逐渐发展为多进程架构甚至面向服务架构。多进程架构将不同功能模块分离到不同进程,如 Chrome 浏览器包括浏览器进程、渲染进程、插件进程、GPU 进程、网络进程等,提高了浏览器的稳定性、安全性和性能 。面向服务架构则进一步将相关组件划分为不同服务,可根据硬件性能灵活配置进程,优化性能。
排查缓存问题的关键步骤
- 检查 Network 面板(浏览器开发者工具)的请求状态:
200 (from disk cache)→ 强缓存生效304 Not Modified→ 协商缓存生效
- 检查 Service Worker 是否拦截请求。
- 检查 CDN 或代理服务器配置。
- 检查代码中是否存在手动缓存逻辑
如何强制跳过缓存/如何实现页面每次打开时清除本页缓存 ⭐️
1. URL 添加随机参数
fetch(`/api/data?timestamp=${Date.now()}`);
2. 在 HTML 中设置 meta 标签
- 在页面的
<head>标签内添加特定的<meta>标签来控制缓存策略。例如:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
其中,Cache-Control指令告诉浏览器不要缓存页面内容,每次都从服务器重新获取;Pragma 是 HTTP 1.0 中用于控制缓存的字段,设置为no-cache 也表示不使用缓存;Expires 设置为0 ,指定缓存立即过期。不过,现代浏览器对这些标签的支持和解释可能存在差异,效果并非完全可靠。
3. 请求头设置 Cache-Control
fetch('/api/data', { headers: { 'Cache-Control': 'no-cache' } });
4. 响应头设置
- 不同服务器端语言设置方式有别。以常见的 PHP 为例:
<?php
header("Cache-Control: no-cache, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
?>
通过在服务器端设置响应头,能更精准地控制浏览器对该页面的缓存行为。
5.使用 JavaScript 操作
在页面加载完成后,利用 JavaScript 清除浏览器缓存(不同浏览器支持情况有差异 )。示例代码如下:
window.addEventListener('load', function() {
if ('caches' in window) {
caches.keys().then(function(cacheNames) {
cacheNames.forEach(function(cacheName) {
caches.delete(cacheName);
});
});
}
});
这段代码尝试获取浏览器中所有的缓存名称,然后逐个删除。不过,这种方式不能确保完全清除所有类型的缓存,且部分浏览器可能不支持。
6.浏览器开发者工具设置
“Network” 面板,勾选 “Disable cache” 选项。这样在当前会话中,浏览器每次请求资源时都不会使用缓存。但这是手动操作方式,无法自动应用到所有用户的每次页面打开行为。
浏览器的存储技术有哪些
1. Cookie
2. Web Storage(包含 localStorage 和 sessionStorage )
3. IndexedDB
4. Cache Storage
-
简介:是 Service Worker 的一部分,开发者可利用它
缓存网络请求和响应。用于在离线状态下快速访问资源,优化网页性能和加载速度,如缓存网页的 HTML、CSS、JavaScript、图片等资源。 -
特点:
- 缓存管理:可灵活控制缓存策略,如设置缓存优先读取、网络优先读取等。
- 离线支持:配合 Service Worker 实现离线应用,提升用户体验。
前端需要注意哪些SEO ⭐️
前端 SEO 检查清单
| 类别 | 关键点 |
|---|---|
| 技术优化 | 语义化 HTML、Meta 标签、URL 结构、页面性能、https、SSR |
| 内容优化 | 关键词策略、Alt 属性、结构化数据 |
| 用户体验 | 移动适配、加载速度、无障碍访问 |
| 爬虫友好 | Sitemap、Robots.txt、避免 JS 阻塞内容 |
| 工具与监控 | Google Search Console、Lighthouse |
什么是SEO?
它是由英文Search Engine Optimization缩写而来,中文意思是“搜索引擎的优化”。
SEO具体是指通过网站结构调整、网站内容建设、网站代码优化、以及站外优化(网站站外推广、网站品牌建设等),使网站满足搜索引擎的收录排名需求,提高网站在搜索引擎中关键字的排名,从而吸引精准用户进入网站,获得免费流量,产生直接销售或品牌推广。
一、技术层面优化
1. HTML 语义化
- 合理使用 HTML5 标签:如
<header>、<nav>、<main>、<article>、<section>、<footer>,帮助爬虫理解页面结构。 - 标题层级清晰:
<h1>主标题(每个页面唯一)</h1> <h2>子标题</h2> <h3>次级子标题</h3> - 避免滥用
<div>:用语义化标签替代无意义的<div>嵌套。
2. Meta 标签优化
<title>标签:唯一且包含核心关键词(不超过 60 字符)。<title>前端 SEO 指南 | 网站名称</title><meta name="description">:简明扼要描述页面内容(吸引点击,影响 CTR)。Click-Through Rate(点击通过率)<meta name="description" content="前端开发中需要注意的 SEO 优化技巧,提升网站在搜索引擎中的排名。">- 其他 Meta 标签:
<meta name="keywords" content="前端, SEO, 优化"> <!-- 权重已降低,但可适当添加 --> <meta name="robots" content="index, follow"> <!-- 控制爬虫行为 -->
1、 合理的title,description,keyswords 搜索引擎对这三项的权重逐个减小,title 值强调重点即可,重要的关键词出现不要超过两次,而且要靠前。
2 、不同页面的tilte要有所不同;description把页面的内容高度概括,长度合适,不可过分堆叠关键词,不同页面。description有所不同。keyswords列举出重要的关键词即可。
3. URL 结构优化
- 静态化 & 可读性:
差示例:/product?id=123
好示例:/product/seo-guide - 短且包含关键词:避免过长或随机字符。
- 统一小写 & 连字符分隔:
/seo-best-practices。
4. 移动端优先 & 响应式设计
- 适配移动端:Google 优先索引移动版页面(Mobile-First Indexing)。
- 使用
viewport标签:<meta name="viewport" content="width=device-width, initial-scale=1.0"> - 避免独立移动端站点:优先选择响应式设计,而非单独移动站(如
m.example.com)。
5. 性能优化
- 加载速度:直接影响排名(Google Core Web Vitals 指标)。
- 优化图片:压缩、使用 WebP 格式、懒加载(
loading="lazy")。 - 减少阻塞渲染的资源:CSS/JS 精简、异步加载(
async/defer)。 - 使用 CDN:加速静态资源分发。
- 优化图片:压缩、使用 WebP 格式、懒加载(
- 首屏内容快速渲染:避免长时间白屏。
6. JavaScript 渲染内容处理
- 避免纯 客户端渲染(CSR):爬虫可能无法执行 JS,导致内容无法被抓取。
- 解决方案:
- 服务端渲染(SSR):如 Next.js、Nuxt.js。
- 预渲染(Prerendering):生成静态 HTML 给爬虫。
- 使用
Hydration:结合 CSR 和 SSR。Hydration 指的是在服务端渲染出 HTML 页面后,将这些静态 HTML 与客户端的 JavaScript 代码进行关联,为其添加交互能力的过程。简单来说,就是给静态的 HTML “注入” 动态的交互行为,让页面从静态状态转变为可交互的动态状态。
- 解决方案:
- 关键内容直出:确保标题、正文等核心内容在 HTML 中直接存在。
7. 结构化数据(Schema Markup)
- 使用 Schema.org 标记:帮助搜索引擎理解内容类型(如文章、产品、事件)。
<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Article", "headline": "前端 SEO 指南", "author": { "@type": "Person", "name": "John Doe" } } </script> - 富媒体搜索结果(Rich Snippets):提升搜索结果的展示效果(如评分、价格)。
8. 内部链接优化
- 合理使用 锚文本:避免“点击这里”,而是用描述性文本。
差示例:<a href="/blog">点击这里</a>
好示例:<a href="/blog">前端开发博客</a> - 扁平化结构:确保重要页面在 3 次点击内可达。
二、内容层面优化
1. 关键词策略
- 自然融入关键词:标题、段落开头、图片 Alt 属性。
- 避免堆砌(如“前端 SEO 优化 前端 SEO 技巧 前端 SEO 指南”)。
- 长尾关键词:针对具体场景(如“React 服务端渲染 SEO 方案”)。
2. 图片与多媒体优化
- Alt 属性:描述图片内容,帮助爬虫和视障用户(
看不懂图片是什么意思)。<img src="seo-tips.jpg" alt="前端 SEO 优化流程图"> - 文件名优化:使用关键词而非随机字符(如
seo-checklist.png)。
3. 避免重复内容
- Canonical 标签:指定权威页面,避免重复内容惩罚。
<link rel="canonical" href="https://example.com/seo-guide"> - 处理分页内容:如
/blog?page=1和/blog?page=2需标记 Canonical。
三、其他关键事项
1. XML Sitemap
- 生成并提交 Sitemap:列出所有重要页面的 URL,帮助爬虫发现内容。
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://example.com/seo-guide</loc> <lastmod>2023-10-01</lastmod> <changefreq>weekly</changefreq> <priority>0.8</priority> </url> </urlset> - 提交到搜索引擎控制台:如 Google Search Console。
2. Robots.txt
- 控制爬虫访问:避免爬虫抓取无关或敏感页面。
User-agent: * Allow: / Disallow: /admin Disallow: /tmp
3. HTTPS 与安全性
- 全站 HTTPS:提升排名和用户信任。
- 避免混合内容:确保所有资源(图片、JS、CSS)通过 HTTPS 加载。
4. 社交媒体元标签
- Open Graph & Twitter Cards:优化社交分享时的展示效果。
<meta property="og:title" content="前端 SEO 指南"> <meta property="og:image" content="https://example.com/seo-thumbnail.jpg">
四、工具与验证
- Google Search Console:监测索引状态、提交 Sitemap、排查问题。
- Lighthouse:
检查性能、SEO、可访问性。 - Screaming Frog:抓取网站,分析 SEO 问题。
- 结构化数据测试工具:验证 Schema Markup 是否正确。
避免常见错误:
- 使用
display: none隐藏重要内容(可能被判定为作弊)。 - 忽略页面 404 错误或死链。
- 未适配移动端或加载速度过慢。
通过技术实现和内容优化结合,前端开发者可以显著提升网站在搜索引擎中的可见性和排名。
网页验证码是干嘛的,是为了解决什么安全问题
- 防止恶意注册:
注册多号发送垃圾信息
- 抵御暴力破解:
破解密码登录
- 防范刷单、抢购作弊等行为
- 防止论坛**灌水*
网页验证码 是 “全自动区分计算机和人类的图灵测试”(Completely Automated Public Turing test to tell Computers and Humans Apart,缩写为 CAPTCHA )的俗称,是一种区分用户是计算机还是人的公共全自动程序,主要作用和解决的安全问题如下:
防止恶意注册
一些人会使用注册机等程序批量注册账号,用于发送垃圾广告、刷流量等恶意行为。验证码要求用户手动识别并输入特定信息(如扭曲变形的字符、图片中的物体等 ),计算机程序难以自动识别和填写,只有人类能完成,有效阻止恶意注册行为,维护网站用户数据真实性和平台秩序 。
抵御暴力破解
黑客可能通过特定程序,用暴力破解方式不断尝试登陆特定用户账号,以破解密码。验证码增加了破解难度,每次尝试需识别并输入验证码,降低了尝试频率,保护用户账号密码安全,防止信息被盗取 。
防范刷票、作弊等行为
在投票、抢购、答题等场景,有人企图用机器程序刷票、刷排名、抢单等。验证码能确保操作由真实用户完成,保证活动公平公正,如防止在投票活动中机器刷票使结果失真,保障抢购活动中每个真实用户有公平机会 。
防止论坛灌水与垃圾信息
论坛、留言板等地方易被机器程序利用发布大量垃圾留言、广告等。验证码使程序难以自动发布信息,减少垃圾信息,营造干净、有序交流环境 。
限制网络爬虫滥用
网络爬虫可自动抓取网站数据,但恶意爬虫过度抓取会占用大量服务器带宽和资源,影响网站性能和正常服务。验证码能限制爬虫访问频率和数量,确保服务器在健康范围内运行,保护网站数据安全和服务稳定性 。
web开发中会话跟踪的方法有哪些 ⭐️
- Cookie
- Web Storage(HTML5 Web 存储)
- URL 重写
- 隐藏表单字段(隐藏域)token
- JWT(JSON Web Tokens )
在 Web 开发中,由于 HTTP 协议是无状态的,即服务器无法识别两次请求是否来自同一个客户端,所以需要会话跟踪技术来识别和跟踪用户。常见的会话跟踪方法如下:
3. URL 重写
- 原理:把会话 ID 附加在 HTML 页面中所有的 URL 上,作为查询参数的一部分(如
https://example.com/page?session_id=abc123)。当用户单击 URL 时,会话 ID 被自动作为请求行的一部分发送回服务器,服务器通过解析 URL 中的会话 ID 来跟踪会话状态。主要用于客户端不支持 Cookie 或用户阻止 Cookie 的情况。 - 优缺点:不依赖 Cookie,兼容性好;但可能导致 URL 冗长,影响美观,且会话 ID 暴露在 URL 中,存在安全风险(可能被篡改、猜测 ),还可能被搜索引擎抓取造成重复内容问题。
4. 隐藏表单字段(隐藏域)
- 原理:在 HTML 表单中添加隐藏的输入字段(如
<input type="hidden" name="session_id" value="123">)来存储会话信息。当用户提交表单时,隐藏字段中的会话信息会随表单数据一起发送到服务器,服务器据此进行会话跟踪。 - 示例:在一个登录表单中,除了用户名和密码输入框,还添加隐藏域
<input type="hidden" name="session_token" value="token_value">,表单提交时,session_token及其值发送给服务器。 - 优缺点:简单直接;但仅适用于表单提交场景,应用场景受限,且查看页面
源代码可看到隐藏字段,存在一定安全隐患。
5. Web Storage(HTML5 Web 存储)
- 原理:使用 HTML5 中的
localStorage或sessionStorage来存储会话状态信息。localStorage可长期存储数据(除非手动清除 ),在同源窗口中共享;sessionStorage仅在当前会话(浏览器窗口或标签页 )有效,会话结束数据销毁。客户端存储数据后,页面加载时可发送给服务器用于会话跟踪。 - 示例:
localStorage.setItem('user_session', JSON.stringify({ id: 1, username: 'example_user' }));存储会话相关用户信息,后续页面加载时读取并发送给服务器。 - 优缺点:容量相对较大(一般 5MB 左右 );
localStorage可跨页面持久存储,sessionStorage能保证会话内数据安全。但数据存储在客户端,存在被用户清除或被恶意读取、篡改风险,且受同源策略限制。
6. JWT(JSON Web Tokens )
- 原理:服务器生成 JWT,将用户会话数据编码为 JWT 并在客户端存储,每次请求时发送到服务器。JWT 包含加密信息,可在客户端解析,减少服务器端状态存储压力。一般由 Header(头部 )、Payload(负载 )、Signature(签名 )三部分组成。
- 示例:服务器生成 JWT,如
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiZXhhbXBsZV91c2VyIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c,发送给客户端。客户端后续请求携带此 JWT,服务器验证签名并解析其中用户信息进行会话跟踪。 - 优缺点:
无状态,便于在分布式系统中使用;可跨域;能在客户端存储和解析部分信息减轻服务器负担。但 JWT 一旦签发,在过期前一直有效,若密钥泄露,存在安全风险;且数据包含在 Token 中,体积过大时会增加传输开销。
Typescript有什么好处
- 类型安全与错误预防
- 代码可读性与可维护性
- 开发效率提升
- 团队协作便利
- 大型项目支持
- 兼容性与渐进式迁移
- 社区与生态优势
TypeScript 是 JavaScript 的超集,为 JavaScript 添加了类型系统等特性,在 Web 开发等场景中具有诸多好处:
类型安全与错误预防
- 静态类型检查:要求在编码阶段明确定义变量、函数参数和返回值等的类型。比如
function add(a: number, b: number): number { return a + b; },明确a、b是number类型,返回值也是number类型。这能在开发阶段就发现类型不匹配等错误,避免在运行时才暴露问题,提升代码稳定性和可靠性。 - 早期错误检测:在编译时进行类型检查,可提前发现潜在错误,减少调试时间和成本。不像 JavaScript 很多错误要到运行时才出现,排查起来更复杂。
代码可读性与可维护性
- 类型注解增强可读性:类型注解清晰表明变量、函数等的用途和数据类型。例如
let age: number = 25;,一眼就能看出age是数字类型,对于理解代码逻辑和功能很有帮助,尤其是在大型项目或多人协作项目中,新加入的开发者能更快理解代码。 - 便于代码理解与修改:类型信息让代码结构更清晰,修改代码时能更清楚各部分的约束和依赖关系,降低因误改导致错误的风险,提高代码可维护性。
开发效率提升
- 更好的 IDE 支持:多数现代 IDE(如 Visual Studio Code、WebStorm )对 TypeScript 有良好支持。能提供智能代码自动完成功能,输入变量或函数名时,IDE 根据类型信息给出相关提示;还能进行实时错误检查和提示,方便及时修正代码;也有助于代码重构,比如重命名变量或函数时,IDE 可根据类型信息自动更新所有相关引用。
- 代码智能感知:类型系统为 IDE 提供丰富信息,实现代码智能感知,如函数签名提示,调用函数时能清晰看到参数类型和个数要求等,提高编码准确性和效率。
团队协作便利
- 清晰的代码共享:在团队开发中,类型定义文件(.d.ts)可用于共享类型信息。其他开发人员通过查看类型定义,能快速了解代码中变量、函数等的类型和参数要求,降低理解和使用代码的难度,减少因类型理解不一致导致的沟通成本和错误。
- 规范代码风格:类型系统促使团队成员遵循一致的编码规范,提升代码整体质量和一致性,让团队协作更顺畅。
大型项目支持
- 模块化与结构化:提供模块系统、命名空间、接口、枚举等特性。模块系统便于将代码按功能拆分成不同模块,实现代码封装和复用;接口可定义对象的结构和行为规范;枚举用于定义一组相关常量。这些特性让大型项目的代码组织更有条理,易于管理和维护。
- 应对项目规模增长:随着项目规模和复杂度增加,JavaScript 代码可能变得难以维护,而 TypeScript 的类型系统和编译时检查能有效管理复杂性,更好地支持大型项目开发。
兼容性与渐进式迁移
- 与 JavaScript 兼容:可编译成纯 JavaScript 代码,能在任何支持 JavaScript 的平台上运行,具有良好跨平台兼容性。并且可以逐步将现有的 JavaScript 项目迁移到 TypeScript,一次迁移一个文件,无需重写整个项目,降低迁移风险,给予项目灵活性。
- 支持最新 JavaScript 特性:支持最新的 ECMAScript 标准,能使用如 async/await、箭头函数等新特性,同时 TypeScript 编译器会将这些特性转换成向下兼容的 JavaScript 代码,确保应用可在老旧浏览器上运行。
社区与生态优势
拥有庞大且活跃的社区,有大量的开源库、工具和类型定义可供使用。许多流行的 JavaScript 库和框架都提供了 TypeScript 的类型定义,便于集成到 TypeScript 项目中,开发者遇到问题也能在社区中获得帮助和资源 。
JS基本数据类型
基本类型:7 种JavaScript 的基本数据类型(Primitive Types)是语言中最基础的数据单元,它们直接存储在栈内存中,按值访问。以下是 7 种基本数据类型(ES6 新增 Symbol,ES2020 新增 BigInt):
- Undefined
- Null
- Number
- String
- Boolean
- Symbol(ES6)
引用类型:3 种
- Function
- Array
- Object
大整数类型(BigInt)
大整数类型是ES2020 引入的新类型,用于表示任意大的整数。
在数字后面加上n来表示大整数。const bigIntValue = 123456789012345678901234567890n;
JavaScript 的基本数据类型(Primitive Types)是语言中最基础的数据单元,它们直接存储在栈内存中,按值访问。以下是 7 种基本数据类型(ES6 新增 Symbol,ES2020 新增 BigInt):
1. number(数字)
- 用途:表示整数或浮点数。
- 示例:
let age = 25; // 整数 let price = 99.99; // 浮点数 let infinity = 1 / 0; // Infinity(无穷大) let nan = 0 / 0; // NaN(非数字) - 特点:
- 所有数字均为 64 位双精度浮点数(无单独整数类型)。
NaN(Not a Number)是特殊的数字类型,表示无效运算结果。
4. null(空值)
- 用途:表示“无”、“空”或“值未知”的显式赋值。
- 示例:
let user = null; // 明确表示 user 为空 - 特点:
typeof null返回"object"(历史遗留问题,实际是基本类型)。
5. undefined(未定义)
- 用途:表示变量已声明但未赋值时的默认值。
- 示例:
let x; // x 为 undefined function foo() {} // 无返回值,默认返回 undefined - 与
null的区别:undefined是未赋值的默认状态,null是主动赋值的空值。
6. symbol(符号,ES6 新增)
- 用途:创建唯一的标识符(常用于对象属性键)。
- 示例:
let id1 = Symbol("id"); // "id" 是描述(可选) let id2 = Symbol("id"); console.log(id1 === id2); // false(每个 Symbol 唯一) // 用作对象属性键 let obj = { [id1]: "secret" // 避免属性名冲突 }; - 特点:
- 不可枚举(
for...in遍历不到),需用Object.getOwnPropertySymbols()获取。
- 不可枚举(
7. bigint(大整数,ES2020 新增)
- 用途:表示超出
Number安全范围的整数(大于2^53 - 1)。 - 示例:
let bigNum = 9007199254740991n; // 后缀 n let bigHex = 0x1fffffffffffffn; // 十六进制 console.log(bigNum + 1n); // 9007199254740992n - 特点:
- 不能与
number直接混合运算(需显式转换)。
- 不能与
如何判断数据类型?
-
typeof运算符(快速区分基本类型):typeof 42; // "number" typeof null; // "object"(注意!历史遗留 Bug) typeof Symbol("id"); // "symbol" typeof 10n; // "bigint"- 局限性:无法区分
null和对象(需用=== null)。
- 局限性:无法区分
-
instanceof(用于检测引用类型,如对象、数组)。
常见问题
-
为什么
typeof null返回"object"?- JavaScript 早期设计错误,但已无法修复(兼容性考虑)。
-
如何安全判断
null?let x = null; console.log(x === null); // true -
基本类型的“方法”从哪来?(如
'str'.toUpperCase()) mark- 临时包装对象:JS 引擎自动将基本类型转为对应的包装对象(如
String),调用方法后销毁。
- 临时包装对象:JS 引擎自动将基本类型转为对应的包装对象(如
JS引用数据类型 ⭐️
总结
JavaScript 的引用数据类型包括:
Object(普通对象)Array(数组)Function(函数)Date(日期)RegExp(正则表达式)Set/Map(ES6 新增)WeakMap/WeakSet(弱引用)关键区别:
- 引用类型 变量存储的是 内存地址,赋值时传递引用。
- 基本类型 变量存储的是 实际值,赋值时复制值。
- 存储方式 栈内存(直接存储值), 堆内存(存储地址)
掌握引用类型的特点能有效避免 浅拷贝问题 和 内存泄漏。
1. Object(对象)
特点:
- 使用
{}或new Object()创建。 - 属性可以是 基本类型 或 引用类型(如函数、数组、其他对象)。
2. Array(数组)
用途:存储有序的数据集合(索引从 0 开始)。
特点:
-
使用
[]或new Array()创建。 -
常用方法:
push()/pop()(末尾增删)shift()/unshift()(头部增删)slice()/splice()(截取/修改数组)map()/filter()/reduce()(高阶函数)
3. Function(函数)
用途:封装可复用的代码块。
特点:
- 函数是 一等公民(可以赋值给变量、作为参数传递、作为返回值)。
- 具有
name(函数名)、length(参数个数)等属性。 - 可以访问外部作用域(闭包)。
4. Date(日期)
用途:处理日期和时间。
示例:
let now = new Date(); // 当前时间
let birthday = new Date("1995-12-17"); // 指定日期
console.log(now.getFullYear()); // 2023(年份)
console.log(birthday.getDay()); // 0(星期日)
常用方法:
getFullYear()/getMonth()/getDate()getHours()/getMinutes()/getSeconds()toISOString()(转为标准格式)
5. RegExp(正则表达式)
用途:匹配和操作字符串。
示例:
let pattern = /hello/i; // 不区分大小写匹配 "hello"
let str = "Hello, world!";
console.log(pattern.test(str)); // true(匹配成功)
// 构造函数方式
let regex = new RegExp("\\d+", "g"); // 匹配数字
console.log("123abc".match(regex)); // ["123"]
常用方法:
test()(返回true/false)exec()(返回匹配结果)match()/replace()(字符串方法)
6. Set(集合,ES6) mark
用途:存储 唯一值(无重复元素)。
示例:
let uniqueNumbers = new Set([1, 2, 2, 3]);
uniqueNumbers.add(4); // 添加元素
console.log(uniqueNumbers); // Set {1, 2, 3, 4}
特点:
- 使用
new Set()创建。 - 常用方法:
add()/delete()/has()size(获取元素数量)
7. Map(映射,ES6) mark
用途:存储键值对(键可以是任意类型)。
示例:
let userMap = new Map();
userMap.set("name", "Alice"); // 键是字符串
userMap.set(1, "ID"); // 键是数字
console.log(userMap.get("name")); // "Alice"
特点:
- 与
Object不同,Map的键可以是 对象、函数等。 - 常用方法:
set()/get()/has()size(获取键值对数量)
8. WeakMap 和 WeakSet(弱引用,ES6) mark ⭐️
用途:存储 弱引用 对象(不影响垃圾回收)。
示例:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "secret data");
// 当 obj 被垃圾回收时,weakMap 中的记录自动删除
适用场景:
- 避免内存泄漏(如缓存、DOM 节点关联数据)。
null是对象吗 ?null其实并不是一个对象
null其实并不是一个对象,尽管typeof null 输出的是object,但是这其实是一个bug。在js最初的版本中使用的是32位系统,为了性能考虑地位存储变量的类型信息,000开头表示为对象类型,然而null为全0,故而null被判断为对象类型。
在 JavaScript 中,null 不是对象,但它有一个历史遗留的 typeof 行为问题,导致 typeof null 返回 "object"。这是 JavaScript 早期设计的一个错误,但为了保持向后兼容性,至今未修复。以下是详细解释:
1. 为什么 typeof null === "object"?
- 历史原因:
在 JavaScript 最初的实现中,变量的类型标签存储在低位字节中:000表示object。null的二进制表示是全0(即000),因此被错误地判定为object。
- 无法修复:
修改这一行为会破坏大量现有代码,因此 ECMAScript 规范选择维持现状。
2. null 的真实类型
null是 JavaScript 的 基本数据类型(Primitive Type) 之一,与undefined、number、string等并列。- 它表示 “无”、“空” 或 “值未知” 的唯一值。
- 通过
Object.prototype.toString.call(null)可以验证其真实类型:console.log(Object.prototype.toString.call(null)); // [object Null]
4. 如何正确检测 null?
由于 typeof 不可靠,推荐以下方法:
方法 1:严格相等(===)
let value = null;
console.log(value === null); // true
方法 2:Object.prototype.toString
console.log(Object.prototype.toString.call(value) === "[object Null]"); // true
5. 为什么需要 null?
- 释放对象引用:
将变量设为null可帮助垃圾回收器释放内存。let data = { /* 大数据对象 */ }; data = null; // 解除引用,允许垃圾回收
总结
null不是对象,而是基本数据类型,但typeof null返回"object"是语言设计缺陷。- 检测
null应使用value === null。 - 使用
null表示显式空值,undefined表示未定义。
这一特性是 JavaScript 的著名“怪癖”之一,理解它有助于避免实际开发中的类型判断错误。
基本数据类型和引用数据类型的区别
1. 核心区别对比
| 特性 | 基本数据类型 | 引用数据类型 |
|---|---|---|
| 存储位置 | 栈内存 | 堆内存(变量存储引用地址) |
| 赋值行为 | 复制值本身 | 复制引用(内存地址) |
| 比较方式 | 比较值是否相等 | 比较引用是否指向同一对象 |
| 可变性 | 不可变(一旦创建,值不能被修改(只能重新赋值)) | 可变 |
| 内存管理 | 自动回收 | 通过引用计数/垃圾回收机制 |
| typeof 结果 | number, string, boolean, undefined, symbol, bigint | object 或 function |
| 传递方式 | 按值传递 | 按引用传递(实际上是按共享传递) |
2. 为什么需要理解这种区别?
- 性能优化:基本数据类型操作通常更快
- 避免意外修改:理解引用传递可以防止意外修改共享数据
- 内存管理:合理使用可以减少内存占用
数据类型判断
typeof 可以判断基本数据类型
Array,Object,null,Date,RegExp,Error这几个类型都被typeof判断为object,所以如果想要判断这几种类型,就不能使用typeof了。
Number,String,Boolean,Function,undefined,如果想判断这几种类型,那就可以使用typeof
instanceof mark
除了使用typeof来判断,还可以使用instanceof。instanceof运算符需要指定一个构造函数,或者说指定一个特定的类型,它用来判断这个构造函数的原型是否在给定对象的原型链上。结果如下:
console.log(
1 instanceof Number, //false
'dsfsf' instanceof String, //false
false instanceof Boolean, //false
[1,2,3] instanceof Array, //true
{a:1,b:2,c:3} instanceof Object, //true
function(){console.log('aaa');} instanceof Function, //true
undefined instanceof Object, //false
null instanceof Object, //false
new Date() instanceof Date, //true
/^[a-zA-Z]{5,20}$/ instanceof RegExp, //true
new Error() instanceof Error //true
)
可以发现如下规律 Number,String,Boolean没有检测出他们的类型,但是如果使用下面的写法则可以检测出来
var num = new Number(123);
var str = new String('dsfsf');
var boolean = new Boolean(false);
还需要注意null和undefined都返回了false,这是因为它们的类型就是自己本身,并不是Object创建出来它们,所以返回了false
使用toString()检测对象类型
可以通过toString() 来获取每个对象的类型。为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为thisArg。
toString.call(123); //"[object Number]"
toString.call('abcdef'); //"[object String]"
toString.call(true); //"[object Boolean]"
toString.call([1, 2, 3, 4]); //"[object Array]"
toString.call({name:'wenzi', age:25}); //"[object Object]"
toString.call(function(){ console.log('this is function'); }); //"[object Function]"
toString.call(undefined); //"[object Undefined]"
toString.call(null); //"[object Null]"
toString.call(new Date()); //"[object Date]"
toString.call(/^[a-zA-Z]{5,20}$/); //"[object RegExp]"
toString.call(new Error()); //"[object Error]"</pre>
这样可以看到使用Object.prototype.toString.call() 的方式来判断一个变量的类型是最准确的方法。封装一个获取变量准确类型的函数这样判断一个变量的数据类型就很方便了。
function gettype(obj) {
var type = typeof obj;
if (type !== 'object') {
return type;
}
//如果不是object类型的数据,直接用typeof就能判断出来
//如果是object类型数据,准确判断类型必须使用Object.prototype.toString.call(obj)的方式才能判断
return Object.prototype.toString.call(obj).replace(/^[object (\S+)]$/, '$1');
}
实现instanceof
function instanceofFunc(obj, cons) {
// 错误判断 构造函数必须是一个function 其他的均报错
if (typeof cons !== 'function') throw new Error('instance error')
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) return false
// 获取到原型对象
let proto = cons.prototype
// 如果obj的原型对象不是null
while (obj.__proto__) {
if (obj.__proto__ === proto) return true
obj = obj.__proto__
}
return false
}
console.log(instanceofFunc(() => {}, Function)) // true
== 和 ===的区别,什么情况下用相等==
== 在表达式两边的数据类型不一致时,会隐式转换为相同数据类型,然后对值进行比较。=== 不会进行类型转换,在比较时除了对值进行比较以外,还比较两边的数据类型。另外,数值是null,"",undefined,Nan的时候,返回的也是false.有时候判断的时候没必要一个个列举出来,一行代码解决的事情,就不要写两行。
值相等 类型不相等的时候用==
怎么判断两个对象是否相等 * 3 ⭐️
1. 严格相等(===):判断是否是同一个引用
- 只需要判断两个变量是否指向内存中的同一个对象。
2. 深比较(Deep Compare)
递归比较对象的所有层级属性(包括嵌套对象和数组):
注意事项:
- 循环引用会导致栈溢出(如
objA.circular = objA),需额外处理。 性能较差,深层次嵌套对象比较耗时。
3. 使用第三方库
推荐使用成熟的工具库进行深比较:
- Lodash:
_.isEqual(obj1, obj2)const _ = require('lodash'); console.log(_.isEqual({ a: 1 }, { a: 1 })); // true - Underscore:
_.isEqual(obj1, obj2)
优点:
- 处理了边界情况(
如循环引用、特殊数据类型)。 - 性能优化较好。
4. Object.is()
var obj = { name: 'shenghua' }
var obj2 = obj
var obj3 = { name: 'shenghua' }
// 字面量比较
console.log(JSON.stringify(obj) == JSON.stringify(obj2));//true
// 全比
console.log(Object.is(obj, obj2));//true
console.log(JSON.stringify(obj) == JSON.stringify(obj3));//true
console.log(Object.is(obj, obj3));//false
总结:如何选择方法?
| 场景 | 推荐方法 |
|---|---|
| 判断是否是同一引用 | obj1 === obj2 |
| 浅层属性比较 | 自定义 shallowEqual 函数 |
| 深层属性比较 | Lodash 的 _.isEqual 或 deepEqual |
| 快速简易比较(无嵌套) | JSON.stringify() |
| 生产环境复杂对象比较 | 使用 Lodash/Underscore |
栈和堆的区别 ⭐️
总结:核心区别
特性 栈 堆 存储内容 基本类型、函数上下文 引用类型、动态分配数据 管理方式 自动分配/释放(高效) 手动/垃圾回收(灵活但复杂) 访问速度 快 慢 生命周期 与函数绑定 由引用决定 内存限制 固定较小 动态较大
在计算机程序中,栈(Stack) 和 堆(Heap) 是两种不同的内存管理机制,它们的核心区别体现在 存储内容、管理方式、生命周期 和 性能 上。以下是它们的详细对比:
1. 存储内容
| 栈 (Stack) | 堆 (Heap) |
|---|---|
存储 基本数据类型(如 number, string, boolean 等) | 存储 引用数据类型(如 Object, Array, Function 等) |
| 存储 函数调用时的执行上下文(如局部变量、参数、返回地址) | 存储动态分配的复杂数据结构 |
示例:
let a = 10; // 基本类型 → 栈内存
let obj = { x: 1 }; // 引用类型 → 堆内存(obj 变量存储堆内存地址)
2. 内存分配与释放
| 栈 | 堆 |
|---|---|
| 自动分配和释放:由编译器/引擎自动管理(如函数执行完毕,局部变量立即释放) | 手动或自动分配:需要开发者管理(如 malloc/free 的 C 语言),或通过垃圾回收机制(如 JavaScript、Java) |
后进先出(LIFO):内存分配和释放顺序严格遵循函数调用顺序 | 动态分配:内存块可任意分配和释放 |
JavaScript 中的垃圾回收:
堆中的对象通过 引用计数 或 标记-清除算法 自动回收(开发者无需手动释放)。
3. 访问速度
| 栈 | 堆 |
|---|---|
访问速度快:内存连续分配,直接通过指针移动操作 | 访问速度慢:内存碎片化,需通过地址间接访问 |
| 适合高频操作:如函数调用、基本类型运算 | 适合存储大块数据:如复杂对象、数组 |
4. 生命周期
| 栈 | 堆 |
|---|---|
| 与函数生命周期一致:函数执行时创建,执行完毕立即销毁 | 生命周期不确定:对象在不再被引用时由垃圾回收器回收 |
| 局部变量随函数结束消失 | 对象可能长期存在(如全局变量引用的对象) |
示例:
function foo() {
let num = 10; // 栈内存,函数结束即释放
let obj = { x: 1 }; // 堆内存,函数结束后若 obj 未被外部引用,会被回收
}
5. 内存大小限制
| 栈 | 堆 |
|---|---|
| 固定大小:由系统预设(通常较小,如几 MB) | 动态扩展:受物理内存限制,可分配更大空间 |
溢出风险高:递归过深或局部变量过大可能导致栈溢出(Stack Overflow) | 更适合存储大量数据:如缓存、大型对象 |
为什么需要区分栈和堆?
- 性能优化:合理使用栈可提升高频操作效率。
- 内存管理:避免内存泄漏(堆)或栈溢出。
- 数据隔离:基本类型的值复制 vs 引用类型的共享修改。
- 理解语言特性:如 JavaScript 中闭包、垃圾回收机制。
理解栈和堆的差异是掌握内存管理和性能优化的基础,尤其在处理复杂数据结构时至关重要。
还有哪些地方会内存泄露 ⭐️
1)意外的全局变量引起的内存泄露
function leak(){
leak="xxx";//leak成为一个全局变量,不会被回收
}
2)闭包引起的内存泄露
3)没有清理的DOM元素引用
4)被遗忘的定时器或者回调
5)两个对象相互引用
介绍垃圾回收 ⭐️⭐️
总结
关键点 说明 核心算法 标记-清除(主流),分代回收优化性能 内存分代 新生代(Scavenge)与老生代(标记-清除/整理) 常见内存泄漏 全局变量、未清理的定时器/DOM 引用、闭包滥用 检测工具 Chrome DevTools 的 Heap Snapshots 和 Allocation Timeline 优化策略 及时解除引用、避免循环引用、合理使用闭包
JavaScript 的垃圾回收(Garbage Collection,GC)机制是自动管理内存的核心,旨在释放不再使用的对象占用的内存空间,防止内存泄漏并优化性能。以下是 JavaScript 垃圾回收的详细解析:
一、垃圾回收的核心原则
JavaScript 引擎通过判断对象是否 “可达”(Reachable) 来决定是否回收内存。如果一个对象不再被任何根对象(Root)通过引用链访问,则视为不可达,会被回收。
根对象(Roots)包括:
- 全局对象(如
window或global)。 - 当前执行栈中的变量(局部变量、函数参数等)。
- DOM 元素(未被移除的节点)。
二、主要垃圾回收算法
1. 标记-清除(Mark-and-Sweep)
- 步骤:
- 标记阶段:从根对象出发,
递归标记所有可达对象。 - 清除阶段:遍历堆内存,清除未标记的对象。
- 标记阶段:从根对象出发,
- 优点:
解决了循环引用问题。 - 缺点:
内存碎片化。
2. 引用计数(Reference Counting)
- 原理:
记录每个对象的引用次数,当引用数为 0 时回收。 - 缺点:无法处理循环引用(现已淘汰,仅作历史了解)。
// 循环引用示例 let objA = { ref: null }; let objB = { ref: null }; objA.ref = objB; objB.ref = objA; // 引用计数无法回收
3. 分代回收(Generational Collection)
- 内存分代:
- 新生代(Young Generation):存放短生命周期对象(如临时变量)。
- 算法:Scavenge(复制存活对象到另一区域,清空当前区域)。
- 老生代(Old Generation):存放长生命周期对象(如全局变量、闭包变量)。
- 算法:标记-清除 + 标记-整理(减少碎片)。
- 新生代(Young Generation):存放短生命周期对象(如临时变量)。
- 提升(Promotion):新生代对象经过多次 GC 仍存活时,移至老生代。
五、内存泄漏检测与优化 ⭐️
1. 开发者工具(Chrome DevTools)
- Memory 面板:
- Heap Snapshots:
对比快照,查找未被释放的对象。 - Allocation Timeline:记录内存分配,定位泄漏点。
- Heap Snapshots:
- Performance 面板:监控内存占用趋势。
2. 编码最佳实践
-
及时解除引用:将不再使用的对象设为
null。 -
避免滥用闭包:确保闭包仅保留必要变量。
-
清理事件监听与定时器:
const handler = () => console.log('click'); element.addEventListener('click', handler); // 移除时 element.removeEventListener('click', handler);
六、垃圾回收的触发时机
- 新生代 GC:频繁触发(如每 16ms),处理短生命周期对象。
- 老生代 GC:当内存达到阈值或页面空闲时触发。
垃圾回收,引用计数和标记清除
标记清除
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
function test(){
var a=10;//被标记,进入环境
var b=20;//被标记,进入环境
}
test();//执行完毕之后a、b又被标记离开环境,被回收
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值(function object array)赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
function test(){
var a={};//a的引用次数为0
var b=a;//a的引用次数加1,为1
var c=a;//a的引用次数加1,为2
var b={};//a的引用次数减1,为1
}
V8垃圾回收机制 ⭐️
V8 是 Google 开发的高性能 JavaScript 引擎,被广泛应用于 Chrome 浏览器和 Node.js 环境中。为了高效管理内存,V8 采用了多种垃圾回收(GC)算法,并且根据不同的内存场景和对象特性,采用分代式垃圾回收策略,将堆内存主要分为新生代和老生代两个区域,下面为你详细介绍其工作机制:
新生代垃圾回收(Scavenge 算法)
1. 内存划分
新生代内存空间较小,主要用于存放存活时间较短的对象。它被划分为两个大小相等的区域:From 空间和 To 空间。新创建的对象会被分配到 From 空间。
2. 回收过程
- 标记活动对象:垃圾回收器会从根对象(如全局对象、当前执行栈中的变量等)开始,遍历对象引用关系,标记所有可达的活动对象。
- 复制活动对象:将 From 空间中的活动对象复制到 To 空间,并且按照顺序依次排列。
- 清空 From 空间:复制完成后,清空 From 空间中的所有对象。
- 交换空间:将 From 空间和 To 空间的角色进行交换,原来的 To 空间变为新的 From 空间,用于下次分配新对象。
3. 对象晋升
当一个对象经过多次垃圾回收仍然存活,或者 To 空间的使用量达到一定阈值时,该对象会被晋升到老生代内存中。
老生代垃圾回收
1. 标记清除(Mark - Sweep)
- 标记阶段:从根对象开始,遍历对象引用关系,标记所有可达的活动对象。
- 清除阶段:扫描整个老生代内存空间,将未被标记的对象(即垃圾对象)所占用的内存空间释放。
2. 标记整理(Mark - Compact)
标记清除算法会产生内存碎片,当内存碎片过多时,会影响新对象的分配。标记整理算法在标记清除的基础上,增加了整理内存的步骤。
- 标记阶段:同标记清除算法,标记所有活动对象。
- 整理阶段:将所有活动对象向内存的一端移动,使它们连续存储,然后清除边界以外的内存空间,从而消除内存碎片。
并发与增量式垃圾回收
为了减少垃圾回收对应用程序性能的影响,V8 还采用了并发和增量式垃圾回收技术。
- 并发垃圾回收:在主线程执行 JavaScript 代码的同时,垃圾回收器在后台线程并行执行部分垃圾回收工作,如标记阶段。这样可以减少垃圾回收的停顿时间。
- 增量式垃圾回收:将垃圾回收过程分成多个小步骤,在每个小步骤之间允许主线程执行一定量的 JavaScript 代码,从而将垃圾回收的停顿时间分散到多个小的时间段内,减少对用户体验的影响。
总结
V8 的垃圾回收机制采用分代式策略,根据对象的存活时间将堆内存分为新生代和老生代,分别采用不同的垃圾回收算法。同时,结合并发和增量式垃圾回收技术,减少垃圾回收对应用程序性能的影响,提高内存管理的效率和用户体验。
三、V8 引擎的垃圾回收优化
1. 增量标记(Incremental Marking)
- 原理:将标记阶段分解为多个小步骤,避免长时间阻塞主线程(Stop-The-World)。
- 优点:减少页面卡顿,提升用户体验。
2. 空闲时间回收(Idle-Time GC)
- 原理:在浏览器空闲时段执行垃圾回收任务。
3. 并行与并发回收
- 并行回收:多个线程协同完成 GC 任务(主线程暂停)。
- 并发回收:GC 线程与主线程同时运行(减少阻塞)。
垃圾回收时栈和堆的区别x3 ⭐️
总结:垃圾回收时栈和堆的核心区别
特性 栈 堆 是否需要 GC 否(自动释放) 是 (依赖 GC 算法) 生命周期 严格绑定函数执行流程由引用关系决定 性能影响 无 GC 开销 可能引起暂停和性能波动 常见问题 栈溢出 内存泄漏 开发者干预 优化递归和局部变量 管理引用、分析内存泄漏
在垃圾回收(Garbage Collection, GC)过程中,栈(Stack) 和 堆(Heap) 的内存管理机制有显著差异。以下是它们在垃圾回收时的核心区别:
1. 是否需要垃圾回收?
| 栈 | 堆 |
|---|---|
| 不需要垃圾回收 | 依赖垃圾回收 |
栈内存由系统自动管理,函数执行完毕时,其局部变量和上下文立即被销毁(遵循后进先出原则)。 | 堆内存中的对象无法自动释放,需通过垃圾回收机制判断对象是否不再被引用,然后回收内存。 |
示例:
function foo() {
let num = 10; // 栈内存,函数结束即释放
let obj = { x: 1 }; // 堆内存,函数结束后若未被引用,由垃圾回收器回收
}
2. 垃圾回收机制
| 栈 | 堆 |
|---|---|
| 无垃圾回收过程:内存释放是即时的、顺序化的。 | 依赖垃圾回收算法(如标记-清除、分代回收、增量标记等)。 |
| 内存管理通过 栈指针 直接移动完成。 | 需要遍历对象引用关系,判断是否可达(未被引用则回收)。 |
堆的垃圾回收流程:
- 标记阶段:从根对象(如全局变量、当前执行栈中的变量)出发,标记所有可达对象。
- 清除阶段:回收未被标记的对象内存。
- 整理/压缩(可选):减少内存碎片。
3. 性能影响
| 栈 | 堆 |
|---|---|
| 零开销:内存分配/释放是极快的指针移动操作。 | 可能引起性能波动:垃圾回收会暂停主线程(Stop-The-World),尤其是全堆扫描时。 |
| 适合高频操作(如函数调用、基本类型运算)。 | 需优化垃圾回收策略(如分代回收减少扫描范围)。 |
示例:
- V8 引擎的堆内存分为 新生代(Young Generation)和 老生代(Old Generation),采用不同回收策略。
- 新生代使用 Scavenge 算法(复制存活对象),老生代使用 标记-清除 和 标记-整理。
4. 内存泄漏的常见原因
| 栈 | 堆 |
|---|---|
| 栈溢出:递归过深或局部变量过多导致超出栈大小限制。 | 内存泄漏:对象不再使用但未被释放(如未清理的全局引用、未解绑的事件监听)。 |
错误提示:Maximum call stack size exceeded。 | 错误现象:内存占用持续增长,页面卡顿。 |
堆内存泄漏示例:
// 未清理的定时器或事件监听
let data = loadHugeData();
setInterval(() => {
// data 一直被引用,无法回收
process(data);
}, 1000);
5. 开发者的关注点
| 栈 | 堆 |
|---|---|
| 无需手动干预,但需避免栈溢出(如优化递归为循环)。 | 需注意对象引用关系,及时解除无用引用(如设为 null)。 |
| 调试工具:通过调用栈(Call Stack)追踪错误。 | 调试工具:Chrome DevTools 的 Memory 面板分析堆快照。 |
关键点:
- 栈的内存管理是 自动且即时 的,垃圾回收仅针对堆内存。
- 堆的垃圾回收是 JavaScript 性能优化的重点,需关注对象引用和内存占用。
栈和堆具体怎么存储
栈内存:存放基本类型。 堆内存:存放引用类型(在栈内存中存一个基本类型值保存对象在堆内存中的地址,用于引用这个对象。)
JS中栈、堆、队列
栈和队列都是动态的集合,在栈中,可以去掉的元素是最近插入的那一个。栈实现了后进先出。在队列中,可以去掉的元素总是在集合中存在的时间最长的那一个。队列实现了先进先出的策略。
介绍JS有哪些内置对象?
内置对象:Object是Javascript中所有对象的父对象
数据封装对象:Object Array Boolean Number String
其他对象:Function Argument Date RegExp Error Math
JavaScript 提供了多种内置对象,这些对象可以直接在代码中使用,无需额外定义。它们可以分为以下几大类:
一、值属性(全局对象)
这些是全局范围内的属性,可以直接访问:
- Infinity - 表示无穷大的数值
- NaN - 表示"非数字"(Not-A-Number)
- undefined - 表示未定义的值
- globalThis - 全局对象(浏览器中是window,Node中是global)
二、函数属性(全局函数)
可以直接调用的全局函数:
- eval() - 执行字符串代码
- isFinite() - 判断是否是有限数
- isNaN() - 判断是否是NaN
- parseFloat() - 解析字符串为浮点数
- parseInt() - 解析字符串为整数
- encodeURI()/decodeURI() - URI编码/解码
- encodeURIComponent()/decodeURIComponent() - URI组件编码/解码
三、基本对象
最基础的内置对象:
- Object - 所有对象的基类
- Function - 函数构造器
- Boolean - 布尔对象
- Symbol - 符号对象(ES6新增)
- Error - 错误对象及其子类:
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
- AggregateError(ES2021新增)
四、数字和日期对象
- Number - 数字对象
- BigInt - 大整数对象(ES2020新增)
- Math - 数学对象(提供数学函数和常量)
- Date - 日期对象
五、字符串处理对象
- String - 字符串对象
- RegExp - 正则表达式对象
六、集合对象
- Array - 数组对象
- Map - 键值对集合(ES6新增)
- Set - 值集合(ES6新增)
- WeakMap - 弱引用键值对(ES6新增)
- WeakSet - 弱引用值集合(ES6新增)
七、结构化数据对象
- ArrayBuffer - 二进制数据缓冲区
- DataView - 缓冲区视图
- JSON - JSON处理对象
- Promise - 异步操作对象(ES6新增)
- Proxy - 代理对象(ES6新增)
- Reflect - 反射对象(ES6新增)
八、国际化和Web API相关对象
- Intl - 国际化对象
- WebAssembly - WebAssembly相关对象
九、其他重要对象
- arguments - 函数参数对象
- Generator - 生成器对象(ES6)
- GeneratorFunction - 生成器函数构造器
- AsyncFunction - 异步函数构造器(ES2017)
这些内置对象为JavaScript提供了丰富的功能,开发者可以直接使用它们来完成各种编程任务,而无需从头实现基础功能。
数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少 ⭐️
JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。
在 JavaScript 中,数组的访问时间复杂度是 O(1) ,即无论数组长度如何,访问任意位置的元素在理论上耗时相同。但实际场景中可能存在细微差异
作用域链 ⭐️
当一个变量在当前作用域下找不到该变量的定义,js引擎会向外层作用域查找,一直如此知道最外层window对象。
作用域链是js函数在创建的时候定义的,用于寻找到变量的一个索引。
我们知道,如果作用域链越深, [0] => [1] => [2] => [...] => [n],我们调用的是 全局变量,它永远在最后一个(这里是第 n 个),这样的查找到我们需要的变量会引发多大的性能问题?JS 引擎查找变量时会耗费多少时间?
所以,这个故事告诉我们,尽量将 全局变量局部化 ,避免,作用域链的层层嵌套,所带来的性能问题。
JavaScript 中的作用域链(Scope Chain)是理解变量查找、闭包和代码执行逻辑的核心机制。它决定了 当前上下文中如何访问变量 以及 变量查找的顺序。以下是作用域链的详细解析:
一、作用域链的形成原理
作用域链是基于 词法作用域(Lexical Scope)建立的,即 函数定义时的位置(而非调用时的位置)决定了它能访问哪些变量。其核心规则是:
- 每个函数在定义时,会记录它所在的词法环境(即父级作用域)。
- 函数执行时,会创建一个新的作用域(执行上下文),并将父级作用域的引用链接起来,形成链式结构。
示例代码
function outer() {
const a = 10;
function inner() {
console.log(a); // 通过作用域链找到 outer 中的 a
}
return inner;
}
const innerFunc = outer();
innerFunc(); // 输出 10
inner函数定义在outer内部,因此它的作用域链包含outer的作用域和全局作用域。
二、作用域链的组成
每个执行上下文的作用域链由以下部分组成(按查找顺序):
- 当前作用域的变量对象(AO, Activation Object)
- 包含当前函数的参数、局部变量和函数声明。
- 父级作用域的变量对象
- 逐级向上直到全局作用域(Global Object)
三、变量查找规则
当访问一个变量时,JavaScript 引擎会 沿作用域链逐级向上查找:
- 先在当前作用域的变量对象中查找。
- 如果找不到,则到父级作用域的变量对象中查找。
- 直到全局作用域,若仍未找到则抛出
ReferenceError。
遮蔽效应(Shadowing)
如果内层作用域声明了与外层同名的变量,则 外层变量会被遮蔽:
const x = 1;
function foo() {
const x = 2; // 遮蔽全局的 x
console.log(x); // 2
}
foo();
四、作用域链与闭包
五、ES6 对作用域链的影响
ES6 引入的 let/const 带来了 块级作用域,改变了作用域链的细节:
- 块级作用域 会创建新的词法环境。
- 暂时性死区(TDZ):变量在声明前不可访问。
示例
function blockScopeDemo() {
if (true) {
let a = 10;
var b = 20;
}
console.log(b); // 20(var 穿透块作用域)
console.log(a); // ReferenceError: a is not defined
}
六、作用域链的常见问题
-
意外修改全局变量
function demo() { x = 10; // 未声明,隐式创建全局变量(严格模式下报错) } demo(); console.log(x); // 10- 始终使用
let/const声明变量避免此问题。
- 始终使用
-
循环中的闭包陷阱
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 输出 3, 3, 3 } // 解决方案:改用 let 或闭包保存 i 的值
八、总结
- 作用域链的本质:函数定义时确定的变量访问规则链。
- 关键规则:
- 变量查找沿作用域链逐级向上。
- 闭包通过保留作用域链实现跨作用域访问。
- 最佳实践:
- 使用
let/const明确作用域。 避免隐式全局变量。- 合理使用闭包,注意内存管理。
- 使用
谈谈你对JS执行上下文栈和作用域链的理解
当函数执行时,会创建一个称为执行上下文的内部对象(可理解为作用域)。一个执行上下文定义了一个函数执行时的环境。
对于每个执行上下文(Execution Context)都有三个重要的属性,
①变量对象(Variable object,VO),
②作用域链(Scope chain)
③this
介绍暂时性死区
在ES6的新特性中,最容易看到 TDZ 作用就是在let/const的使用上。
- let/const与var的主要不同有两个地方:
- let/const是使用区块作用域;var是使用函数作用域。
- 在let/const声明之前就访问对应的变量与常量,会抛出 ReferenceError 错误;但在var声明之前就访问对应的变量,则会得到undefined,
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为 “暂时性死区”(temporal dead zone,简称 TDZ)。
console.log(aVar) // undefined
console.log(aLet) // causes ReferenceError: aLet is not defined
var aVar = 1
let aLet = 2
JS代码最终在哪里执行的
JavaScript 代码的执行位置取决于其运行环境,以下是常见的运行环境及其执行机制:
浏览器环境
在浏览器中,JavaScript 代码主要由浏览器的 渲染引擎 和 JavaScript引擎 协同执行。
渲染引擎
渲染引擎负责解析 HTML、CSS 并构建 DOM 树和 CSSOM 树,最终将页面渲染到屏幕上。在这个过程中,渲染引擎会遇到嵌入在 HTML 中的 <script> 标签,从而触发 JavaScript 代码的执行。例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
console.log('This is a JavaScript code in the browser.');
</script>
</body>
</html>
当浏览器解析到 <script> 标签时,会暂停 HTML 解析,将控制权交给 JavaScript 引擎来执行其中的代码。
JavaScript 引擎
JavaScript 引擎是浏览器中执行 JavaScript 代码的核心组件。不同的浏览器使用不同的 JavaScript 引擎,常见的有:
- V8 引擎:由 Google 开发,被 Chrome 浏览器和 Node.js 使用。V8 引擎将 JavaScript 代码编译成机器码,从而提高执行效率。
- SpiderMonkey 引擎:由 Mozilla 开发,用于 Firefox 浏览器。它是第一个 JavaScript 引擎,具有良好的兼容性和性能。
- JavaScriptCore 引擎:由苹果公司开发,用于 Safari 浏览器。它在移动设备上具有出色的性能表现。
Node.js 环境
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,使 JavaScript 可以在服务器端执行。
事件驱动架构
Node.js 采用事件驱动、非阻塞 I/O 模型,具有高效、可扩展的特点。当执行 JavaScript 代码时,Node.js 会创建一个事件循环(Event Loop)来处理异步操作。例如:
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
console.log('Reading file...');
在上述代码中,fs.readFile 是一个异步操作,Node.js 会将其放入事件队列中,继续执行后面的代码。当文件读取完成后,事件循环会将相应的回调函数放入执行栈中执行。
模块系统
Node.js 提供了模块化的开发方式,通过 require 函数可以引入其他模块。每个模块都有自己的作用域,避免了全局变量的污染。例如:
// module.js
module.exports = {
sayHello: function() {
console.log('Hello!');
}
};
// main.js
const module = require('./module');
module.sayHello();
其他环境
除了浏览器和 Node.js,JavaScript 还可以在其他环境中执行,如:
- 嵌入式系统:一些嵌入式设备可以运行 JavaScript 代码,用于控制硬件设备、处理传感器数据等。
- 桌面应用程序:使用 Electron 等框架可以将 JavaScript 代码打包成跨平台的桌面应用程序,实现与操作系统的交互。
如何判断一个变量是不是数组
- Array.isArray 判断,返回 true,说明是数组
- instanceof Array 判断,返回 true。说明是数组
- 使用 Object.prototype.toString.call 判断,如果值是 [object Array], 说明是数组
- 通过 constructor 来判断,如果是数组,那么 arr.constructor === Array (不准确,因为我们可以指定 obj.constructor = Array)
function fn() {
console.log(Array.isArray([1, 2, 3, 4])); //true
console.log(arguments instanceof Array); //fasle
console.log([1, 2, 3, 4] instanceof Array); //true
console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
console.log(Object.prototype.toString.call([1, 2, 3, 4])); //[object Array]
console.log(arguments.constructor === Array); //false
arguments.constructor = Array;
console.log(arguments.constructor === Array); //true
console.log(Array.isArray(arguments)); //false
}
fn(1, 2, 3, 4);
在实际开发中,推荐优先使用 Array.isArray() 方法,因为它简单、准确且兼容性较好。如果需要兼容旧版本 JavaScript 环境,可以考虑使用 Object.prototype.toString.call() 方法。
为什么console.log(0.2+0.1==0.3)
因为浮点型数使用64位存储时,最多只能存储52位的小数位,对于一些存在无限循环的小数位浮点数,会截取前52位,从而丢失精度
在计算机中数字无论是定点数还是浮点数都是以多位二进制的方式进行存储的。由于0.1转换成二进制时是无限循环的,所以在计算机中0.1只能存储成一个近似值。另外说一句,除了那些能表示成 x/2^n 的数可以被精确表示以外,其余小数都是以近似值得方式存在的。在0.1 + 0.2这个式子中,0.1和0.2都是近似表示的,在他们相加的时候,两个近似值进行了计算,导致最后得到的值是0.30000000000000004,此时对于JS来说,其不够近似于0.3,于是就出现了0.1 + 0.2 != 0.3 这个现象。 当然,也并非所有的近似值相加都得不到正确的结果(误差在相加时可能相互抵消,最终使得总和接近或等于真实值)
解决办法
想办法规避掉这类小数计算时的精度问题就好了,那么最常用的方法就是将浮点数转化成整数计算。因为整数都是可以精确表示的。
0.1+0.2 => (0.1*10+0.2*10)/10
说一下JS中类型转换的规则?
在条件判断运算 == 中的转换规则是这样的:
- 如果比较的两者中有布尔值(Boolean),会把 Boolean 先转换为对应的 Number,即 0 和 1,然后进行比较。\
- 如果比较的双方中有一方为 Number,一方为 String时,会把 String 通过 Number() 方法转换为数字,然后进行比较。\
- 如果比较的双方中有一方为 Boolean,一方为 String时,会将双方转换为数字,然后再进行比较。
如果比较的双方中有一方为 Number,一方为Object时,则会调用 valueOf 方法将Object转换为数字,然后进行比较。对象在转换基本类型时,首先会调用 valueOf 然后调用 toString。并且这两个方法你是可以重写的。\
这两个运算符在大部分上面都是与(1)相同的,不同的是:
字符串 op 字符串:不会进行类型转换,直接比较。
对象 op 对象:引用都指向同一个对象才为true。
在条件判断时,除了 undefined, null, false, NaN, '' , 0 -0,其他所有值都转为 true,包括所有对象
建议在所有使用条件判断的时候都使用全等运算符 === 来进行条件判断。全等运算符会先进行数据类型判断,并且不会发生隐式类型转换。
同(一),但是对于两个操作数均是字符串的时候&无法转换时的返回值会有不同。当两个操作数均是字符串的时候,它会执行大家熟悉的字符串比较,即从左到右依次比较每一个字符的ASCII码,若出现符合操作符的情况,则返回true,否则返回false。无法将操作数转换为数字的情况下总是返回false
变量a和b,如何交换
- 使用临时变量
- 不使用临时变量(算术运算)
- 不使用临时变量(ES6 解构赋值)
- 不使用临时变量(位运算)
在实际开发中,推荐使用 ES6 解构赋值的方式,因为它简洁、易读且适用于各种数据类型。
在 JavaScript 中,交换变量 a 和 b 的值有多种实现方式,以下为你详细介绍:
1. 使用临时变量
- 原理:这是最常见且直观的方法,借助一个临时变量来暂存其中一个变量的值,从而实现两个变量值的交换。
- 示例代码:
let a = 10;
let b = 20;
let temp = a;
a = b;
b = temp;
console.log(a);
console.log(b);
2. 不使用临时变量(算术运算)
- 原理:利用数学运算(加法和减法)来交换两个变量的值。
- 示例代码:
let a = 10;
let b = 20;
a = a + b;
b = a - b;
a = a - b;
console.log(a);
console.log(b);
- 注意事项:这种方法仅适用于数值类型的变量,并且当数值过大时,
可能会出现溢出问题。
3. 不使用临时变量(ES6 解构赋值)
- 原理:ES6 引入的解构赋值语法提供了一种简洁的方式来交换变量的值。
- 示例代码:
let a = 10;
let b = 20;
[a, b] = [b, a];
console.log(a);
console.log(b);
- 优点:代码简洁,易于理解,且适用于各种数据类型。
4. 不使用临时变量(位运算)
- 原理:使用按位异或运算符
^来交换两个整数变量的值。 - 示例代码:
let a = 10;
let b = 20;
a = a ^ b;
b = a ^ b;
a = a ^ b;
console.log(a);
console.log(b);
- 注意事项:此方法仅适用于整数类型的变量。
什么是类数组 ⭐️
JavaScript 中的 类数组对象(Array-like Objects) 是一种特殊的数据结构,它们具有类似数组的特征(如数字索引和 length 属性),但缺乏数组的原生方法(如 push、pop、forEach 等)。以下是类数组对象的全面解析:
一、类数组对象的特征
- 数字索引属性:通过
[0]、[1]等形式访问元素。 - length 属性:表示元素的数量。
- 不具备数组方法:如
push、slice等方法不可用。
示例:
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
二、常见的类数组对象
1. arguments 对象
- 场景:函数内部自动生成的参数集合(ES6 前常用)。
- 特点:
- 仅在函数体内可用。
- 不支持数组方法。
function example() { console.log(arguments); // 类数组对象 } example(1, 2, 3);
2. DOM 元素集合
NodeList:通过document.querySelectorAll获取。const nodeList = document.querySelectorAll('div'); // 类数组HTMLCollection:通过document.getElementsByClassName等获取。
3. 字符串(String)
- 字符串的字符可通过索引访问,且有
length属性:const str = 'hello'; console.log(str[0]); // 'h' console.log(str.length); // 5
五、手动创建类数组对象
可以通过定义对象模拟类数组结构:
const arrayLike = {
// 数字键属性
0: 'zero',
1: 'one',
2: 'two',
// length 属性
length: 3,
// 可选:添加迭代器使其可迭代(ES6)
[Symbol.iterator]: function* () {
for (let i = 0; i < this.length; i++) {
yield this[i];
}
}
};
// 使用 for...of 遍历(需要迭代器)
for (const item of arrayLike) {
console.log(item); // 'zero', 'one', 'two'
}
七、类数组的局限性及注意事项
- 无法直接使用数组方法:需转换或借用方法。
- 手动维护
length:修改元素时需同步更新length。 - 可迭代性:
默认不可迭代,需手动实现Symbol.iterator。
八、实际应用场景
- 函数参数处理(ES6 前):
Array.prototype.slice.call(arguments)function sum() { const args = Array.prototype.slice.call(arguments); return args.reduce((acc, val) => acc + val, 0); } - DOM 操作:
Array.from(buttons)const buttons = document.querySelectorAll('button'); const buttonsArray = Array.from(buttons); buttonsArray.forEach(btn => btn.addEventListener('click', handleClick)); - 与第三方库交互:某些 API 返回类数组结构,需转换后处理。
九、总结
| 关键点 | 说明 |
|---|---|
| 定义 | 具有数字索引和 length,但无数组方法的对象 |
| 常见例子 | arguments、NodeList、字符串 |
| 转换方法 | Array.from()、扩展运算符、slice.call() |
理解类数组对象有助于处理遗留代码和特定 API 返回的数据结构,合理转换为真数组可提升代码可读性和功能性。
类数组和数组的区别
| 特性 | 类数组对象 | 真数组 |
|---|---|---|
| 原型链 | 无 Array.prototype 方法 | 继承 Array.prototype 方法 |
| 类型检测 | Array.isArray(obj) → false | Array.isArray(arr) → true |
| 方法支持 | 不可直接使用 push、map 等 | 支持所有数组方法 |
| 创建方式 | 手动定义或 API 返回 | 通过 [] 或 new Array() |
arguments
其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载。Javascrip中每个函数都会有一个Arguments对象实例arguments,它引用着函数的实参,可以用数组下标的方式"[]"引用arguments的元素。arguments.length为函数实参个数,arguments.callee引用函数自身
- 1.arguments对象和Function是分不开的。
- 2.
因为arguments这个对象不能显式创建。 - 3.
arguments对象只有函数开始时才可用。
function demo(a,b,c){
console.log(...arguments);// 1 2 3
}
demo(1,2,3)
JS延迟加载的方式有哪些?
- defer属性、
- async属性、
- 动态创建dom方式、
- 使用setTimeout延迟方法、让js最后加载
说说严格模式的限制
严格模式主要有以下限制
变量必须声明后再使用
不能删除不可删除的属性,否则报错
不能对只读属性赋值,否则报错
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局对象
函数的参数不能有同名属性,否则报错
不能使用with语句
不能使用前缀0表示八进制数,否则报错
不能删除变量delete prop,会报错,只能删除属性delete global[prop]
eval不会在它的外层作用域引入变量
eval和arguments不能被重新赋值
arguments不会自动反映函数参数的变化
不能使用fn.caller和fn.arguments获取函数调用的堆栈
增加了保留字(比如protected、static和interface)
设立"严格模式"的目的,主要有以下几个
消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
消除代码运行的一些不安全之处,保证代码运行的安全;
提高编译器效率,增加运行速度;
为未来新版本的Javascript做好铺垫。
注:经过测试IE6,7,8,9均不支持严格模式。
attribute和property的区别是什么?
attribute是dom元素在文档中作为html标签拥有的属性;
property就是dom元素在js中作为对象拥有的属性。
对于html的标准属性来说,attribute和property是同步的,是会自动更新的,attributes是属于property的一个子集
- property能够从attribute中得到同步;
attribute不会同步property上的值;- attribute和property之间的数据绑定是单向的,attribute->property;
更改property和attribute上的任意值,都会将更新反映到HTML页面中;
但是对于自定义的属性来说,他们是不同步的,
函数防抖节流的原理,防抖和节流的区别(两题合并)
函数节流是指一定时间内js方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次。这是函数节流最形象的解释。
函数防抖是指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。
词法作用域和动态作用域
词法作用域和动态作用域是编程语言中关于变量作用域的两种不同概念,它们的主要区别如下:
定义与确定时机
- 词法作用域:也叫静态作用域 ,在代码的词法分析阶段(也就是写代码或定义时)就确定了,之后不会改变。它由变量和块作用域在代码中的书写位置决定 。比如在 JavaScript 中,函数的词法作用域由其声明时所处位置决定,无论函数在哪里被调用、如何被调用,作用域都不变。
- 动态作用域:变量作用域在程序运行时,根据函数的调用位置和顺序来动态确定 。它不关心函数和作用域在何处声明,只关注函数从哪里被调用。
作用域链与变量查找规则
- 词法作用域:作用域链基于代码的静态结构,比如函数的嵌套关系。查找变量时,从当前作用域(函数或块)开始,由内向外逐层查找,先在当前作用域找,找不到就去外层作用域找,直到全局作用域 。例如:
var x = 10;
function outer() {
var x = 20;
function inner() {
console.log(x);
}
inner();
}
outer();
这里 inner 函数中 console.log(x) 会先在 inner 作用域找 x,没找到就去 outer 作用域找,输出 20。
- 动态作用域:作用域链基于函数的调用栈。查找变量时,从当前函数的调用者开始,沿着调用栈向上找,先在调用者函数中找,找不到再去调用者的调用者函数中找,依此类推 。例如在支持动态作用域的语言中(如 Emacs Lisp ),如果函数
A调用函数B,函数B中访问变量时,会先去函数A的作用域找。
可预测性与代码分析难度
-
词法作用域:作用域相对固定,可预测性高,便于进行静态分析和优化。代码编写和阅读时,能较容易根据代码结构判断变量的作用域和可访问性 。大多数现代编程语言(如 C、C++、Java、JavaScript 等)都采用词法作用域。
-
动态作用域:依赖运行时上下文,可预测性低,在编写代码时难以完全确定变量的实际取值,不利于代码的静态分析和理解,增加了程序调试和维护难度 ,所以使用相对较少。
虽然 JavaScript 主要采用词法作用域,但理解动态作用域有助于更好地区分和掌握词法作用域的概念及特性。
词法作用域和this的区别
词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。
词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
需要明确的是,JavaScript并不具有动态作用域。它只有词法作用域,简单明了。但是this机制某种程度上很像动态作用域。
介绍this各种情况,箭头函数的this是什么 ⭐️⭐️
- call apply bind指的this是谁就是谁(bind不会调用,只会将当前的函数返回)
- fun.call(obj,a,b)
- fun.apply(obj,[ ])
- fun.bind(obj,a,b)()
this的情况
- 以函数形式调用时,this永远都是window
- 以对象方法的形式调用时,this是调用方法的对象
- 以构造函数的形式调用时,this是新创建的那个对象
- 使用call和apply调用时,this是指定的那个对象
箭头函数:箭头函数的this看外层是否有函数
如果有,外层函数的this就是内部箭头函数的this
如果没有,就是window
特殊情况:通常意义上this指针指向为最后调用它的对象。这里需要注意的一点就是如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例
闭包和this一起谈谈
箭头函数中的this是通过继承得到的,可以理解成其本身并没有this,是通过谁调用(其父对象来判断的,谁调用它谁就是this)并不像普通函数中的this一样,箭头函数中的this是看其定义时,test2();这种写法等同于window.text2();所以this就指向window对象,而new text2();这种写法意思是实例化一个text2对象,是构造函数,所以this指向的是text2对象
什么是变量提升
在JavaScript中,变量提升(Hoisting)是代码执行前将变量和函数声明提升至作用域顶部的机制。以下是分步解释及示例:
1. 变量声明提升(var)
- var声明的变量会被提升到作用域顶部,初始值为undefined。
console.log(a); // undefined
var a = 5;
// 相当于:
var a; // 提升声明
console.log(a); // undefined
a = 5; // 赋值留在原地
2. 函数声明提升
- 函数声明整体提升,可在声明前调用。
foo(); // "Hello"
function foo() {
console.log("Hello");
}
3. 函数表达式与变量提升
- 函数表达式仅变量名被提升,赋值不提升。
bar(); // TypeError: bar is not a function
var bar = function() {
console.log("World");
};
// 相当于:
var bar; // 提升声明,值为undefined
bar(); // 调用undefined()报错
bar = function() { ... };
4. let/const的暂时性死区(TDZ)
- let/const声明的变量提升但不可访问,直到声明语句执行。
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
5. 同名变量与函数声明的优先级
- 函数声明优先于变量声明。
console.log(c); // ƒ c() {}
var c = 1;
function c() {}
console.log(c); // 1
6. 块级作用域的影响
- var无视块,而let/const受限于块。
{
var e = 5; // 提升至全局作用域
let f = 10; // 仅块内有效
}
console.log(e); // 5
console.log(f); // ReferenceError
JavaScript 代码的执行分为两个阶段 ⭐️
- 第一个阶段在当前词法环境中注册所有的变量和函数声明,简单说就是,解析
- 第二个阶段的 JavaScript 执行就开始了!
JS中创建函数有两种方式:函数声明式和函数字面量式。只有函数声明才存在函数提升。 在 JavaScript 中,函数字面量(Function Literal) 是定义函数的一种方式,也称为 “函数表达式”(Function Expression)。它与函数声明(Function Declaration)的主要区别在于:函数字面量是在表达式上下文中定义的函数,通常会被赋值给一个变量或作为参数传递。
函数字面量的基本形式
// 函数字面量(匿名函数,赋值给变量)
const 函数名 = function(参数) {
// 函数体
};
- 所有的声明都会提升到作用域的最顶上去。
- 同一个变量只会声明一次,其他的会被忽略掉。
- 函数声明的优先级高于变量申明的优先级,并且函数声明和函数定义的部分一起被提升
- 只有声明本身会被提升,而赋值操作不会被提升。
- 变量会提升到其所在函数的最上面,而不是整个程序的最上面。
- 函数声明会被提升,但函数表达式不会被提升。
深拷贝和浅拷贝的区别?如何实现 ⭐️
深拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用,简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。
浅拷贝--- 浅拷贝是指复制对象的时候,只对第一层键值对进行独立的复制,如果对象内还有对象,则只能复制嵌套对象的地址
深拷贝---深拷贝是指复制对象的时候完全的拷贝一份对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。其实只要递归下去,把那些属性的值仍然是对象的再次进入对象内部一 一进行复制即可。
深拷贝
1、JSON.parse(JSON.stringify()),这种拷贝方法不可以拷贝一些特殊的属性(例如正则表达式,undefined,function)
2、用递归去复制所有层级属性
浅拷贝
1、object.assign(target,source) 对象
Object.assign 方法只复制源对象中可枚举的属性和对象自身的属性
如果目标对象中的属性具有相同的键,则属性将被源中的属性覆盖。后来的源的属性将类似地覆盖早先的属性
Object.assign 会跳过那些值为 [null] null 是一个 JavaScript 字面量,表示空值(null or an "empty" value),即没有对象被呈现(no object value is present)。它是 JavaScript 原始值 之一。") 或 undefined 的源对象。\
2.es6三个点
loadsh深拷贝实现原理 ⭐️
Lodash 是一个非常实用的 JavaScript 工具库,其中的深拷贝函数 _.cloneDeep 可以递归地复制对象及其所有嵌套的属性,确保新对象和原对象在内存中是完全独立的,修改新对象不会影响原对象。下面详细介绍其实现原理:
整体思路
Lodash 的深拷贝主要通过递归的方式来处理对象和数组。对于基本数据类型(如 number、string、boolean 等),直接返回其值;对于对象和数组,则创建一个新的对象或数组,并递归地对其属性或元素进行深拷贝。
注意点
复制代码递归容易造成爆栈,尾部调用可以解决递归的这个问题,Chrome 的 V8 引擎做了尾部调用优化,我们在写代码的时候也要注意尾部调用写法。递归的爆栈问题可以通过将递归改写成枚举的方式来解决,就是通过for或者while来代替递归
怎么实现this对象的深拷贝
- 手动递归实现方式灵活,但需要处理各种边界情况;
JSON.parse和JSON.stringify方法简单,但有诸多限制;- 使用第三方库则方便快捷,功能全面,推荐在实际项目中使用。
使用setTimeout代替setInterval进行间歇调用
原因:区别在于,setInterval间歇调用,是在前一个方法执行前,就开始计时,比如间歇时间是500ms,那么不管那时候前一个方法是否已经执行完毕,都会把后一个方法放入执行的序列中。这时候就会发生一个问题,假如前一个方法的执行时间超过500ms,加入是1000ms,那么就意味着,前一个方法执行结束后,后一个方法马上就会执行,因为此时间歇时间已经超过500ms了。
setTimeout(arguments.callee,intervalTime);
为什么会出现setTimeout倒计时误差?如何减少
JavaScript 单线程机制:JavaScript 是单线程语言,在同一时间只能执行一个任务。setTimeout 只是将回调函数放入任务队列中,当主线程空闲时才会去执行。如果主线程被其他任务占用,那么 setTimeout 设定的回调函数就会延迟执行,从而导致倒计时出现误差。例如,页面中正在进行复杂的计算或者有大量的 DOM 操作,就会阻塞 setTimeout 回调函数的执行
setTimeout(function () {
console.log('biubiu');
}, 1000);
某个执行时间很长的函数();
requestAnimationFrame
随着技术与设备的发展,用户的终端对动画的表现能力越来越强,更多的场景开始大量使用动画。在 Web 应用中,实现动画效果的方法比较多,JavaScript 中可以通过定时器 setTimeout 来实现,css3 可以使用 transition 和animation 来实现, html5 中的 canvas 也可以实现。
除此之外,html5 还提供一个专门用于请求动画的 API,即 requestAnimationFrame
应用
简单的进度条动画,用 requestAnimationFrame 动画比较平滑
优势
- 1.提升性能,防止掉帧
- 2.节约资源,节省电源
- 3.函数节流
立即执行函数和使用场景
声明一个函数,并马上调用这个匿名函数就叫做立即执行函数;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行
有时,我们定义函数之后,立即调用该函数,这时不能在函数的定义后面直接加圆括号,这会产生语法错误。产生语法错误的原因是,function 这个关键字,既可以当做语句,也可以当做表达式,比如下边:
//语句
function fn() {};
//表达式
var fn = function (){};
为了避免解析上的歧义,JS引擎规定,如果function出现在行首,一律解析成语句。因此JS引擎看到行首是function关键字以后,认为这一段都是函数定义,不应该以原括号结尾,所以就报错了。解决方法就是不要让function出现在行首,让JS引擎将其理解为一个表达式,最简单的处理就是将其放在一个圆括号里,比如下边:
(function(){
//code
}())
(function (){
//code
})()
上边的两种写法,都是以圆括号开头,引擎会意味后面跟的是表达式,而不是一个函数定义语句,所以就避免了错误,这就叫做"立即调用的函数表达式"。
立即执行函数,还有一些其他的写法(加一些小东西,不让解析成语句就可以),比如下边:
(function () {alert("我是匿名函数")}()) //用括号把整个表达式包起来
(function () {alert("我是匿名函数")})() //用括号把函数包起来
!function () {alert("我是匿名函数")}() //求反,我们不在意值是多少,只想通过语法检查
+function () {alert("我是匿名函数")}()
-function () {alert("我是匿名函数")}()
~function () {alert("我是匿名函数")}()
void function () {alert("我是匿名函数")}()
new function () {alert("我是匿名函数")}()
立即执行函数的作用
- 不必为函数命名,避免了污染全局变量
- 立即执行函数内部形成了一个单独的作用域,
- 可以封装一些外部无法读取的私有变量封装变量总而言之:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量
立即执行函数使用的场景
避免全局变量污染
在网页开发中,多个脚本可能会被合并或加载,如果不注意,很容易导致全局变量冲突。立即执行函数可以创建一个独立的作用域,将内部的变量和函数封装起来,避免污染全局作用域。
(function () {
var localVar = '我是局部变量,不会污染全局';
console.log(localVar);
})();
console.log(localVar); // 这里会报错,因为 localVar 不在全局作用域中
模块封装
当需要将相关的功能代码封装成一个模块时,立即执行函数可以提供一个封闭的环境,防止模块内部的变量和函数被外部随意访问和修改,同时可以通过返回值暴露一些公共的方法或属性。
const myModule = (function () {
// 私有变量
let privateVar = 10;
// 私有函数
function privateFunction() {
console.log('这是一个私有函数');
}
// 公共函数
function publicFunction() {
console.log('这是一个公共函数,能访问私有变量:' + privateVar);
}
return {
publicFunction: publicFunction
};
})();
myModule.publicFunction(); // 能正常调用公共函数
myModule.privateFunction(); // 报错,无法访问私有函数
立即执行函数的参数
(function(j){
//代码中可以使用j
})(i)
如果立即执行函数中需要全局变量,全局变量会被作为一个参数传递给立即执行函数(上例中的i就是一个全局变量,i代表的是实参,j是i在立即执行函数中的形参)。
说一下如何实现闭包
二、闭包的形成条件 ⭐️
- 嵌套函数:函数内部定义另一个函数。
- 引用外部变量:内部函数引用了外部函数的变量。
- 内部函数仍然保留:内部函数被返回、传递或绑定到其他位置。
JavaScript 闭包是一个强大的特性,它允许函数访问并记住其词法作用域中的变量,即使函数在其作用域外执行。以下是闭包的详细解析:
一、闭包的定义
闭包(Closure)是 函数与其定义时的词法环境的结合。当一个内部函数引用了外部函数的变量,且该内部函数在外部函数执行完毕后仍存在(如被返回、传递给其他函数或绑定到事件),就会形成闭包。闭包使得内部函数能够持续访问外部函数的作用域链中的变量。
示例代码
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner; // 返回内部函数,形成闭包
}
const closureFunc = outer();
closureFunc(); // 1
closureFunc(); // 2
五、闭包的注意事项
1. 内存泄漏
闭包可能导致外部函数的作用域变量无法被垃圾回收。若闭包不再需要,应解除对其的引用。
// 解除闭包引用示例
let closure = outer();
closure = null; // 手动解除引用
2. 循环中的闭包陷阱
在循环中使用闭包时,需注意变量捕获的时机。
// 错误示例:输出5次5
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// 解决方案1:使用let(块级作用域)
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// 解决方案2:IIFE创建新作用域
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
3. this指向问题
闭包中的this可能指向全局对象(非严格模式)或undefined(严格模式),需通过bind、箭头函数或保存this来绑定正确上下文。
const obj = {
value: 10,
getValue: function() {
const inner = () => console.log(this.value); // 箭头函数捕获外层this
inner();
}
};
六、闭包与垃圾回收
- 闭包保留引用:闭包会使外部函数的变量保持在内存中,直到闭包不再被引用。
- 手动释放内存:通过将闭包引用设为
null,可以触发垃圾回收。
七、总结
| 特性 | 说明 |
|---|---|
| 本质 | 函数与其词法作用域的结合 |
| 核心机制 | 作用域链保留,变量引用不释放 |
| 应用场景 | 数据封装、柯里化、事件处理、记忆化 |
| 优点 | 实现私有变量、模块化、延迟执行 |
| 缺点 | 潜在内存泄漏、需注意循环陷阱和this指向 |
| 最佳实践 | 及时解除引用、优先使用let/const、利用模块化减少全局污染 |
闭包是 JavaScript 的核心概念之一,深入理解其机制能帮助开发者编写更高效、更安全的代码。
对闭包的理解,闭包的核心是什么
- 保持对外部作用域的引用
- 延长变量的生命周期
- 创建函数的私有状态和方法
- 实现数据封装与隐藏
工程中闭包使用场景x2
应用闭包的主要场合是:设计私有的方法和变量。
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和函数内定义的其他函数。把有权访问私有变量的公有方法称为特权方法(privileged method)。
- 模块化开发
- 函数柯里化
- 事件处理(循环注册dom事件中的index)
- setTimeOut中的闭包应用
四、闭包的常见应用场景
1. 模块化开发
在模块化编程里,需要将代码封装起来,避免全局变量污染,同时对外提供特定的接口。闭包可以实现这一需求,将私有变量和函数封装在内部,只暴露必要的公共接口。
const myModule = (function () {
// 私有变量
let privateVariable = 0;
// 私有函数
function privateFunction() {
privateVariable++;
}
// 公共接口
return {
increment: function () {
privateFunction();
return privateVariable;
},
getValue: function () {
return privateVariable;
}
};
})();
console.log(myModule.getValue());
console.log(myModule.increment());
2. 事件处理
在处理事件时,可能需要在事件处理函数中访问外部的一些状态信息。闭包可以让事件处理函数记住这些外部状态。
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', (function (index) {
return function () {
console.log(`你点击了第 ${index + 1} 个按钮`);
};
})(i));
}
3. 函数柯里化
函数柯里化是把一个多参数函数转换为一系列单参数函数的技术。闭包在函数柯里化中起着关键作用,使得每个返回的函数都能记住之前传入的参数。
function add(a, b) {
if (typeof b === 'undefined') {
return function (b) {
return a + b;
};
}
return a + b;
}
const addFive = add(5);
console.log(addFive(3));
4. 异步编程与回调
在异步编程中,回调函数常常需要访问外部函数的变量。闭包能确保回调函数在执行时可以访问这些变量。
function asyncOperation(callback) {
let data = '异步获取的数据';
setTimeout(() => {
callback(data);
}, 1000);
}
asyncOperation((result) => {
console.log(result);
});
5. 实现私有变量和方法
闭包可以模拟类的私有成员,让变量和方法只能在特定的作用域内被访问和修改。
function Person(name) {
let privateName = name;
this.getName = function () {
return privateName;
};
this.setName = function (newName) {
privateName = newName;
};
}
const person = new Person('Alice');
console.log(person.getName());
person.setName('Bob');
console.log(person.getName());
6. 循环中的定时器
在循环中使用定时器时,闭包可以确保每个定时器都能正确捕获循环变量的值。
for (let i = 0; i < 5; i++) {
(function (index) {
setTimeout(() => {
console.log(index);
}, index * 1000);
})(i);
}
综上所述,闭包在工程开发的多个方面都有重要应用,它能够帮助开发者更好地组织代码、管理状态和实现特定的功能需求。但需要注意的是,不当使用闭包可能会导致内存泄漏,因此在使用时要谨慎。
7. 记忆化(Memoization)
缓存函数计算结果,提升性能。
function memoize(fn) {
const cache = {};
return (...args) => {
const key = JSON.stringify(args);
return cache[key] || (cache[key] = fn(...args));
};
}
介绍闭包以及闭包为什么没清除
闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
在javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收;如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。将用完的函数或者变量置为null,不过现在V8引擎做了很多优化,根本不需要考虑了。
添加原生事件不移除为什么会内存泄露
因为都DOM 被删除了 事件没有被取消