news 2026/10/5 8:09:20

JavaScript基础核心知识点与实战避坑指南:从类型判断到异步编程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript基础核心知识点与实战避坑指南:从类型判断到异步编程

JS基础这一块,我估计是每个前端人都绕不过去的坎。哪怕你后面用了再多的框架,Vue、React、Angular,绕来绕去,最后啃的其实还是原生JavaScript这颗硬骨头。我在带团队的时候发现一个规律:基础语法扎实的人,看框架源码就像看小说,遇到bug也能快速定位;基础不牢的,光一个this指向问题就能折腾一上午。这篇笔记是我自己整理的核心知识点,加上这些年踩过的坑,希望能帮新手捋顺思路,也能帮进阶的朋友查漏补缺。

1. 数据类型与类型判断:从底层逻辑到面试高频坑

1.1 基础数据类型详解

JavaScript里数据类型分两大类:原始类型(Primitive)和引用类型(Reference)。原始类型有七种:string、number、boolean、null、undefined、symbol、bigint。引用类型主要就是object,包括array、function、date、regexp、map、set这些。

这里有个非常容易踩坑的点:typeof null返回的是"object",这是个历史遗留bug,从ES1时代就存在,官方也没打算修。所以判断null不能依赖typeof,要用Object.prototype.toString.call()或者直接=== null。

再说说number类型。它不是纯粹的整数或浮点数,是双精度64位二进制浮点格式。最经典的坑就是0.1 + 0.2 !== 0.3,这个不用多讲了。实际开发中遇到金额计算,我一般建议用整数分来算,或者引入decimal.js这类库。NaN也很特殊,typeof NaN返回number,但NaN === NaN是false。判断NaN我用Number.isNaN()而不是全局的isNaN(),因为后者会把字符串也转换了再判断,容易误判。

1.2 typeof与instanceof的适用边界

typeof适合判断原始类型,但区分不了具体对象类型。instanceof适合判断引用类型,但有个问题:它能沿着原型链往上找。所以如果你有跨iframe的场景,instanceof可能失效。

我给个经验结论:

  • 判断字符串、数字、布尔、undefined、symbol、bigint,用typeof
  • 判断null,用value === null
  • 判断数组,用Array.isArray(value)
  • 判断纯对象,用Object.prototype.toString.call(value) === '[object Object]'
  • 判断日期,用value instanceof Date配合!isNaN(value.getTime())

1.3 动态类型与隐式转换的避坑指南

JS是动态类型语言,变量可以随意赋不同类型的值。这带来灵活性的同时,也带来一堆隐式转换的坑。

核心规则:+运算符,如果任一侧是字符串,就做字符串拼接;其他运算符尽量会把两边转成数字。==会做类型转换,===不做。我强烈建议项目里全局禁用==,ESLint规则eqeqeq打开,省去一堆破事。

还有一个常见场景:if判断里,0、空字符串、null、undefined、NaN、false都会被转成false。所以if (someString)这种写法要小心,空字符串也会走false分支。严谨的判断应该是if (typeof someString === 'string' && someString.length > 0)。

2. 函数与作用域:理解闭包和执行上下文的本质

2.1 函数声明、函数表达式与立即执行函数

函数有三种常见的定义方式,它们之间有微妙差异。

函数声明会提升(hoisting),函数表达式不会。提升的意思是,在代码执行前,解析阶段就把函数名和函数体绑到了当前作用域顶部。所以可以这样:

// 函数声明:可以提前调用 sayHello(); // 正常运行 function sayHello() { console.log('hello'); } // 函数表达式:会报错,sayHi是undefined sayHi(); // TypeError: sayHi is not a function var sayHi = function() { console.log('hi'); };

因为var声明的变量会提升声明但不会提升赋值,所以调用时是undefined,自然报错。

立即执行函数(IIFE)的写法我常用两种:(function(){ ... })()和(function(){ ... }())。作用就一个:创建一个独立的作用域,不污染外部。现在有了块级作用域let和const,IIFE用得少了,但在一些老代码和某些工具库封装里还是会见到。

2.2 闭包的形成机制与应用场景

闭包是JS中一个绕不开的概念。简单说:如果一个函数能访问其外部函数作用域中的变量,那么这个函数和它引用变量的组合就是闭包。

形成闭包有几个必要条件:函数嵌套函数、内部函数引用外部变量、内部函数被保留下引用。比如:

function counter() { let count = 0; return function() { count++; return count; }; } const c1 = counter(); const c2 = counter(); console.log(c1()); // 1 console.log(c1()); // 2 console.log(c2()); // 1,c1和c2各自独立的count

这里最有意思的细节是:每调用一次counter(),会创建一份全新的作用域,count不再共享。这也是为什么闭包常被用来做数据私有化。

闭包有个性能隐患,就是变量无法被垃圾回收,因为引用链一直存在。所以如果一个闭包长期挂在全局变量上,它捕获的对象会一直存活。我一般会建议,用完的闭包引用,置为null,帮GC一把。

2.3 作用域链与let/const/var的本质差异

作用域链的查找规则是:当前作用域找不到,就向上一级找,直到全局。全局再找不到,非严格模式就隐式创建全局变量(这会污染window),严格模式直接报错。

var是函数作用域,let和const是块级作用域。块级作用域包括if、for、while这些{}包裹的区域。

if (true) { var a = 1; let b = 2; } console.log(a); // 1 console.log(b); // ReferenceError: b is not defined

var还有个坑:在for循环里用var声明的循环变量会泄漏到循环外面,而且所有异步回调共享同一个变量。用let就没这个问题,因为每次迭代都会创建新的绑定。这是闭包经典面试题的本质解法。

2.4 this指向规则与箭头函数的例外

this的指向在普通函数里,由调用方式决定,有五种规则:

  1. 默认绑定:独立调用,非严格模式指向全局对象,严格模式是undefined
  2. 隐式绑定:作为对象方法调用时,指向这个对象
  3. 显式绑定:call、apply、bind指定this
  4. new绑定:构造函数里,this指向新创建的对象
  5. 绑定优先级:new > 显式 > 隐式 > 默认

箭头函数没有自己的this,它继承外层作用域的this。这个特性让它特别适合用在回调函数里,不用再写var self = this那套了。

const obj = { name: 'demo', init() { setTimeout(() => { console.log(this.name); // demo,箭头函数继承init的this }, 100); } };

但如果箭头函数在全局作用域定义,this就是全局对象(严格模式下也是全局对象)。这个要注意区分。

3. 运算符与流程控制:表达式计算的优先级陷阱

3.1 算术、比较、逻辑运算符的实际运用

算术运算符里,+最复杂,涉及字符串拼接和数字相加的判断。++和--有前后缀差异,前缀先自增再返回值,后缀先返回值再自增。这个在写循环或者计数器的时候容易出边界问题。

比较运算符有个经典场景:比较对象时,会先调用对象的valueOf()或toString()方法,再比较。所以两个对象不管内容是否一样,用==或===比较都是false,因为比较的是内存引用地址。

逻辑运算符返回的不一定是布尔值,它返回的是操作数的值。&&和||是短路运算:

const result = a || defaultValue; // 如果a是假值,返回defaultValue const value = obj && obj.name; // 如果obj是空,返回undefined,避免报错

??空值合并运算符是ES2020新增的,它只在左侧是null或undefined时返回右侧值,不会像||那样把0、空字符串也替换掉。这个区别很实用。

3.2 保留两位小数与运算符优先级

处理浮点数保留两位小数,我之前常被问。toFixed(2)是最简单的,但有个坑:它返回的是字符串,且四舍五入行为和预期可能不一致(底层是浮点数精度问题)。如果只是展示用还好,做计算还是得小心。

更稳妥的做法:

function roundToTwo(num) { return Math.round((num + Number.EPSILON) * 100) / 100; }

Number.EPSILON是用来修正浮点数误差的,加上它再四舍五入,结果更符合直觉。

运算符优先级方面,最容易出问题的是:&&优先级高于||,赋值运算符=优先级极低。还有三元运算符?:的嵌套可读性很差,我一般建议嵌套超过两层就改写为if分支。

表达式里的分号自动插入(ASI)机制也值得提一句。虽然JS引擎会自动补分号,但某些场景它的策略和你想的不一样。比如:

function demo() { return { ok: true }; } // 实际返回undefined,因为return后面自动补了分号

这种“return换行导致bug”的问题,我踩过好机会,后来习惯在return后把{放在同一行。

3.3 解构赋值与展开运算符的进阶用法

解构赋值真的是提升代码可读性的利器。数组解构支持默认值、跳过元素、剩余元素:

const [first, , third, ...rest] = [1, 2, 3, 4, 5]; console.log(first); // 1 console.log(third); // 3 console.log(rest); // [4, 5]

对象解构支持重命名和默认值:

const { name: username = 'default', age = 18 } = person;

展开运算符可以用来做浅拷贝:const copy = [...arr]、const objCopy = {...obj}。但记住,只是浅拷贝,嵌套对象还是共享引用。

有个小技巧:合并对象时,Object.assign({}, a, b)和{...a, ...b}在功能上等价,但后者更简洁。如果属性值是undefined,前者会忽略,后者也会忽略(在对象字面量展开时)。

4. 事件机制:从DOM事件流到委托与自定义事件

4.1 DOM事件流的三个阶段

事件在浏览器里从window出发,经过目标元素再返回window,形成三个阶段:捕获阶段、目标阶段、冒泡阶段。

addEventListener的第三个参数,如果传true或{ capture: true },就是在捕获阶段触发;默认是false,在冒泡阶段触发。

我实际工作中,大部分场景都在冒泡阶段处理事件,因为它更符合一般的交互直觉。但focus、blur等事件是不冒泡的,如果需要在捕获阶段处理,就要用addEventListener配合true。

4.2 事件委托的性能与正确姿势

事件委托核心原理是利用事件冒泡:把事件监听器挂到父元素上,通过event.target判断实际触发元素。这有两个好处:性能好(监听器少)、动态元素无需重复绑定。

document.querySelector('#list').addEventListener('click', (event) => { const target = event.target.closest('li'); if (!target) return; console.log(target.dataset.id); });

这里用closest('li')而不是target.tagName === 'LI',是为了处理用户点到li里面的span这类子元素的情况。closest会向上查找匹配选择器,更健壮。

4.3 Event对象常用属性与自定义事件

event对象里我经常用的属性:target(实际事件源)、currentTarget(当前绑定监听器的元素)、eventPhase(当前阶段)、preventDefault()(阻止默认行为)、stopPropagation()(阻止冒泡)、stopImmediatePropagation()(阻止冒泡且阻止同元素上其他监听器执行)。

自定义事件开发中很有用,尤其做组件通信:

const event = new CustomEvent('refresh', { detail: { id: 1 } }); element.dispatchEvent(event);

配合detail字段传数据,比全局事件总线干净得多。

4.4 浏览器事件循环:setTimeout与Promise的执行顺序

事件循环是理解异步代码执行顺序的钥匙。宏任务(macrotask)和微任务(microtask)是两个队列。每次执行完一个宏任务,都会清空微任务队列,再取下一个宏任务。

setTimeout(() => console.log('timeout'), 0); Promise.resolve().then(() => console.log('promise')); console.log('sync'); // 输出顺序:sync -> promise -> timeout

微任务包括Promise.then、MutationObserver、queueMicrotask。渲染和事件处理都发生在宏任务之间。所以如果你有数据变化的通知,放在微任务里,能保证在下次渲染前执行。

注意事项:嵌套的setTimeout层层加深度,可能触发浏览器的嵌套定时器最小间隔限制(5ms),但Promise的微任务没有这个限制,所以某些循环用微任务效率更高。

5. 异步编程:从回调地狱到Promise和async/await

5.1 Promise的三种状态与链式调用

Promise对象有三种状态:pending(进行中)、fulfilled(已完成)、rejected(已失败)。状态只能从pending转变到其他两个,一旦转变就不可逆。

链式调用的核心规矩:then或catch都会返回新的Promise,所以可以无限链下去。then回调里如果抛异常,会被下一个catch捕获;如果返回普通值,会被下一个then接收。

一个容易被忽略的点:如果then回调里返回一个Promise,下一个then会等这个Promise resolve后再执行,相当于展平了一层异步嵌套。

5.2 async/await错误处理的关键技巧

async函数其实就是生成器加Promise的语法糖。await会暂停函数执行,等待Promise settled后再继续。它让异步代码看起来像同步代码,但要注意并发问题。

我见过很多项目在请求数据时,多个相互独立的请求用了串联的await,白白浪费了大量等待时间。正确做法是用Promise.all:

const [users, posts] = await Promise.all([ fetch('/users'), fetch('/posts') ]);

错误处理方面,try/catch配合await是基本操作。但还有个技巧:await一个被rejected的Promise,异常会被抛出,如果没有try/catch包裹,会变成unhandledrejection,可能静默失败。所以规范做法是每个await路径都要有兜底,或者在函数内部统一catch后转成正常返回值。

5.3 并发控制与Promise.allSettled

Promise.all有个缺陷:只要有一个rejected,整个Promise直接变成rejected,其他结果都拿不到。如果某个请求失败,你就全丢了。这时候用Promise.allSettled更适合:

const results = await Promise.allSettled([p1, p2, p3]); // results里每项是 {status: 'fulfilled', value} 或 {status: 'rejected', reason}

还有Promise.race,只取最快的那个结果,常用于超时控制:

const withTimeout = Promise.race([ fetch('/data'), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)) ]);

注意timer要clearTimeout清理,不然定时器还会触发,只是reject被忽略了而已。

6. 运行时报错排查与console调试技巧

6.1 常见运行时错误分类

前端错误大致分几类:

  • 语法错误:代码无法解析,整个脚本都不会执行
  • 引用错误(ReferenceError):变量未定义或不在作用域
  • 类型错误(TypeError):访问了undefined的属性、调用了不是函数的东西
  • 范围错误(RangeError):数组长度非法、栈溢出
  • 自定义错误类,可以用throw new Error('xxx')主动抛出

有一个高频错误:Cannot read properties of undefined (reading 'xxx')。排查思路是找到哪个对象是undefined,往上回溯它的赋值来源。我会在怀疑点用console.log或debugger打断点,逐步确认。

6.2 console全家桶的实用姿势

console.log就不说了。其他几个很有用:

  • console.log('%c...', 'color:#f00;font-size:20px'):带颜色输出,排查分组信息很方便
  • console.table(arr):把数组或对象以表格形式打印,看结构化数据一目了然
  • console.time/console.timeEnd:测代码块执行时间
  • console.trace():打印调用栈,比log更强大,能查到函数是哪里被调用的
  • console.assert(false, 'msg'):条件为false时打印消息,适合在循环里做断言

6.3 浏览器开发者工具Sources断点调试

用断点调试比console.log效率高很多。在Sources面板里点击行号即可打断点。然后重点关注几个面板:

  • Scope面板:查看当前作用域变量、闭包变量
  • Call Stack面板:查看完整调用栈,有助于定位调用源头
  • Watch面板:添加你关注的表达式,实时变化一目了然

条件断点很实用,比如想要在count大于5时才断住,右键断点编辑条件count > 5即可,省去大量手动跳过干扰步骤。

7. 常用开发工具与浏览器API扩展

7.1 严格模式与ESLint的必要性

文件顶部写'use strict';或者通过ESM模块天然启用严格模式后,JS会避免一些静默错误:未声明变量直接赋值会报错、this在全局不再指向window、delete不可删除属性会报错。

配合ESLint,建议规则集:equeeq禁用==、no-var禁用var、no-unused-vars禁未使用变量、prefer-const优先const。这些规则能拦截很多低级错误。

7.2 JSON序列化与深拷贝的实现

处理JSON字符串时有个经典坑:JSON.stringify遇到undefined、函数、symbol值,会直接跳过这些属性。NaN和Infinity会变成null。所以不能把JSON当普通对象的通用序列化方式。

深拷贝除了引库或JSON.parse(JSON.stringify(obj))外(有局限,不支持Date、RegExp、循环引用),可以用structuredClone(支持循环引用和更多类型)。它是浏览器原生API,能在window和worker之间传递数据,对性能要求高的场景可以开transfer列表。

7.3 浏览器存储方案对比:cookies、localStorage与IndexedDB

本地存储常用的三种:

特性localStoragesessionStoragecookie
大小约5MB约5MB约4KB
生命周期永久,除非清除标签页关闭即清除根据expires/Max-Age
请求自动携带否否是(同域)
数据格式字符串字符串字符串

cookie会自动附加到请求头,所以尽量只存会话标识,不要存业务数据。localStorage是同步的,写入大量数据时可能阻塞主线程。IndexedDB是异步的,适合存储结构化大对象。

读取localStorage有个注意点:键不存在时返回null,读取的结果要用JSON.parse并包一层try/catch,因为一旦数据损坏或被篡改,解构会直接抛异常,应用直接崩溃。

8. 浏览器兼容性实践:从差异化封装到构建降级

8.1 兼容性判断基础

判断一个API能不能用,标准做法是查Can I Use,或直接看MDN的浏览器兼容性表。但日常开发时,工具链的处理可能更高效:

  • Babel把ES6+语法转成ES5,但要明白:语法能转,API问题它管不了。Promise、Array.from这些需要polyfill
  • 打包工具会把代码转换成目标浏览器能跑的样子,但targets配置对了才有效

8.2 特性检测代替浏览器检测

一个正确的思路是“特性检测”而非“浏览器版本检测”。比如判断是否支持IntersectionObserver做懒加载:

if ('IntersectionObserver' in window) { // 新浏览器 } else { // 老浏览器 }

不推荐用navigator.userAgent判断,因为字符串可以被篡改,而且新版本浏览器特征一直在变。用特性检测写出来的代码天然就更面向未来。

9. Canvas与基础算法:从图形绘制到性能调优

9.1 Canvas基础绘制流程

Canvas的起点很简单:获取2D上下文,然后开始绘制。

const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#3498db'; ctx.fillRect(50, 50, 100, 100);

但有个细节坑:canvas的宽高属性和CSS宽高不一样。如果只想用CSS控制显示大小,必须同时设置canvas.width和height与CSS一致,否则会出现画布内容模糊或被拉伸问题。

绘制路径:beginPath、moveTo、lineTo、closePath、stroke或fill。beginPath之后别忘了每次重绘时清屏,最常见的动画污染问题是没调用clearRect,导致上一帧的图像残留。

用requestAnimationFrame做动画,它比setInterval更平滑,且能跟随显示刷新率。同时注意:requestAnimationFrame在每个宏任务里只会执行一次,如果在里面又注册一帧,还能继续下一帧循环。

9.2 高频操作的数据结构与算法基础

写交互组件时,数据结构的选择直接影响性能。常见场景:

  • 大量列表筛选:用filter遍历没问题,但如果超过万级,用Set或Map做索引更快
  • 防抖与节流:防抖是等用户停止输入再执行;节流是固定时间间隔执行一次。这两种都是性能优化里非常基础的模式
function debounce(fn, wait) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), wait); }; }
  • 深比较:如果对象层级复杂,没必要手写,用JSON.stringify比较粗略可以,但属性顺序不同会导致误判。更严谨的是递归比较每个属性。

9.3 跨平台开发中的JavaScript运行环境差异

JavaScript不只跑在浏览器里。Node.js环境里没有window、document,而是有process、Buffer。有些桌面开发框架中,渲染进程和主进程之间通信也和浏览器不同。

还有一个“运行环境差异”的典型场景:Object的遍历顺序。整数键按升序遍历,字符串键按插入顺序遍历,symbol键最后。这套规则在Node和浏览器里一致,但跨引擎如果实现差异,就容易出bug。

遇到跨环境代码,建议每个环境都做一遍冒烟测试,尤其注意typeof window === 'undefined'的判断,用来区分浏览器和Node。

10. 框架与工具链视角:如何反哺基础语法

10.1 框架外的JavaScript,依然要掌握的核心API

现在很多开发者一上来就学框架,忽略了原生能力。但无论什么框架,底层都依赖这些API:

  • Object.keys/Object.values/Object.entries:遍历对象的三种方式
  • Array.prototype.map/filter/reduce/forEach:函数式操作数据
  • String.prototype.includes/startsWith/padStart:字符串处理
  • URLSearchParams:读写URL参数
  • FormData:处理表单提交

这些API是框架的基石,也是面试里逃不过的重点。刷一遍文档,再结合小项目用一用,基本上就能融会贯通。

10.2 组件化开发中如何利用基础语法优化状态管理

状态管理无论用什么库,本质还是JavaScript对象和数组的变更检测。有些框架依赖数据劫持(Vue的reactive),有些靠不可变数据(React的setState)。

如果你用React,理解不可变更新很重要:不能直接修改state对象里的属性,要返回新对象。这里用展开运算符就比深拷贝高效:

setState(prev => ({ ...prev, field: 'newValue' }));

如果你用Vue,要小心响应式丢失:用索引直接修改数组项、添加新的对象属性,可能不会被侦测到。正确姿势是用Vue.set或$set,或者干脆整体替换。

10.3 框架时代为什么还要夯实基础

我经常和新同事说:框架能帮你解决80%的重复劳动,但剩下的20%——性能优化、复杂交互、内核级调试——必需原生JavaScript的功底。

很多框架的源码其实都是原生语法的高级运用:比如Vue的Object.defineProperty和Proxy的配合、React的链表结构调度。你如果连Object.defineProperty的特性都不清楚,读源码就像在看天书。

另外,无论是npm install的依赖升级,还是底层API的兼容降级,都有可能需要手动处理原生暴露出来的差异。基础语法越扎实,处理这些边界情况时就越从容。

写在最后的一些体会

回头再看这份笔记,我删掉了很多教科书式的赘述,尽量把每个知识点都和我实际写代码时踩过的坑挂钩。尤其是数据类型判断、事件委托、异步并发、Canvas宽度设置、Object.defineProperty响应式这些,几乎每个月都会在项目里遇到相关的问题。基础语法从来不是背出来的,是在写代码、出bug、修bug的过程中慢慢内化的。如果这份笔记能帮你少走几个弯路,或者在工作里理清某个模糊点,我就觉得值了。JS这条路很长,保持好奇,多敲多练,才是唯一的捷径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 8:08:25

插件加载失败全解析:从机制原理到web boot报错排查实战

“plugins”,这个词只要碰过软件开发就绕不开。最近一周,我至少被三个人问到了跟它直接相关的问题:有人问 IAR 里的插件到底是干什么的,有人问 MusicFree 的插件包怎么配,还有人直接把控制台摔给我看——“failed to l…

作者头像 李华
网站建设 2026/10/5 8:07:22

ThingsBoard集成TDengine:从规则引擎直写到Kafka管道实践

你有没有遇到过这种局面:ThingsBoard控制台上设备数据刷得飞快,但查询历史曲线时页面转圈,数据库磁盘三天涨了一大截,PostgreSQL的CPU常年飘在70%以上。我最初接ThingsBoard的时候,觉得它自带的那套存储方案够用&#…

作者头像 李华
网站建设 2026/10/5 8:07:12

OpenShell实战:用自然语言生成Shell命令的AI终端部署与安全配置

1. 为什么我又折腾了一个AI辅助终端那天凌晨两点,我在处理一堆跨了三个月的Nginx访问日志,想把所有 4xx 和 5xx 状态码的请求按来源IP聚合统计,然后再把7天前的压缩文件归档到冷存储目录。命令本身不复杂,但涉及awk字段切割、sort…

作者头像 李华
网站建设 2026/10/5 8:06:13

Python并发编程核心:GIL、多线程、asyncio与多进程选型实战

1. 并发与并行:先搞清楚你面对的到底是哪个问题聊Python并发,十个有九个半会先撞上GIL这堵墙。但很多新手还没走到GIL那一步,就已经把"并发"和"并行"两个词混着用了。先说人话版本:并发是多个任务在同一个时间…

作者头像 李华
网站建设 2026/10/5 8:05:02

AWS上FortiGate HA高可用配置实战:FGCP与SDN Connector实现秒级切换

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

作者头像 李华
网站建设 2026/10/5 8:04:42

SpringBoot+Vue宠物商城项目全解析:从架构到部署

先说一句大实话:SpringBoot Vue 这类商城项目,放到 GitHub 上一抓一大把,但绝大多数都是“能跑就行”的半成品——代码乱、没注释、表结构随意、前端页面粗制滥造。真正适合拿去当毕设、课设,或者静下心来学一遍的,反…

作者头像 李华