Câu 1Chạy localStorage.getItem("theme") với một key chưa từng được lưu thì sao?
localStorage — lưu dữ liệu trong trình duyệt
Lưu giá trị trong trình duyệt bằng setItem, getItem, removeItem, lưu mảng dưới dạng chuỗi JSON, xử lý null và giới hạn dung lượng.
Nếu bạn giữ sản phẩm trong giỏ hàng hay cài đặt hiển thị người dùng đã chọn trong biến, chúng sẽ mất khi trang được tải lại hoặc đóng, và lần mở sau mọi thứ trở về trạng thái ban đầu. Code bạn viết từ đầu khóa học đến giờ chưa có chỗ nào giữ được giá trị qua những lần mở lại trang.
Bài này nói về localStorage, nơi lưu giá trị trong trình duyệt, và chuỗi JSON dùng để lưu mảng và object.
Giữ giá trị sau khi đóng trang — setItem và getItem
Giả sử một trang tin tức muốn nhớ cỡ chữ người dùng đã chọn để dùng lại ở lần mở sau. Dù bạn gán vào biến như const fontSize = "large";, biến sẽ mất khi trang được tải lại hoặc đóng, nên trang mở lần sau không đọc được nó.
localStorage (nơi lưu các cặp key và giá trị do trình duyệt cung cấp) lưu giá trị bằng setItem(key, giá trị), đọc ra bằng getItem(key) và xóa bằng removeItem(key).
Nơi lưu được tách riêng theo origin (phần đứng trước path của URL, như https://news.example trong https://news.example/settings), và các trang cùng origin đọc ghi cùng một bộ giá trị.
// Nếu vẫn còn giá trị đã lưu từ trước thì xóa đi, để bắt đầu từ trạng thái chưa lưu gì
localStorage.removeItem("fontSize");
// Lưu khi người dùng chuyển cỡ chữ sang "lớn"
localStorage.setItem("fontSize", "large");
// Khi mở trang, đọc giá trị đã lưu ra để dùng
const fontSize = localStorage.getItem("fontSize");
console.log(fontSize); // large
// Lưu với cùng key thì giá trị cũ bị thay thế
localStorage.setItem("fontSize", "small");
console.log(localStorage.getItem("fontSize")); // small
- Các trang và tab của origin này dùng chung một localStorage
"fontSize"→"small"(giá trị đã bị thay khi lưu vào cùng key)- Vẫn còn sau khi đóng trang hay tải lại
- Các biến như
const fontSize - Khi tải lại trang, biến được tạo lại
- Có localStorage riêng
- Không đọc được
"fontSize"do news.example lưu
Nếu đọc bằng getItem khi trang mở và ghi bằng setItem khi người dùng đổi cài đặt, trang vẫn hiển thị đúng cỡ chữ đó sau khi tải lại. Giá trị đã lưu còn nguyên cho đến khi code của trang xóa nó, hoặc người dùng xóa trong phần cài đặt của trình duyệt.
Giá trị lưu trong bài tập còn lại đến lần chạy sau
Console bài tập chạy mọi bài tập JavaScript trên trang web này trong cùng một origin, nên chúng dùng chung một localStorage, và dữ liệu vẫn còn dù bạn mở lại trang. Để giá trị lưu từ những lần chạy trước không lẫn vào kết quả, các bài tập bắt đầu bằng việc dùng removeItem xóa những key sẽ dùng.
Lưu mảng và object — JSON.stringify
Giả sử một trang đặt phòng du lịch giữ các khách sạn người dùng đã thêm vào mục yêu thích trong một mảng, và muốn hiện lại danh sách đó ở lần mở sau. Truyền thẳng mảng cho setItem không báo lỗi, nhưng từ giá trị đọc ra, bạn không lấy được mã hay tên khách sạn nào cả.
setItem đổi giá trị được truyền vào thành chuỗi rồi mới lưu. Vì vậy, hãy đổi mảng và object thành chuỗi JSON bằng JSON.stringify trước khi lưu, rồi dùng JSON.parse để đổi chuỗi đọc ra về lại mảng hay object ban đầu.
// Nếu vẫn còn giá trị đã lưu từ trước thì xóa đi trước khi bắt đầu
localStorage.removeItem("favoriteHotels");
const favoriteHotels = [{ id: "H-101", name: "Khách sạn Biển" }, { id: "H-204", name: "Nhà nghỉ Phố Cổ" }];
// Truyền thẳng thì mảng bị đổi thành chuỗi ngay lúc lưu
localStorage.setItem("favoriteHotels", favoriteHotels);
console.log(localStorage.getItem("favoriteHotels")); // [object Object],[object Object]
// Đổi thành chuỗi JSON rồi mới lưu
localStorage.setItem("favoriteHotels", JSON.stringify(favoriteHotels));
const saved = localStorage.getItem("favoriteHotels");
console.log(saved); // [{"id":"H-101","name":"Khách sạn Biển"},{"id":"H-204","name":"Nhà nghỉ Phố Cổ"}]
// Đổi chuỗi đọc ra về lại mảng rồi mới dùng
const restored = JSON.parse(saved);
console.log(restored[1].name); // Nhà nghỉ Phố Cổ
Số và giá trị boolean cũng bị đổi thành chuỗi mà không báo lỗi, nên bạn chỉ nhận ra khi phép tính hay điều kiện if trên giá trị đọc ra cho kết quả sai. Bảng dưới đây cho biết, với mỗi giá trị truyền cho setItem, chuỗi nào được lưu và điều gì xảy ra khi bạn đọc nó ra.
| Giá trị truyền cho setItem | Chuỗi được lưu | Khi đọc ra |
|---|---|---|
| Số 20 | "20" | "20" + 1 thành "201" (dùng Number) |
| Boolean false | "false" | Là true trong if (dùng JSON.parse) |
| Mảng chuỗi ["A-1", "B-2"] | "A-1,B-2" | Một chuỗi nối bằng dấu phẩy |
Đọc key chưa được lưu — null mà getItem trả về
Trên một trang công thức nấu ăn, mã các công thức đã xem được lưu làm lịch sử xem, và khi mở trang thì hiện số lượng. Người mở lần đầu hoặc đã xóa lịch sử thì không có gì được lưu, nên nếu đọc length của kết quả JSON.parse với giả định lịch sử đã có, dòng đó sẽ báo lỗi TypeError.
Khi nhận một key chưa được lưu, getItem trả về null chứ không phải undefined. Nếu viết JSON.parse(localStorage.getItem(key)) ?? [], mảng rỗng chỉ được dùng khi chưa có gì được lưu.
// Đọc lịch sử xem. Nếu chưa lưu gì thì dùng mảng rỗng
function loadHistory() {
return JSON.parse(localStorage.getItem("viewHistory")) ?? [];
}
// Lần mở đầu tiên — chưa lưu gì nên getItem trả về null
localStorage.removeItem("viewHistory");
console.log(localStorage.getItem("viewHistory")); // null
console.log(loadHistory().length); // 0
// Khi đã lưu — chuỗi được đổi về lại mảng
localStorage.setItem("viewHistory", JSON.stringify(["R-12", "R-30"]));
console.log(loadHistory().length); // 2
// Sau removeItem — lại thành null
localStorage.removeItem("viewHistory");
console.log(loadHistory().length); // 0
Nếu gom phần đọc vào một hàm, phía gọi dùng được kết quả như một mảng mà không cần kiểm tra null, kể cả sau khi removeItem được gọi lúc đăng xuất. removeItem không báo lỗi dù key chưa từng được lưu, nên bạn cũng không cần kiểm tra có dữ liệu hay không trước khi xóa.
Giữ lại giỏ hàng sau khi tải lại trang — hàm lưu và hàm đọc
Tiếp theo, với giỏ hàng của một cửa hàng online, bạn muốn giữ nguyên nội dung giỏ khi trang được tải lại sau lúc thêm sản phẩm. Thêm sản phẩm bằng cart.push chỉ làm thay đổi biến, nên chỉ cần quên viết dòng lưu ở một chỗ là sản phẩm đó biến mất khi tải lại trang.
Hãy gom việc lưu vào hàm saveCart và việc đọc vào hàm loadCart, rồi gọi chúng ngay sau khi giỏ hàng thay đổi và khi trang mở. setItem chạy đồng bộ (lưu xong rồi mới chuyển sang dòng tiếp theo), nên khác với fetch, bạn đọc được giá trị vừa lưu ở dòng kế tiếp mà không cần await.
localStorage.removeItem("cart"); // Nếu vẫn còn giỏ hàng đã lưu từ trước thì xóa đi
// Lưu giỏ hàng dưới dạng chuỗi JSON. Không cần await
function saveCart(cart) {
localStorage.setItem("cart", JSON.stringify(cart));
}
// Đổi giỏ hàng đã lưu về lại mảng. Nếu chưa lưu gì thì trả về giỏ rỗng
function loadCart() {
return JSON.parse(localStorage.getItem("cart")) ?? [];
}
// Đọc khi mở trang, và lưu sau khi thêm sản phẩm
const cart = loadCart();
cart.push({ id: "K-310", qty: 1 });
saveCart(cart);
// Thay cho việc tải lại trang: không dùng biến cart mà đọc lại từ chuỗi đã lưu
const reloaded = loadCart();
console.log(reloaded.length, reloaded[0].id); // 1 K-310
Nếu gom mọi thao tác thay đổi giỏ hàng vào một hàm và gọi saveCart ở cuối hàm đó, bạn không phải viết lệnh lưu ở từng nơi gọi, nên cũng không thể quên. Việc đọc cũng đã gom vào loadCart, nên không có chỗ nào quên viết ?? [].
Vượt quá dung lượng thì setItem báo lỗi
Trên Chrome, một origin lưu được khoảng 5,24 triệu ký tự, tính cả key lẫn giá trị. Vượt quá mức đó, setItem báo lỗi QuotaExceededError và giá trị không được lưu. Việc đọc ghi là đồng bộ, nên trong lúc lưu một giá trị lớn, trang cũng ngừng phản hồi thao tác. Hãy dùng localStorage cho những giá trị nhỏ như giỏ hàng hay cài đặt.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Sau localStorage.setItem("darkMode", false), nếu dùng giá trị đọc ra làm điều kiện if thì sao?
Câu 3Nếu tải lại trang sau cart.push(book) mà không gọi saveCart, loadCart() trả về gì?