# Bài 02 — JavaScript cần thiết cho dự án
⬅️ Bài trước | Mục lục | Bài tiếp theo ➡️
🎯 Mục tiêu
Bài này không dạy JavaScript từ A–Z. Nó dạy đúng những cú pháp xuất hiện trong dự án — để khi đọc code ở bài 04 trở đi, bạn không bị "khựng" ở dòng nào.
Mọi ví dụ dưới đây đều trích từ code thật của dự án.
💡 Cách học bài này: mở terminal, gõ
nodeđể vào chế độ tương tác, rồi gõ thử từng đoạn code. Học bằng tay nhớ lâu hơn học bằng mắt.
1. var, let, const — khai báo biến
var x = 1; // Kiểu cũ, tránh dùng
let y = 2; // Có thể gán lại
const z = 3; // Không gán lại được
Quy tắc thực hành: mặc định dùng const. Chỉ đổi sang let khi thật sự cần gán lại.
Trong dự án bạn sẽ thấy cả var (ở file do express-generator sinh ra) lẫn const:
// backend/routes/products.js — dòng đầu, do generator sinh, dùng var
var express = require("express");
var router = express.Router();
// Code do người viết thêm, dùng const
const connectDb = require("../model/db");
const { ObjectId } = require("mongodb");
⚠️
constvới object/mảng chỉ khóa biến, không khóa nội dung:const cart = []; cart.push("A"); // ✅ Được — thay đổi nội dung cart = ["B"]; // ❌ Lỗi — gán lại biến
2. Arrow function — hàm mũi tên
Ba cách viết dưới đây tương đương nhau:
// Cách cũ
function cong(a, b) {
return a + b;
}
// Arrow function đầy đủ
const cong = (a, b) => {
return a + b;
};
// Arrow function rút gọn — 1 biểu thức thì tự return
const cong = (a, b) => a + b;
Xuất hiện trong dự án ở đâu?
// frontend/app/(user)/page.js — hàm fetcher của SWR
const fetcher = (...agrs) => fetch(...agrs).then((res) => res.json());
Đọc từng phần:
(...agrs)— nhận bao nhiêu tham số cũng được, gom thành mảngagrs(xem mục 5)=> fetch(...)— không có{}nên tự độngreturnkết quả củafetch(...).then((res) => res.json())— khi fetch xong, lấyresrồi trả vềres.json()
Trả về object phải bọc trong ngoặc tròn
const taoUser = (ten) => ({ name: ten }); // ✅ Đúng
const taoUser = (ten) => { name: ten }; // ❌ Sai — JS hiểu {} là thân hàm
3. Template literal — chuỗi có nhúng biến
Dùng dấu backtick ` (phím bên trái số 1) thay vì nháy đơn/kép:
const ten = "Gucci";
const gia = 5050000;
// Cách cũ — nối chuỗi
const s1 = "Sản phẩm " + ten + " giá " + gia + "đ";
// Template literal — dễ đọc hơn nhiều
const s2 = `Sản phẩm ${ten} giá ${gia}đ`;
Trong dự án
// URL API được ghép từ biến môi trường
useSWR(`${process.env.NEXT_PUBLIC_API_URL}/products`, fetcher)
// URL ảnh
<img src={`${process.env.NEXT_PUBLIC_IMA_URL}${product.image}`} />
// Class CSS có điều kiện
<Link className={`nav-link ${patname == "/" ? "active" : " "}`} href="/">
Cái cuối kết hợp cả toán tử 3 ngôi (mục 8) — rất hay gặp trong React.
4. Destructuring — bóc tách object và mảng
Đây là cú pháp bạn sẽ gặp nhiều nhất trong dự án.
Với object
const product = { name: "Gucci Flora", price: 5050000, rating: 4.5 };
// Cách cũ
const name = product.name;
const price = product.price;
// Destructuring — ngắn hơn nhiều
const { name, price } = product;
Đổi tên khi bóc tách
const { name: tenSanPham, price: giaBan } = product;
console.log(tenSanPham); // "Gucci Flora"
Trong dự án, cú pháp này dùng để đặt tên rõ ràng khi gọi SWR nhiều lần trong một component:
// frontend/app/(user)/menu/layout.js
const {
data: category, // đổi `data` thành `category`
error: errorcategory, // đổi `error` thành `errorcategory`
isLoading: isLoadingcategory
} = useSWR(`${process.env.NEXT_PUBLIC_API_URL}/categories`, fetcher);
Nếu không đổi tên, gọi SWR lần thứ hai trong cùng component sẽ bị trùng biến data.
Bóc tách ngay ở tham số hàm
// frontend/app/(user)/component/addcardbuton.js
export default function AddCardbutton(props) {
const { product, quantity, size, className } = props;
// ...
}
// Hoặc gọn hơn — bóc ngay tại tham số:
export default function AddCardbutton({ product, quantity, size, className }) {
// ...
}
// frontend/app/(user)/product/[id]/page.js
export default function Productdetail({ params }) {
// params.id chính là phần [id] trên URL
}
Bóc tách khi import
const { ObjectId } = require("mongodb");
// tương đương: const ObjectId = require("mongodb").ObjectId;
Với mảng
const [min, max] = [0, 100];
console.log(min); // 0
useState của React trả về một mảng 2 phần tử, nên ta bóc tách như sau:
const [quantity, setquantity] = useState(1);
// ↑giá trị ↑hàm cập nhật
5. Spread & Rest — dấu ba chấm ...
Cùng ký hiệu ... nhưng có 2 công dụng ngược nhau.
Spread — "trải" ra
const a = [1, 2, 3];
const b = [...a, 4]; // [1, 2, 3, 4] — sao chép mảng a rồi thêm 4
const p = { name: "Gucci", price: 100 };
const q = { ...p, quantity: 2 }; // { name: "Gucci", price: 100, quantity: 2 }
Trong dự án:
// redux/slices/cartSlicer.js — thêm quantity & size vào bản sao sản phẩm
cart.push({ ...product, quantity, size });
// component/productlist.js — sao chép mảng trước khi lưu
let defaultData = useRef([...props.data]);
💡
[...a]tạo ra mảng mới, khác vớia. Đây gọi là "shallow copy" — rất quan trọng trong React và Redux, vì React chỉ render lại khi thấy tham chiếu thay đổi.
Rest — "gom" lại
function tong(...cacSo) {
return cacSo.reduce((t, n) => t + n, 0);
}
tong(1, 2, 3); // 6 — cacSo = [1, 2, 3]
Trong dự án:
const fetcher = (...agrs) => fetch(...agrs).then((res) => res.json());
// ↑rest: gom mọi tham số ↑spread: trải ra để truyền cho fetch
Viết vậy để fetcher truyền lại y nguyên mọi tham số mà SWR đưa vào cho fetch.
6. Các phương thức mảng quan trọng
Đây là "vũ khí chính" của bạn khi làm việc với danh sách sản phẩm.
Dữ liệu mẫu dùng chung cho các ví dụ:
const products = [
{ name: "Gucci Flora", price: 5050000, rating: 4.5 },
{ name: "Scandal", price: 4850000, rating: 4.5 },
{ name: "Versace Eros", price: 3480000, rating: 4.0 },
];
.map() — biến đổi từng phần tử, trả mảng mới cùng độ dài
const ten = products.map((p) => p.name);
// ["Gucci Flora", "Scandal", "Versace Eros"]
Trong React, .map() dùng để biến mảng dữ liệu thành mảng JSX:
// frontend/app/(user)/component/productlist.js
{data.map((item) => (
<div key={item._id} className="col">
<Product data={item} />
</div>
))}
⚠️ Luôn có
keykhi map ra JSX. React dùngkeyđể biết phần tử nào thay đổi. Thiếukey→ cảnh báo trong console và có thể render sai.
.filter() — giữ lại phần tử thỏa điều kiện, trả mảng ngắn hơn hoặc bằng
const daT = products.filter((p) => p.price > 4000000);
// [Gucci Flora, Scandal]
Trong dự án — lọc theo khoảng giá:
// frontend/app/(user)/component/productlist.js
data = data.filter((item) => min <= item.price && item.price <= max);
.find() — tìm phần tử đầu tiên thỏa điều kiện, trả về 1 phần tử (hoặc undefined)
const sp = products.find((p) => p.name === "Scandal");
// { name: "Scandal", price: 4850000, rating: 4.5 }
Trong dự án — ghép danh mục vào sản phẩm:
// backend/routes/products.js
const category = categories.find(
(cat) => cat._id.toString() === item.categoryId.toString()
);
item.category = category;
.findIndex() — trả về vị trí (số), hoặc -1 nếu không có
// redux/slices/cartSlicer.js
const index = cart.findIndex(
(item) => item._id == product._id && item.size == size
);
if (index == -1) {
cart.push({ ...product, quantity, size }); // chưa có → thêm mới
} else {
cart[index].quantity += Number(quantity); // đã có → cộng dồn
}
Đây chính là logic "thêm vào giỏ hàng" của dự án. Đọc kỹ đoạn này!
.reduce() — gom cả mảng thành một giá trị
Cú pháp: arr.reduce((tichLuy, phanTuHienTai) => giaTriMoi, giaTriBanDau)
const tongTien = products.reduce((tong, p) => tong + p.price, 0);
// 0 + 5050000 → 5050000 + 4850000 → 9900000 + 3480000 → 13380000
Trong dự án — 2 chỗ dùng reduce:
// component/navbar.js — đếm tổng số món trong giỏ
const totalitem = cart.reduce((total, item) => total + item.quantity, 0);
// cart/page.js — tính tổng tiền
const total = cart.reduce((total, item) => total + item.price * item.quantity, 0);
.sort() — sắp xếp
const tangDan = [...products].sort((a, b) => a.price - b.price);
const giamDan = [...products].sort((a, b) => b.price - a.price);
Quy tắc hàm so sánh: trả về số âm thì a đứng trước, số dương thì b đứng trước.
⚠️
.sort()thay đổi mảng gốc! Đây là bug thật trongproductlist.jscủa dự án — nó gọidata.sort()trực tiếp trên mảng đến từ props. Bài 15 sẽ mổ xẻ và sửa. Cách an toàn:[...data].sort(...).
.splice() — xóa/chèn tại vị trí, thay đổi mảng gốc
const arr = ["a", "b", "c"];
arr.splice(1, 1); // xóa 1 phần tử từ vị trí 1
console.log(arr); // ["a", "c"]
// redux/slices/cartSlicer.js — xóa sản phẩm khỏi giỏ
const index = cart.findIndex((item) => item._id == product._id && item.size == size);
cart.splice(index, 1);
Bảng so sánh nhanh
| Phương thức | Trả về | Đổi mảng gốc? |
|---|---|---|
.map() |
Mảng mới cùng độ dài | ❌ |
.filter() |
Mảng mới ngắn hơn | ❌ |
.find() |
1 phần tử / undefined |
❌ |
.findIndex() |
Số / -1 |
❌ |
.reduce() |
1 giá trị bất kỳ | ❌ |
.sort() |
Chính mảng đó | ✅ Có |
.splice() |
Mảng các phần tử bị xóa | ✅ Có |
.push() |
Độ dài mới | ✅ Có |
7. == vs ===
1 == "1" // true — so sánh lỏng, tự ép kiểu
1 === "1" // false — so sánh chặt, khác kiểu là false
Quy tắc: luôn dùng ===, trừ khi bạn cố ý muốn ép kiểu.
Trong dự án, code gốc dùng == khá nhiều:
// redux/slices/cartSlicer.js
const index = cart.findIndex((item) => item._id == product._id && item.size == size);
Ở đây == may mắn vẫn đúng vì _id đều là chuỗi. Nhưng viết === sẽ an toàn hơn.
Có một chỗ == là cần thiết: khi so sánh số đến từ input HTML.
Input luôn trả về chuỗi, nên "5" === 5 là false. Cách đúng là ép kiểu tường minh:
Number(e.target.value) // ✅ Rõ ràng
Dự án đã làm đúng ở chỗ này:
cart[index].quantity = Number(cart[index].quantity) + Number(quantity);
8. Toán tử 3 ngôi và toán tử logic
Toán tử 3 ngôi ? :
const ketQua = dieuKien ? "khi đúng" : "khi sai";
Trong dự án — dùng để đổi class CSS theo trang hiện tại:
// component/navbar.js
<Link className={`nav-link ${patname == "/" ? "active" : " "}`} href="/">
// admin/component/leftbar.js
<Link className={`nav-link rounded-0 ${pathname == "/admin" ? "active" : "text-white"}`}>
Ba ngôi lồng nhau
// cart/page.js — tính đơn giá theo size
product.size === 'l'
? (product.price * product.quantity + (product.quantity * 10000)).toLocaleString()
: product.size === 'm'
? (product.price * product.quantity + 15000).toLocaleString()
: (product.price * product.quantity).toLocaleString()
⚠️ Đoạn này khó đọc và có lỗi logic: size
lcộng phụ phí theo số lượng, nhưng sizemchỉ cộng một lần cố định. Bài 16 sẽ viết lại cho rõ ràng và nhất quán.
&& — render có điều kiện
// component/product.js
{product.rating > 0 && (
<small className="text-warning fw-bold me-1">
{product.rating}<i className="fa-solid fa-star"></i>
</small>
)}
Đọc là: "nếu rating > 0 thì render phần JSX phía sau, ngược lại không render gì".
⚠️ Bẫy kinh điển:
{soLuong && <p>...</p>}— nếusoLuong = 0, React sẽ in ra chữ0trên màn hình! Vì0là giá trị falsy nhưng vẫn render được. Cách sửa:{soLuong > 0 && <p>...</p>}.
9. Bất đồng bộ: Promise và async/await
Đây là khái niệm quan trọng nhất trong bài này.
Vấn đề
Gọi database hay gọi API mất thời gian (vài mili giây đến vài giây). JavaScript không đứng chờ — nó chạy tiếp dòng dưới. Nên đoạn code sau không hoạt động:
const products = db.collection("products").find().toArray();
console.log(products); // ❌ In ra: Promise { <pending> } — chưa có dữ liệu!
Promise là gì?
Promise là "lời hứa sẽ có kết quả sau". Nó có 3 trạng thái:
pending— đang chờfulfilled— thành công, có dữ liệurejected— thất bại, có lỗi
Cách 1 — .then()
fetch("http://localhost:5000/products")
.then((res) => res.json()) // res chưa phải JSON, phải parse
.then((data) => console.log(data))
.catch((err) => console.log("Lỗi:", err));
Dự án dùng cách này ở fetcher và ở nút đặt hàng:
// cart/page.js
fetch(`${process.env.NEXT_PUBLIC_API_URL}/orders`, { method: 'POST', /* ... */ })
.then(res => {
alert("đặt hàng thành công");
dispatch(removeCart());
router.push("/");
})
Cách 2 — async/await (dễ đọc hơn nhiều)
async function layProducts() {
const res = await fetch("http://localhost:5000/products");
const data = await res.json();
console.log(data); // ✅ Có dữ liệu thật
}
Hai quy tắc:
awaitchỉ dùng được bên trong hàm cóasyncawaitlàm code "dừng lại chờ" cho đến khi Promise xong
Toàn bộ backend của dự án viết theo kiểu này:
// backend/routes/products.js
router.get("/", async (req, res, next) => {
const db = await connectDb(); // chờ kết nối DB
const productCollection = db.collection("products");
const products = await productCollection.find().toArray(); // chờ query
res.status(200).json(products);
});
Bắt lỗi với try/catch
router.get('/products/:id', async (req, res, next) => {
try {
const db = await connectDb();
const product = await db.collection('products').findOne({ _id: new ObjectId(id) });
res.status(200).json(product);
} catch (err) {
next(err); // chuyển lỗi cho Express xử lý
}
});
⚠️ Hầu hết route trong dự án gốc KHÔNG có
try/catch. Hậu quả: nếu client gửiidsai định dạng (ví dụ/products/id/abc),new ObjectId("abc")sẽ ném lỗi, Express không bắt được và request bị treo vô hạn. Bài 06 sẽ chỉ cách sửa.
10. Module — chia code ra nhiều file
Dự án dùng 2 hệ thống module khác nhau. Đừng nhầm lẫn.
CommonJS — dùng ở backend (Node.js)
// backend/model/db.js — xuất ra
module.exports = connectDb;
// backend/routes/products.js — nhập vào
const connectDb = require("../model/db");
const { ObjectId } = require("mongodb");
ES Modules — dùng ở frontend (Next.js)
// redux/slices/cartSlicer.js — xuất ra
export const { addItem, removeItem } = cartSlice.actions; // named export
export default cartSlice.reducer; // default export
// Nhập vào
import cartSlicer from "./slices/cartSlicer"; // default
import { addItem } from "@/redux/slices/cartSlicer"; // named
import cartSlicer, { addItem } from "./slices/cartSlicer"; // cả hai
| Default export | Named export | |
|---|---|---|
| Số lượng mỗi file | Tối đa 1 | Bao nhiêu cũng được |
| Cú pháp xuất | export default X |
export const X |
| Cú pháp nhập | import X from "..." |
import { X } from "..." |
| Đổi tên khi nhập | Tự do | Phải dùng as |
💡 Có một chỗ trong dự án trộn lẫn 2 kiểu:
// redux/slices/cartSlicer.js const { createSlice } = require("@reduxjs/toolkit"); // CommonJS export const cartSlice = createSlice({ ... }); // ES ModuleNext.js xử lý được nên vẫn chạy, nhưng không nên viết vậy. Nên dùng nhất quán:
import { createSlice } from "@reduxjs/toolkit";
Alias @/ trong Next.js
// frontend/jsconfig.json
{
"compilerOptions": {
"paths": { "@/*": ["./*"] }
}
}
Nhờ cấu hình này, @/redux/store = <thư mục frontend>/redux/store.
Bạn không cần đếm ../../../ nữa.
11. Vài hàm tiện ích hay dùng
.toLocaleString() — định dạng số có dấu phân cách
(5050000).toLocaleString(); // "5,050,000"
(5050000).toLocaleString("vi-VN"); // "5.050.000"
Dự án dùng ở nhiều chỗ để hiển thị giá:
<div className="display-1">{product.price.toLocaleString()}đ</div>
⚠️ Có lỗi chính tả trong
admin/component/recentOrder.js: viếttoLOcaString()(thiếu chữle, sai hoa thường) → sẽ crash nếu component này được dùng. May mà nó đang bị comment lại trongadmin/page.js.
Array(n) + spread — tạo mảng để lặp n lần
// product/[id]/page.js — vẽ số sao đầy
{[...Array(Math.floor(product.rating))].map((i) => (
<i className="fa-solid fa-star"></i>
))}
// Vẽ số sao rỗng còn lại
{[...Array(5 - Math.floor(product.rating))].map((i) => (
<i className="fa-regular fa-star"></i>
))}
Giải thích: Array(4) tạo mảng 4 ô rỗng — không .map() được.
Phải [...Array(4)] để biến thành [undefined, undefined, undefined, undefined] rồi mới map.
Math.max / Math.min — chặn giá trị
// redux/slices/cartSlicer.js — không cho số lượng nhỏ hơn 1
cart[index].quantity = Math.max(1, quantity);
JSON.stringify / JSON.parse
JSON.stringify({ a: 1 }); // '{"a":1}' — object → chuỗi
JSON.parse('{"a":1}'); // { a: 1 } — chuỗi → object
Khi gửi dữ liệu lên server phải chuyển thành chuỗi:
body: JSON.stringify({ user: { fullname, phone, address }, detail: cart })
Object shorthand — viết tắt khi key trùng tên biến
const fullname = "An";
const phone = "0901";
const user = { fullname: fullname, phone: phone }; // dài dòng
const user = { fullname, phone }; // ✅ tương đương, ngắn gọn
Dùng trong cart/page.js:
body: JSON.stringify({
user: { fullname, phone, address }, // shorthand
detail: cart,
total_money: total,
})
✅ Kiểm tra hiểu bài
Tự viết ra kết quả trước khi chạy thử:
const sp = [
{ ten: "A", gia: 100, sl: 2 },
{ ten: "B", gia: 200, sl: 1 },
{ ten: "C", gia: 50, sl: 3 },
];
// 1. Tổng tiền toàn bộ?
// 2. Mảng tên các sản phẩm có giá > 80?
// 3. Sản phẩm nào có tên "B"?
// 4. Vị trí của sản phẩm "C"?
// 5. Sắp xếp theo giá tăng dần mà KHÔNG làm đổi mảng gốc?
<details>
<summary>Đáp án</summary>
// 1
const tong = sp.reduce((t, p) => t + p.gia * p.sl, 0); // 100*2 + 200*1 + 50*3 = 550
// 2
const ten = sp.filter((p) => p.gia > 80).map((p) => p.ten); // ["A", "B"]
// 3
const b = sp.find((p) => p.ten === "B"); // { ten: "B", gia: 200, sl: 1 }
// 4
const i = sp.findIndex((p) => p.ten === "C"); // 2
// 5
const tangDan = [...sp].sort((a, b) => a.gia - b.gia); // C, A, B
</details>
📝 Bài tập
-
Viết hàm
async function layDanhMuc()gọihttps://jsonplaceholder.typicode.com/usersvà in ra tên tất cả user. Làm cả 2 cách:.then()vàasync/await. -
Cho
const cart = [{_id:"1", size:"s", quantity:2}, {_id:"1", size:"m", quantity:1}]. Viết hàm thêm sản phẩm{_id:"1", size:"s"}vớiquantity = 3sao cho kết quả làquantitycủa phần tử đầu thành5(không thêm phần tử mới). -
Giải thích tại sao đoạn này in ra
Promise { <pending> }:function layData() { return fetch("...").then(r => r.json()); } console.log(layData());Và sửa lại cho đúng.
All rights reserved