7 Tính Năng JavaScript Bị Đánh Giá Thấp Giúp Bạn Tiết Kiệm Thời Gian
Hôm nọ, khi đang lướt một subreddit dành cho lập trình viên, tôi tình cờ tìm thấy một mỏ vàng: một chủ đề về các tính năng JavaScript bị đánh giá thấp nhất. Đó không phải là về các framework mới hào nhoáng, mà là về những công cụ tích hợp sẵn, âm thầm thực hiện các công việc nặng nhọc mà không cần phô trương.

Đây là những tính năng khiến bạn nhìn lại code cũ của mình (hoặc cái file utils.js khổng lồ đó) và nghĩ, "Lẽ ra mình chỉ cần dùng cái đó là được rồi?".
Vì vậy, tôi đã tổng hợp bảy tính năng tốt nhất. Đây không chỉ là những mẹo hay ho; chúng là những giải pháp đã được kiểm chứng qua thực tế, thực sự có thể làm cho code của bạn sạch hơn, nhanh hơn và dễ đọc hơn. Nhưng trước khi chúng ta bắt đầu, hãy nói một chút về việc cài đặt. Để sử dụng một số tính năng hiện đại này, đặc biệt là trong môi trường Node.js, bạn cần phải sử dụng phiên bản mới. Việc xoay xở với các phiên bản Node khác nhau cho các dự án khác nhau có thể là một nỗi đau thực sự. Nếu bạn đã từng phải vật lộn với nvm hay brew, bạn sẽ hiểu ý tôi. Một công cụ như ServBay có thể là cứu tinh trong trường hợp này, cho phép bạn cài đặt và chuyển đổi giữa các phiên bản Node.js chỉ bằng một cú nhấp chuột. Nhưng chúng ta sẽ nói thêm về điều đó sau.

Hãy bắt đầu với code nào.
Set: Công Cụ Tối Thượng cho Các Mục Duy Nhất và Tra Cứu Nhanh
Khi bạn nghe đến "loại bỏ các phần tử trùng lặp khỏi một mảng", não bạn có thể sẽ nhảy ngay đến filter + indexOf. Cách này hoạt động, nhưng đó là con đường vòng—chậm và không hiệu quả, với độ phức tạp thời gian là O(n²).
Set chính là chuyến tàu tốc hành. Nó là một cấu trúc dữ liệu mà bản chất chỉ chứa các giá trị duy nhất. Và việc kiểm tra xem một mục có tồn tại hay không nhanh như chớp (O(1)).
Ví dụ: Loại bỏ email trùng lặp khỏi danh sách người dùng
// Danh sách email từ một lần nhập dữ liệu lộn xộn
const emails = ['dev@example.com', 'ceo@example.com', 'dev@example.com', 'support@example.com'];
// Một dòng để lấy các email duy nhất
const uniqueEmails = [...new Set(emails)];
console.log(uniqueEmails);
// -> ['dev@example.com', 'ceo@example.com', 'support@example.com']
Object.entries() & Object.fromEntries(): Những Người Bạn Thân Mới Của Bạn Để Thao Tác Với Object
Bạn còn nhớ vòng lặp for...in cũ kỹ không? Bạn phải kiểm tra hasOwnProperty mỗi lần để tránh lặp qua các thuộc tính của prototype. Cảm giác như đang đi trên vỏ trứng vậy.
Cặp phương thức này giúp làm việc với đối tượng dễ dàng như làm việc với mảng. Object.entries() biến một đối tượng thành [[key, value], [key, value]], và Object.fromEntries() làm điều hoàn toàn ngược lại. Nó hoàn hảo để lọc hoặc ánh xạ các đối tượng.
Ví dụ: Lọc ra các giá trị null hoặc undefined từ phản hồi API
// Một đối tượng sản phẩm từ API, với một số dữ liệu bị thiếu
const product = {
id: 123,
name: 'Super Cool Gadget',
price: 99.99,
description: 'The best gadget ever.',
inStock: null, // Chúng ta muốn loại bỏ cái này
rating: undefined // Và cái này nữa
};
// 1. Chuyển thành mảng, 2. Lọc, 3. Chuyển ngược lại thành đối tượng
const cleanProduct = Object.fromEntries(
Object.entries(product).filter(([key, value]) => value != null)
);
console.log(cleanProduct);
// -> { id: 123, name: 'Super Cool Gadget', price: 99.99, description: 'The best gadget ever.' }
?? và ??=: Các Toán Tử Gán Giá Trị Mặc Định Thông Minh Hơn
Trong nhiều năm, chúng ta đã sử dụng toán tử || (OR) cho các giá trị mặc định. Vấn đề là || coi bất kỳ giá trị "falsy" nào (0, '', false) là thứ cần được thay thế. Điều này có thể dẫn đến lỗi.
Toán tử nullish coalescing (??) chính xác hơn nhiều. Nó chỉ kích hoạt khi gặp null hoặc undefined.
Ví dụ: Xử lý đầu vào của người dùng khi 0 là một giá trị hợp lệ
const userInputQuantity = 0;
// Cách cũ, dễ gây lỗi: 0 là falsy, nên nó sẽ mặc định là 1
const quantity_buggy = userInputQuantity || 1;
// Cách mới, chính xác: 0 không phải là nullish, nên nó được giữ lại
const quantity_correct = userInputQuantity ?? 1;
console.log(`Lỗi: ${quantity_buggy}`); // -> Lỗi: 1
console.log(`Đúng: ${quantity_correct}`); // -> Đúng: 0
Toán tử gán logic nullish (??=) thậm chí còn tiện lợi hơn. Nó chỉ gán một giá trị nếu biến hiện tại là null hoặc undefined.
Ví dụ: Thiết lập các tùy chọn cấu hình mặc định
// Cấu hình do người dùng cung cấp có thể thiếu một số giá trị
const userConfig = {
theme: 'dark'
};
// Áp dụng các giá trị mặc định mà không ghi đè cài đặt hiện có
userConfig.theme ??= 'light'; // Không làm gì cả, 'dark' đã được thiết lập
userConfig.timeout ??= 5000; // timeout là nullish, nên nó được gán giá trị 5000
console.log(userConfig);
// -> { theme: 'dark', timeout: 5000 }
Intersection Observer: Cuộn Trang Mượt Mà Không Bị Giật, Lác
Cách cũ để triển khai lazy loading (tải lười) hoặc infinite scroll (cuộn vô hạn) là lắng nghe sự kiện scroll và gọi getBoundingClientRect() trong một vòng lặp. Đây là một cơn ác mộng về hiệu năng vì nó buộc trình duyệt phải liên tục tính toán lại layout, dẫn đến trải nghiệm giật, lác.
Intersection Observer là cách tiếp cận hiện đại, không chặn (non-blocking). Bạn yêu cầu nó theo dõi một phần tử, và nó sẽ thông báo cho bạn một cách bất đồng bộ khi phần tử đó đi vào hoặc rời khỏi khung nhìn (viewport).
Ví dụ: Lazy-loading ảnh đơn giản
<!-- URL ảnh thật nằm trong data-src -->
<img class="lazy-image" src="placeholder-spinner.gif" data-src="real-image.jpg">
const lazyImages = document.querySelectorAll('.lazy-image');
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// Nếu ảnh nằm trong khung nhìn
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // Thay ảnh giữ chỗ bằng ảnh thật
img.classList.remove('lazy-image');
observer.unobserve(img); // Ngừng theo dõi ảnh này
}
});
});
lazyImages.forEach(img => observer.observe(img));
Promise.allSettled(): Khi Bạn Cần Mọi Promise Hoàn Thành, Dù Thắng Hay Thua
Promise.all rất tuyệt, nhưng nó là một thỏa thuận "được ăn cả, ngã về không". Nếu một promise trong nhóm bị reject, toàn bộ sẽ thất bại, và bạn sẽ mất kết quả từ những promise đã thành công.
Promise.allSettled() linh hoạt hơn. Nó đợi cho tất cả các promise hoàn thành, bất kể chúng thành công hay thất bại. Sau đó, nó cung cấp cho bạn một báo cáo gọn gàng về từng kết quả.
Ví dụ: Lấy dữ liệu từ nhiều API nơi một số có thể thất bại
const apiEndpoints = [
fetch('https://api.github.com/users/github'), // Cái này sẽ hoạt động
fetch('https://api.example.com/non-existent'), // Cái này sẽ thất bại
fetch('https://api.github.com/users/vercel') // Cái này sẽ hoạt động
];
Promise.allSettled(apiEndpoints)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Endpoint ${index} thành công với giá trị:`, result.value.status);
} else {
console.error(`Endpoint ${index} thất bại với lý do:`, result.reason.message);
}
});
});
// -> Endpoint 0 thành công với giá trị: 200
// -> Endpoint 1 thất bại với lý do: Failed to fetch
// -> Endpoint 2 thành công với giá trị: 200
URL và URLSearchParams: Ngừng Vật Lộn Với Các Chuỗi URL
Việc phân tích các tham số truy vấn (query parameters) từ một URL bằng regex là một việc đau đớn mà nhiều lập trình viên phải trải qua. Các đối tượng tích hợp sẵn URL và URLSearchParams khiến việc này trở nên cực kỳ đơn giản.
Ví dụ: Đọc và thao tác với các tham số truy vấn URL
const urlString = 'https://mysite.com/products?category=electronics&page=2';
const url = new URL(urlString);
// Đọc tham số
const category = url.searchParams.get('category'); // "electronics"
console.log(`Danh mục là: ${category}`);
// Thêm một tham số mới
url.searchParams.append('sort', 'price_desc');
// Sửa một tham số hiện có
url.searchParams.set('page', '3');
// Chuỗi URL mới
console.log(url.href);
// -> https://mysite.com/products?category=electronics&page=3&sort=price_desc
Top-Level await: Nói Lời Tạm Biệt Với async IIFE
Trước đây, nếu bạn muốn sử dụng await ở cấp cao nhất của một module ES (ví dụ: để lấy cấu hình trước khi phần còn lại của module chạy), bạn phải bọc nó trong một Biểu thức Hàm được Gọi Ngay lập tức (IIFE) (async () => { ... })();. Thật là cồng kềnh.
Top-level await cho phép bạn sử dụng await trực tiếp trong các module của mình. Bất kỳ module nào khác nhập nó sẽ chỉ đơn giản là đợi promise được giải quyết trước khi thực thi.
Ví dụ: Tải một tệp cấu hình một cách bất đồng bộ
// trong file config.js
const response = await fetch('/api/app-settings');
export const AppConfig = await response.json();
// trong file main.js
import { AppConfig } from './config.js';
// Đoạn code này chỉ chạy sau khi cấu hình đã được lấy và phân tích.
console.log(`URL của API là: ${AppConfig.apiUrl}`);
Tạm dừng một chút: Quản lý các phiên bản Node.js cho các tính năng hiện đại
Tính năng cuối cùng này, top-level await, là một ví dụ hoàn hảo về một tính năng phụ thuộc vào môi trường của bạn. Nó được hỗ trợ trong các trình duyệt hiện đại và trong Node.js v14.8+.
Đây là lúc các lập trình viên thường gặp khó khăn. Bạn hào hứng muốn sử dụng một tính năng hiện đại, nhưng dự án của khách hàng lại bị kẹt ở Node.js 12. Dự án cá nhân mới của bạn lại cần Node.js 20. Chẳng mấy chốc, bạn rơi vào địa ngục quản lý phiên bản, vật lộn với nvm, n, hoặc trình quản lý gói của hệ thống. Nó làm bạn xao lãng khỏi điều bạn thực sự muốn làm: viết code.
Đây chính xác là loại nhức đầu mà một công cụ như ServBay được thiết kế để loại bỏ. Nó là một môi trường phát triển cục bộ coi Node.js (cùng với PHP, MariaDB, v.v.) là một công dân hạng nhất.
- Cài đặt bằng một cú nhấp chuột: Bạn có thể cài đặt nhiều phiên bản Node.js (ví dụ: 14, 16, 18, 20) chỉ bằng một cú nhấp chuột đơn giản. Không còn lỗi dòng lệnh khó hiểu.
- Chuyển đổi dễ dàng: Cần chuyển một dự án từ Node 16 sang Node 20? Chỉ cần một menu thả xuống.
- Cùng tồn tại: Các dự án khác nhau có thể chạy trên các phiên bản Node.js khác nhau đồng thời mà không xung đột.
Nó cho phép bạn tập trung vào việc sử dụng các tính năng JS tuyệt vời này mà không lãng phí một giờ để gỡ lỗi thiết lập cục bộ của mình.
Lời kết
JavaScript đã tiến một chặng đường dài. Nhiều vấn đề mà chúng ta từng phụ thuộc vào các thư viện nặng nề như Lodash hoặc Moment.js để giải quyết giờ đây có thể được xử lý bởi chính ngôn ngữ.
Khi các trình duyệt cũ dần trở nên lỗi thời, các API gốc này đang trở thành cách làm tiêu chuẩn. Vì vậy, lần tới khi bạn định gõ npm install, hãy dành một giây để tự hỏi: "Liệu JavaScript đã có thể tự làm điều này chưa?" Câu trả lời có thể sẽ làm bạn ngạc nhiên.
All rights reserved