Một đoạn code chuyển danh sách chuỗi thành số chỉ có một dòng: values.map(parseInt). Nó trông hợp lý vì parseInt nhận chuỗi, còn map gọi hàm cho mỗi phần tử. Vấn đề là hai bên không chỉ trao đổi một argument.
Các snippet được chạy bằng Node.js 24.18.1, tài liệu MDN được đối chiếu ngày 03/11/2026. Bài dùng mảng chuỗi đặc, không xử lý sparse array hoặc pipeline dữ liệu lớn. Đây là kiểm chứng hợp đồng callback, không phải benchmark giữa các cách parse số.
Xác nhận kết quả trước khi giải thích
Lưu thành radix.cjs, chạy node radix.cjs:
const assert = require('node:assert/strict');
const values = ['10', '10', '10'];
const result = values.map(parseInt);
assert.equal(result[0], 10);
assert.equal(Number.isNaN(result[1]), true);
assert.equal(result[2], 2);
console.log(result);
Kết quả đã kiểm tra là [10, NaN, 2]. Dùng Number.isNaN cho phần tử giữa để kiểm tra NaN đúng cách, không so result[1] === NaN. Dữ liệu cố ý giữ nguyên cả ba chuỗi để thấy phần khác biệt đến từ vị trí callback, không phải text đầu vào.
Ghép hai chữ ký hàm
map gọi callback với phần tử, index và mảng. parseInt dùng argument đầu làm chuỗi, argument thứ hai làm radix, tức cơ số. Khi đưa trực tiếp parseInt vào map, index đã trở thành radix dù bạn không viết tên nó trong dòng code.
Với fixture này, ba lần gọi có ý nghĩa tương ứng parseInt('10', 0), parseInt('10', 1) và parseInt('10', 2). Radix 0 được suy luận từ chuỗi; với chuỗi đang dùng, kết quả là 10. Radix 1 không hợp lệ nên cho NaN. Chuỗi 10 trong cơ số 2 biểu diễn số 2 trong hệ thập phân.
Bạn có thể kiểm tra đường đi arguments bằng một callback quan sát:
const assert = require('node:assert/strict');
const values = ['10', '10', '10'];
const seen = [];
const result = values.map((value, index, array) => {
seen.push([value, index, array === values]);
return parseInt(value, index);
});
assert.deepEqual(seen, [['10', 0, true], ['10', 1, true], ['10', 2, true]]);
assert.deepEqual(result, [10, NaN, 2]);
console.log(seen);
Snippet đã chạy qua assertion. Nó không thay đổi hành vi để sửa lỗi; nó chỉ làm các arguments vốn được truyền sẵn trở nên dễ quan sát. Đây là bước giải thích có thể áp dụng cho các câu hỏi callback khác: viết ra chữ ký phía gọi và phía nhận, rồi ghép theo vị trí.
Dùng wrapper để chốt cơ số
Chạy snippet độc lập:
const assert = require('node:assert/strict');
const values = ['10', '10', '10'];
const result = values.map(value => parseInt(value, 10));
assert.deepEqual(result, [10, 10, 10]);
assert.equal(parseInt('10px', 10), 10);
console.log(result);
Wrapper chỉ lấy giá trị cần dùng và tự cung cấp radix 10. Index vẫn được map truyền cho callback nhưng callback không chuyển nó sang parseInt. Kết quả đã kiểm tra là ba số 10.
Assertion với 10px nhắc một giới hạn riêng: sửa radix chưa biến parseInt thành phép validation toàn chuỗi. Nếu nghiệp vụ chỉ nhận chuỗi số nguyên hoàn chỉnh, hãy định nghĩa grammar, khoảng giá trị và quy tắc dữ liệu thiếu ở lớp nhận đầu vào. Bài này không cung cấp validator tổng quát.
Đừng sửa bằng cách đoán một hàm thay thế
Một hàm chuyển đổi khác có thể cho kết quả mong muốn trên ba chuỗi này nhưng có quy tắc riêng với chuỗi rỗng, số thập phân hoặc giá trị không phải chuỗi. Hãy chọn theo hợp đồng dữ liệu, không chỉ vì một dòng output trông đúng.
Khi trả lời phỏng vấn, trình bày kết quả, số arguments của map, ý nghĩa radix rồi wrapper sửa lỗi. Thứ tự đó cho thấy bạn hiểu cơ chế thay vì chỉ nhớ câu đố. Trong code review, một wrapper ngắn cũng giúp ý định “parse thập phân” xuất hiện ngay tại nơi chuyển đổi.


