购物车这玩意儿,将商看着简单,品加真动手写起来坑不少。购物我上周帮朋友改一个电商页面,将商他那个加购逻辑写得乱七八糟——点一下按钮,品加数字跳两下,购物再点又没反应。将商后来发现是品加状态没同步,DOM更新和实际数据各玩各的购物。所以这篇直接聊怎么用JS把商品塞进购物车,将商从数据到界面,品加一条线走通。购物
先定数据结构。将商别一上来就想着往数组里push一个对象完事,品加后面改数量、购物删条目、算总价的时候会哭。我习惯用对象存,键是商品ID,值是商品信息加数量。这样查找是O(1),更新数量不用遍历整个数组。
let cart = { "sku_001": { name: "机械键盘", price: 399, qty: 1 }, "sku_007": { name: "无线鼠标", price: 129, qty: 2 } }; 为什么不用数组?数组找某个商品得find一遍,商品多了性能就下来。对象直接cart[id]拿到,干净。当然你要用Map也行,看个人习惯。
接下来是加购函数。核心逻辑就三行:判断商品在不在车里,在就加数量,不在就新建条目。但实际写的时候得考虑库存、上限这些。我一般会传一个完整的商品对象进来,而不是只传ID再去查——查一次数据库或接口的代价比多传几个字段大得多。
function addToCart(product) { const { id, name, price, stock } = product; if (cart[id]) { if (cart[id].qty >= stock) { alert("库存不够了"); return; } cart[id].qty += 1; } else { cart[id] = { name, price, qty: 1 }; } renderCart(); } 注意这里renderCart()是必须的。很多人写加购只改数据不刷界面,用户点完按钮没反应,以为坏了。渲染函数负责把cart对象翻译成页面上的列表和角标数字。
渲染这块我踩过坑。早期我用innerHTML拼字符串,每次加购整个购物车区域重绘一遍。商品少没事,商品一多,输入框焦点丢了,滚动位置也跳。后来改成只更新变化的部分——角标数字单独一个span,列表项用data-id标记,加购时只改对应那一条的qty文本。
function renderCart() { let total = 0; let count = 0; const list = document.getElementById("cart-list"); list.innerHTML = ""; for (let id in cart) { const item = cart[id]; total += item.price * item.qty; count += item.qty; const li = document.createElement("li"); li.dataset.id = id; li.innerHTML = `${ item.name} × ${ item.qty} `; list.appendChild(li); } document.getElementById("badge").textContent = count; document.getElementById("total").textContent = total.toFixed(2); } 减数量、删条目同理。减到0就从cart里delete掉,别留个qty: 0的僵尸条目,不然算总价还得过滤一遍。
有个细节容易忽略:加购按钮的防抖。用户手快连点五下,库存直接扣五件。我一般会在按钮上加一个disabled状态,请求返回前不让再点。如果是纯前端操作,至少加个200毫秒的节流。
let adding = false; btn.addEventListener("click", async () => { if (adding) return; adding = true; btn.disabled = true; await addToCart(product); adding = false; btn.disabled = false; }); 最后说持久化。页面一刷新购物车空了,用户会骂人。localStorage存一份是最低成本的方案。初始化时读出来,每次renderCart前写回去。注意存的时候JSON.stringify,读的时候JSON.parse,别忘了try-catch——用户手动改过localStorage的话,解析会炸。
整套下来不到一百行,但每个环节都有小坑。数据结构和渲染策略选对了,后面加优惠券、算运费、同步后端都只是往上叠的事。选错了,改起来就是重写。