首页/新闻资讯/正文详情

JavaScript学习笔记:字符串包含、数组排序与异步编程

发布时间:2026/9/26 6:20:16 来源:云帆数科 栏目:资讯中心
JavaScript学习笔记:字符串包含、数组排序与异步编程
说实话JavaScript 这门语言我已经写了挺多年但真正把学习笔记整理成一条完整知识线还是最近的事。网上一搜“js 判断字符串是否包含”“js 数组排序”这类零散问题一大把可初学者照着抄完代码下个场景照样懵。我这份笔记的核心思路是不把语法当考点背而是围绕一个真实小项目把数据类型、运算符、函数、数组操作、异步流程和工程场景串起来。无论你刚接触前端还是写过一点 HTML 和 CSS 想补上 JS 这块拼图这份笔记都能帮你少走很多弯路。1. 整体思路把 JS 当成“给网页打招呼的语言”很多人学 JavaScript 的第一个动作是打开控制台打印一句console.log这没问题。但我更推荐在动手之前先建一个知识地图知道 JS 在网页里到底扮演什么角色。1.1 先搞懂 HTML、CSS、JS 的关系我一直喜欢用这个类比HTML 是毛坯房CSS 是装修JS 是水电和智能家居。HTML 负责结构比如页面上有一个按钮、一个输入框、一个列表。CSS 负责外观比如按钮是蓝色还是圆角、列表是横排还是竖排。JS 负责行为比如点击按钮后弹窗、输入关键词后筛选列表、提交表单后更新页面。如果你只写 HTML 和 CSS页面是静态的用户怎么点都不会有反应。一旦引入 JS页面就“活”了。在学习过程中我强烈建议每学一个 JS 特性都立刻想一个使用场景判断字符串是否包含关键词能用来做搜索框数组排序能用来给列表按时间排列操作对象取值能用来读取后端返回的数据。1.2 用一个例子贯穿所有知识点这份笔记的“主线任务”是一个待办事项管理器。它需要做这几件事输入框输入内容点击按钮后把内容加入列表用到数据类型、数组操作、DOM 操作搜索框输入关键词只显示匹配的条目用到字符串判断点击“排序”按钮按创建时间或者内容长度排序用到数组排序删除重复条目用到数组合并去重处理加载数据时的延迟用到同步异步一个小项目几乎覆盖了 80% 的基础语法。比起背知识点这种“我要做功能→需要什么语法→查笔记→写代码→调试”的路径记忆留存率高得多。1.3 学习路径怎么规划我复盘自己走过的弯路总结了一个比较顺的阶段顺序你可以在笔记目录里按这个顺序浏览语言基础数据类型、运算符、条件、循环函数进阶回调、作用域、执行上下文、闭包数据操作字符串方法、数组方法、对象取值异步编程回调函数、Promise、async/await工程实践引入方式、DOM、浏览器 API、常见工具库调试排查阅读报错、使用 DevTools、定位问题下文就按照这个顺序把每一块的关键点和踩坑经验都讲清楚。2. 语言地基数据类型、运算符和流程控制这部分是 JS 最容易“一看就会一用就废”的地方。因为表面语法和直觉差别不大但细节坑特别多。2.1 数据类型typeof 的五个坑JS 的原始类型有七种实际使用中你至少要熟练区分这几种string、number、boolean、null、undefined、object、symbol我当年最容易被typeof骗到下面这些结果都记在小本本上表达式typeof 结果实际原因typeof 42number数字正常typeof hellostring字符串正常typeof trueboolean布尔正常typeof undefinedundefinedundefined 是独立类型typeof nullobject历史遗留 bugnull 不是对象typeof []object数组本质是对象typeof function(){}function函数特殊处理判断一个变量是不是数组不要用typeof要用Array.isArray()。判断 null 要用 null。这些属于“语法背不出来但调试时会被反复教做人”的经验。2.2 运算符 和 的差距以及 ?? 的妙用新手最容易犯的错误写条件判断时用了。JS 里的会做隐式类型转换规则极其反直觉。比如0 0 // true 0 // true false // true null undefined // true我建议业务代码里一律用和!这样既避免隐式转换的坑也让代码语义更清晰。想判断“值相等且类型相同”就用这是最稳的写法。还有一个运营频率很高的运算符是空值合并??。它和||看起来像但差别很大const a 0; const b a || 10; // 10因为 0 被当成 falsy const c a ?? 10; // 0因为 ?? 只处理 null 和 undefined我在做配置项默认值的时候如果这个值可能是 0 或者空字符串一定用??而不是||否则用户填了 0 也会被无脑替换掉。2.3 条件语句与循环语句学会写“人能读懂”的代码JS 的条件语句常见有三种if...else、switch、三元表达式? :。if...else适合复杂逻辑switch适合对同一个值做多个分支判断三元表达式适合简单的二选一赋值循环语句也一样至少掌握四套写法// 传统 for 循环 for (let i 0; i arr.length; i) { console.log(arr[i]); } // for...of 循环推荐直接拿值 for (const item of arr) { console.log(item); } // for...in 循环注意一般用来遍历对象键名别用来遍历数组 for (const key in obj) { console.log(key, obj[key]); } // forEach 循环不能中途 break arr.forEach((item, index) { console.log(index, item); });我自己的习惯是数组用for...of或forEach对象用Object.keys()或者for...in。for...in遍历数组会把原型链上的可枚举属性也带上很容易出意想不到的 bug初学者尽量避开。3. 函数是 JS 的灵魂定义、回调与执行上下文如果只能挑一个章节让人反复读我一定会选函数。JS 的函数不仅是一段可复用逻辑更是一等公民可以赋值给变量、作为参数传递、作为返回值返回。3.1 函数定义的几种方式和使用区别常见四种写法// 1. 函数声明 function add(a, b) { return a b; } // 2. 函数表达式 const add function (a, b) { return a b; }; // 3. 箭头函数 const add (a, b) a b; // 4. 构造函数方式不推荐日常使用 const add new Function(a, b, return a b);这里的核心区别有两个。第一函数声明会“提升”也就是在定义之前调用也不会报错。函数表达式和箭头函数不行它们在执行到那一行之前变量是undefined。第二箭头函数没有自己的this它继承定义时所在作用域的this。这个特性在写事件回调时特别省心但也导致它不能用来做构造函数。3.2 回调函数不立即执行而是“回头再调”回调函数这个概念我第一次看定义觉得玄乎其实一句话就能讲清楚把一个函数作为参数传给另一个函数对方在合适的时机调用它。举一个日常例子function processUserData(userId, onSuccess, onError) { // 模拟异步获取数据 setTimeout(() { const data { id: userId, name: 张三 }; onSuccess(data); }, 1000); } processUserData( 1, (data) console.log(拿到用户, data.name), (err) console.error(出错了, err) );这里onSuccess和onError就是回调函数。按钮点击、定时器、网络请求全是基于回调的。理解了回调后面再学 Promise 就不费力。3.3 执行上下文与作用域链为什么循环里的“var i”是坑执行上下文可以理解为 JS 引擎在运行代码时的“环境快照”里面包含变量、函数声明、this指向等。全局有全局执行上下文每次调用函数都会创建一个新的函数执行上下文。作用域链则决定了变量从哪里找当前函数内找不到就往定义它的外层找一直找到全局作用域。这个机制带来了“闭包”也带来经典的var循环陷阱for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出 3 3 3而不是 0 1 2原因就是var声明的i属于函数作用域循环结束后i已经是 3每个 setTimeout 回调记住的其实是同一个变量。解决方法是把var换成let让每次循环都产生一个块级作用域或者用立即执行函数包一层。3.4 闭包既好用又容易被忽视闭包就是“函数 它定义时所处的作用域”。最简单的闭包function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2外部函数执行完后内部的count并没有被垃圾回收因为返回的函数还持有对它的引用。日常写防抖、节流、封装私有变量都在用闭包。但也要注意闭包用多了可能会让变量长期占用内存如果不再使用最好把引用置空。4. 字符串与数组处理数据的日常操作写业务代码最花时间的不是语法而是处理用户输入和后端返回的数据。字符串判断、数组排序、合并去重、对象取值基本是每天必用。4.1 判断字符串是否包含includes、indexOf 还是正则初学者最常见的问题“js 判断字符串是否包含”用哪个方法我直接给结论只判断是否包含includes需要知道位置或统计次数indexOf忽略大小写先toLowerCase()再includes复杂模式匹配正则test()const str Hello JavaScript World; console.log(str.includes(Script)); // true console.log(str.indexOf(Java)); // 6没有则返回 -1 console.log(str.toLowerCase().includes(hello)); // true console.log(/j.*s/i.test(str)); // true正则忽略大小写一个小细节includes第二个参数可以指定开始搜索的位置。比如判断aaa.includes(aa, 1)结果也是true因为从索引 1 开始还能匹配到aa。这个参数在处理“排除开头匹配”时很有用。4.2 数组排序sort() 的默认行为容易出乎意料很多人直接用arr.sort()然后发现 10 排在 2 前面。因为sort()默认是把元素转成字符串后按 Unicode 编码排序所以数字2会排在10前面。正确写法必须传比较函数const arr [10, 2, 22, 3]; // 升序 arr.sort((a, b) a - b); // 降序 arr.sort((a, b) b - a);按对象属性排序也一样const tasks [ { title: 写笔记, createdAt: 20230105 }, { title: 复习, createdAt: 20230102 }, ]; tasks.sort((a, b) b.createdAt - a.createdAt); // 按时间倒序注意sort()会修改原数组。想保留原数组先[...arr].sort(...)再做后续操作这是高频踩坑点。4.3 数组合并去重Set 是你的好朋友数组去重方法很多最简洁的写法是用Setconst arr1 [1, 2, 3]; const arr2 [2, 3, 4]; // 合并 const merged [...arr1, ...arr2]; // 合并并去重 const unique [...new Set(merged)]; // [1, 2, 3, 4]对象数组去重不能用这个办法因为每个对象引用不同。需要根据某个字段去重const users [ { id: 1, name: 张三 }, { id: 2, name: 李四 }, { id: 1, name: 张三 }, ]; const map new Map(); users.forEach((u) map.set(u.id, u)); const deduplicated [...map.values()];4.4 获取对象的所有值别手动循环了经常有后端返回一个对象比如{ 任务A: 3, 任务B: 5 }你只想把值收集成一个数组。用三件套const obj { taskA: 3, taskB: 5, taskC: 8 }; const keys Object.keys(obj); // [taskA, taskB, taskC] const values Object.values(obj); // [3, 5, 8] const entries Object.entries(obj); // [[taskA, 3], ...]需要“获取字典的所有值”时Object.values()一行搞定。需要同时用键和值做处理就解构entriesObject.entries(obj).forEach(([key, value]) { console.log(key, value * 2); });5. 同步与异步理解 JS 的运行机制“js 同步和异步”这个话题很多同学问得最多其实理解起来并不难关键看能不能分清“谁先执行”和“谁后执行”。5.1 为什么需要异步JS 是单线程语言同一时间只能做一件事。如果网络请求要等 2 秒期间不让用户操作页面就会卡死。所以 JS 把耗时任务交给浏览器去处理等任务完成后再把回调放进一个队列里这就是异步的基础思路。异步编写方式经典的三个阶段回调函数 → Promise → async/await。5.2 事件循环记住一个顺序就够了事件循环可以简化成一句话同步代码先执行然后不断从任务队列里取出任务执行。微任务的优先级高于宏任务。console.log(1); setTimeout(() console.log(2), 0); Promise.resolve().then(() console.log(3)); console.log(4); // 输出结果1 4 3 2为什么Promise.then输出在setTimeout前面因为 Promise 的回调属于微任务而setTimeout属于宏任务。浏览器执行完一段同步代码后会先把所有微任务执行完再去取宏任务执行。5.3 Promise 和 async/await告别回调地狱简单理解 Promise 就是一个容器里面装的是一段异步操作。它有三个状态pending进行中、fulfilled成功、rejected失败。状态一旦改变就不会再变。function fetchTask() { return new Promise((resolve, reject) { setTimeout(() { resolve(下载完成); }, 1000); }); } fetchTask() .then((data) console.log(data)) .catch((err) console.error(err));async/await只是让 Promise 的链式写法更可读async function load() { try { const data await fetchTask(); console.log(data); } catch (err) { console.error(err); } }注意await只能在async函数里用。如果循环里需要串行请求可以用for...of配合await如果多个互不依赖的请求用Promise.all并发执行速度会快很多。5.4 实战案例用异步处理待办事项的“假请求”回到我们的待办事项项目假设要从服务端拿任务列表我用一个setTimeout模拟延迟async function loadTasks() { const list document.getElementById(taskList); list.innerHTML 加载中...; try { const tasks await new Promise((resolve) setTimeout(() resolve([买牛奶, 写周刊, 读书]), 800) ); renderTasks(tasks); } catch (err) { list.innerHTML 加载失败请重试; } }调接口的时候切忌“拿到接口就写在render里”一定要有加载态和错误态。用户不会喜欢一个点了没反应的按钮。6. 工程场景把 JS 放进真实项目学基础语法只是第一步真正让人印象深刻的是把 JS 和各种浏览器 API、外部库结合起来解决实际问题。6.1 三种引入方式怎么选行内事件处理button onclickalert(hi)适合演示不推荐用于复杂逻辑内联脚本script...代码.../script适合单页面小功能外部脚本script srcapp.js/script推荐正式项目使用外部脚本里有个常见报错“script 标签放在 body 底部或者使用defer属性”因为脚本执行时如果 DOM 还没解析完document.getElementById会拿到null。script srcapp.js defer/scriptdefer会等到 HTML 解析完成后再下载执行脚本是当前最推荐的引入方式。6.2 DOM 操作打开 URL、关闭弹窗、刷新父页面浏览器页面上高频操作就几类打开新页面window.open(url)或者设置location.href url在当前页面跳转location.href url新标签页打开window.open(url, _blank)关闭当前弹窗window.close()还有一个实际项目经常遇到的需求父页面打开了一个 iframe 弹窗操作完成后要关闭 iframe 并刷新父页面。代码很简单但很多人不知道写在哪// 在 iframe 内部执行 window.parent.location.reload(); // 刷新父页面 window.parent.closeModal(); // 调用父页面的弹窗关闭方法如果想更“正规”一点用window.parent.postMessage()通知父页面执行逻辑避免直接耦合父页面的全局函数。6.3 操作 Excel推荐 xlsx 库“操作 excel 的 js 工具库 - xlsx 的使用方法”这个问题几乎每周都有人搜。最常用的是SheetJS安装方式一句npm install xlsx读取文件的核心三步import * as XLSX from xlsx; const fileInput document.getElementById(upload); fileInput.addEventListener(change, (e) { const file e.target.files[0]; const reader new FileReader(); reader.onload (event) { const data new Uint8Array(event.target.result); const workbook XLSX.read(data, { type: array }); const sheet workbook.Sheets[workbook.SheetNames[0]]; const json XLSX.utils.sheet_to_json(sheet); console.log(json); }; reader.readAsArrayBuffer(file); });生成并导出一个 Excel 也一样简洁const worksheet XLSX.utils.json_to_sheet([ { 标签: 学习, 数量: 3 }, { 标签: 工作, 数量: 5 }, ]); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 分类统计); XLSX.writeFile(workbook, 统计.xlsx);用这个库做数据导出比服务端生成 Excel 再下载简单很多。一个小提醒文件较大时最好在 Web Worker 里处理避免菜单卡顿。6.4 现代框架入门从 Angular 到 Vue 的版本问题不少人在笔记里记了“angular9 与 node js 的版本”这个问题。Angular 对 Node.js 版本有要求一般官方文档都会标注推荐版本Node 版本太老或者太新都可能触发依赖安装报错。遇到这类问题第一反应去查官方文档的 compatibility 表格而不是自己瞎猜。Vue 的核心思想则很适合 JS 学习者数据驱动视图。你只需要改数据页面自动更新这背后本质上还是 JS 的响应式原理和 DOM 操作只是封装好了。学习路线建议先用原生 JS 写小项目再去看 Vue 文档会发现自己已经能读懂大半原理。7. 常见报错与调试排查DevTools 是最好的老师很多人学 JS 最崩溃的不是语法而是“页面白屏了但不知道错在哪”。这时候别慌打开浏览器开发者工具按 F12切到 Console 面板所有报错都列在那里。7.1 高频报错速查表报错信息常见原因快速排查xxx is not defined变量名拼错或未声明检查声明位置和作用域Cannot read properties of null元素没找到拿不到 DOM检查 script 标签位置、id 名jquery-3.6.1.min.js 404资源路径不对右键查看请求地址确认文件是否真的存在Unexpected token语法错误比如中文标点看报错行号逐字符排查is not a function变量不是函数却被调用打印该变量看类型Cannot read properties of undefined对象没定义或层级不对先用console.log打印完整对象文件找不到的问题比语法错误更隐蔽。比如本地项目里引了script srcjs/jquery-3.6.1.min.js实际文件却在js/lib/目录下就会 404。我建议打开 Network 面板看请求路径再对着项目文件目录一步步比对基本都能解决。7.2 调试流程先找现象再定位范围遇到 bug不要直接在源码里乱改。我的标准调试流程是复现问题记录触发步骤。打开 Console 面板看第一行报错。点报错右侧的链接跳到 Sources 面板对应行。在可疑位置打console.log或打断点观察变量值。修正后验证顺手跑一遍相关功能。断点调试比console.log更强的地方在于它能暂停在指定行左侧面板可以看到所有局部变量的实时状态还能单步执行。调试陌生页面代码时这种方式尤其有效。7.3 学会“读”别人的 JS 代码不管你是接手前辈的项目还是在网上看示例代码都要养成先总后分的习惯。先看整体结构哪些是变量声明哪些是函数定义哪些是入口调用再挑核心函数逐行阅读最后在关键节点打断点观察运行过程。遇到陌生项目先在 Sources 面板看加载了哪些 JS 文件顺着入口文件往下追。这个过程不需要特殊工具但是对理解别人的语法风格和代码组织方式帮助很大。我自己的学习笔记里有相当一部分知识就是在这种“读代码 打断点 改代码”的循环中学到的。8. 最后想分享的一点学习习惯如果要让我给这份“js 学习笔记”一个真正的核心那我会说不要为了学而学而是为了做一个东西而学。每次遇到新语法就想想能不能把它用到你的待办事项项目、个人主页或者小工具里。遇到报错不要急着复制粘贴关键词找答案先尝试自己读一遍报错信息。我在整理笔记的过程中反复体会到JS 的知识点之间是互相连通的字符串方法、数组方法、函数回调、异步流程、DOM 操作最终都汇入同一个目标——让页面更好地响应用户的操作。希望这份笔记也能成为你自己知识体系的第一块基石之后无论去学 Vue、Angular 还是 Node都会发现底层还是这些基础的东西在支撑。

相关推荐

嵌套路由全解析:原理、配置与常见坑位详解
嵌套路由全解析:原理、配置与常见坑位详解

嵌套路由这个玩意儿,我估计很多前端同学刚接触的时候没少绕弯子。看起来不就是「路由里面套路由」吗?但真正落地的时候,页面死活不渲染、子路由匹配不上、刷新就白屏,各种问题层出不穷。做了这么多年前端,我自己的体感… · 2026/9/26 6:20:16

视易S69点歌机刷机全指南:RK3288固件烧录与硬件适配
视易S69点歌机刷机全指南:RK3288固件烧录与硬件适配

1. 项目概述:这不是“刷个机”,而是点歌系统底层能力的重置与释放“视易S69点歌机刷机包下载|配套刷机工具图文视频教程(含单机版/网络版全型号支持)”——这个标题里藏着的,不是简单的文件搬运&#xff0c… · 2026/9/26 6:20:16

二叉树直径(LeetCode 543)递归解法:原理、调试与变体延伸
二叉树直径(LeetCode 543)递归解法:原理、调试与变体延伸

刷LeetCode热题100的时候,二叉树相关的题目里,543这道“二叉树的直径”看起来人畜无害,结果点开评论区,发现一堆人跟我当初一样:代码跑起来就是报运行时错误,或者一提交就在某个隐藏用例上翻车。今天就把这… · 2026/9/26 6:20:16

243.Fastboot 与 EDL 协议底层拆解,读懂安卓最核心的两种刷机通道
243.Fastboot 与 EDL 协议底层拆解,读懂安卓最核心的两种刷机通道

摘要: 本文面向具备一定计算机基础的开发人员与维修工程师,系统性地阐述安卓手机刷机与维修的底层逻辑。文章从Android分区表结构、Bootloader引导流程讲起,深入解析Fastboot与EDL(Emergency Download)模式通信协议。通过三个典型实战案例(系统崩溃救砖、Magisk Root提权… · 2026/9/26 6:57:59

抗遗忘单词表:科学记忆效果与真实学习体验展示
抗遗忘单词表:科学记忆效果与真实学习体验展示

很多孩子学英语都有个共同的痛点:单词背的时候挺熟,合上书转头就忘。家长看着着急,报班、买书、盯着背,钱花了不少,时间也搭进去了,可一到考试还是因为词汇量不够丢分。其实,这真不是孩子记性差… · 2026/9/26 6:57:59

战略规划部关键绩效指标设计与战略目标实现路径优化解析
战略规划部关键绩效指标设计与战略目标实现路径优化解析

在当今的企业管理中,绩效考核不仅是评价员工表现的工具,它还涉及到战略目标的实现和业务推进。通过精准的KPI(关键绩效指标)设计和监控,企业能够更好地掌握业务进展,优化资源配置,并确保战略执行的高效性。 本文将对绩效考核中几个核心KPI指标进行详细解读,并结合实际… · 2026/9/26 6:57:59

AI改文档总翻车?用Git搭一套后悔药机制
AI改文档总翻车?用Git搭一套后悔药机制

AI 改文档这件事,用好了是效率神器,用翻了就是返工灾难。我自己的翻车经历就挺典型:让 AI 帮忙“润色”一份技术方案,它把标题梳理得漂漂亮亮,却把接口延迟参数从 37ms 悄悄改成了 50ms,还删掉了一段部署环… · 2026/9/26 6:57:53

共享厨房租赁系统开发实战:Spring Boot+MyBatis设计核心
共享厨房租赁系统开发实战:Spring Boot+MyBatis设计核心

做共享厨房租赁这个选题的毕设,在很多老师眼里可能觉得就是个普通的管理系统,但我自己做完一遍之后最大的感受是:业务逻辑的深度决定了这个项目的上限。技术框架确实就是Spring Boot那一套,难点全在“租赁”这两个字——怎么设计订… · 2026/9/26 6:57:53

Apache APISIX 演进史:从颠覆 Kong 到顶级 AI 网关
Apache APISIX 演进史:从颠覆 Kong 到顶级 AI 网关

在当今云原生基础设施与大规模分布式系统领域,Apache APISIX 是中国开源项目走向国际顶级开源软件基金会最为耀眼的标杆之一。 今天,无论是在腾讯、百度、Bilibili、中国移动等国内互联网与运营商的生产集群中,还是在欧盟数字机构、Airbus&am… · 2026/9/26 6:57:53

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码