Symbol là giá trị chắc chắn duy nhất
Symbol là một trong các kiểu nguyên thủy của JavaScript, bên cạnh string, number, boolean, null, undefined và bigint. Đặc điểm cốt lõi của nó rất đơn giản: mỗi symbol bạn tạo ra đều khác mọi symbol khác, mãi mãi.
Hai symbol, đều được tạo không có đối số, vẫn không bằng nhau. Đó không phải tình cờ mà chính là mục đích. Bạn không thể làm giả một symbol, vô tình tạo lại một symbol, hay đụng độ với symbol của người khác.
Bạn có thể gắn mô tả để dễ gỡ lỗi. Mô tả không ảnh hưởng tới danh tính:
Cùng mô tả, khác symbol. Mô tả chỉ là nhãn cho người đọc log.
Vì sao symbol tồn tại: khóa không đụng độ
Lý do chính symbol tồn tại là để bạn thêm thuộc tính vào đối tượng mà không lo khóa của mình trùng với khóa của người khác. Khóa chuỗi cạnh tranh trong một không gian tên phẳng: nếu hai thư viện cùng quyết định cất siêu dữ liệu ở obj.meta, chúng sẽ giẫm lên nhau. Khóa symbol thì không cạnh tranh, vì không ai khác có symbol của bạn.
Dấu ngoặc vuông trong [ID]: 42 là cú pháp thuộc tính tính toán (computed property): nó nói "hãy dùng giá trị của ID làm khóa". Chỉ code nào giữ cùng symbol ID mới đọc hoặc ghi đè được thuộc tính đó. Bất kỳ module nào khác dùng chuỗi "id" hay Symbol("id") của riêng nó đều đang nói về một ô hoàn toàn khác.
Khóa symbol (phần lớn) được ẩn đi
Thuộc tính có khóa symbol không xuất hiện trong for...in, Object.keys hay JSON.stringify. Chúng không bí mật, code quyết tâm tìm vẫn thấy, nhưng chúng tránh đường khi duyệt thông thường.
Object.keys và JSON.stringify bỏ qua hoàn toàn khóa symbol. Nếu bạn cố ý cần tìm thuộc tính symbol, Object.getOwnPropertySymbols và Reflect.ownKeys sẽ cho bạn thấy chúng. Đây đúng là hành vi bạn muốn cho siêu dữ liệu: nhìn thấy khi bạn chủ động tìm, vô hình với code chỉ duyệt khóa chuỗi.
Chia sẻ symbol với Symbol.for
Symbol() cho bạn một symbol cục bộ, dùng một lần. Đôi khi bạn muốn điều ngược lại: một symbol giống nhau ở mọi nơi trong chương trình, qua các module, để nhiều phần code thống nhất được một khóa. Đó là việc của Symbol.for.
Symbol.for(key) kiểm tra một registry toàn cục: nếu symbol cho khóa đó đã tồn tại, bạn nhận lại nó; nếu chưa, nó tạo mới và lưu lại. Symbol.keyFor đi theo chiều ngược lại: nó trả về khóa đã dùng để tạo một symbol được đăng ký (hoặc undefined với symbol chưa đăng ký).
Phần lớn thời gian, Symbol() thông thường là thứ bạn cần. Hãy dùng Symbol.for khi một symbol thực sự phải được chia sẻ qua ranh giới module.
Well-known symbol: điểm móc vào ngôn ngữ
Đây là chỗ symbol thực sự phát huy giá trị. JavaScript cung cấp một nhóm symbol định nghĩa sẵn, gọi là well-known symbol, mà chính ngôn ngữ kiểm tra trên đối tượng của bạn. Cài đặt một phương thức ở một trong các khóa này, đối tượng của bạn sẽ kết nối thẳng vào một tính năng có sẵn.
Hữu ích nhất là Symbol.iterator. Đối tượng nào có phương thức ở khóa này đều là iterable: nó dùng được với for...of, spread, destructuring và Array.from.
range không phải mảng. Nó là một đối tượng thường với một phương thức có khóa đặc biệt. Nhưng vì phương thức đó trả về một iterator, cả for...of lẫn spread đều dùng được trên nó, giống như với mảng, chuỗi và Map. Đó là giao ước: cài đặt Symbol.iterator, và ngôn ngữ coi bạn là iterable.
Các well-known symbol khác nên biết
Còn vài symbol nữa, mỗi cái là một điểm móc vào một phần khác của ngôn ngữ:
Symbol.iterator: biến đối tượng thành iterable.Symbol.asyncIterator: tương tự, nhưng chofor await...of.Symbol.toPrimitive: điều khiển cách đối tượng chuyển thành giá trị nguyên thủy (số, chuỗi hoặc mặc định) khi bị ép kiểu.Symbol.hasInstance: tùy chỉnh kết quảinstanceoftrả về cho lớp của bạn.Symbol.toStringTag: đặt nhãn hiển thị trongObject.prototype.toString.call(obj).
Bạn không cần thuộc lòng chúng. Biết chúng tồn tại là đủ: khi gặp tình huống muốn đối tượng của mình hoạt động như một kiểu có sẵn, thường sẽ có một well-known symbol cho việc đó.
Symbol không phải chuỗi
Một chỗ hay vấp: symbol không tự chuyển thành chuỗi. Nối chuỗi với symbol sẽ ném lỗi, và đưa symbol cho một API cần chuỗi cũng vậy:
Template literal cũng ném cùng TypeError đó. Dùng String(symbol) hoặc symbol.toString() khi bạn thực sự cần văn bản. Những góc cạnh này là có chủ ý: ngôn ngữ đang bảo vệ bạn khỏi việc vô tình coi một giá trị danh tính duy nhất như một mẩu dữ liệu chuỗi bình thường.
Khi nào nên dùng symbol (và khi nào không)
Hãy dùng symbol khi:
- Bạn gắn siêu dữ liệu vào đối tượng không phải của mình và cần một khóa không bị trùng.
- Bạn thiết kế một giao thức: "đối tượng nào muốn dùng với thư viện của tôi thì cài đặt một phương thức ở symbol này".
- Bạn muốn một thuộc tính không lọt vào
JSON.stringifyhayfor...in. - Bạn cài đặt
Symbol.iteratorhoặc một well-known symbol khác.
Bỏ qua symbol khi:
- Bạn chỉ cần một khóa đối tượng và không có nguy cơ trùng tên. Chuỗi đơn giản hơn và hiện trong log đúng như bản thân nó.
- Bạn muốn trường "private". Thuộc tính có khóa symbol không thật sự private:
Object.getOwnPropertySymbolstìm thấy chúng. Để riêng tư thật sự, hãy dùng private field#privatecủa lớp. - Bạn lưu dữ liệu cần giữ nguyên qua
JSON.stringify. Nó sẽ không giữ được.
Phần lớn code JavaScript vẫn ổn mà không bao giờ phải viết trực tiếp Symbol(...). Nhưng ngay khi bạn muốn đối tượng của mình dùng được với for...of hay hoạt động tự nhiên khi bị ép kiểu, symbol là cánh cửa dẫn vào cơ chế đó.
Tiếp theo: khai báo hàm
Symbol, iterator và generator đều dựa nhiều vào hàm: phương thức đặt ở Symbol.iterator, các hàm factory trả về iterator, các generator với dạng function* giấu đi phần lớn code lặp lại. Hàm là chương tiếp theo, bắt đầu với các cách khai báo hàm khác nhau và điểm khác biệt giữa từng dạng.
Câu hỏi thường gặp
Symbol trong JavaScript là gì?
Symbol là một kiểu nguyên thủy mà mọi giá trị đều được đảm bảo là duy nhất. Bạn tạo symbol bằng Symbol() hoặc Symbol('description'), và không bao giờ có hai lần gọi tạo ra hai symbol bằng nhau. Chúng chủ yếu được dùng làm khóa đối tượng không đụng độ với khóa khác, và làm điểm móc vào tính năng ngôn ngữ thông qua các well-known symbol như Symbol.iterator.
Khi nào nên dùng symbol thay cho khóa chuỗi?
Dùng symbol khi bạn muốn gắn một thuộc tính vào đối tượng mà không sợ trùng tên với code khác: thư viện thêm siêu dữ liệu, framework đánh dấu đối tượng, hoặc định nghĩa một khóa cố ý không thuộc API công khai. Với khóa thường ngày, khi khả năng đọc quan trọng và không lo trùng tên, chuỗi vẫn là lựa chọn đúng.
Symbol.iterator dùng để làm gì?
Symbol.iterator là một well-known symbol cho for...of, spread và destructuring biết cách duyệt một đối tượng. Nếu bạn định nghĩa một phương thức ở khóa Symbol.iterator và trả về một iterator, đối tượng của bạn trở thành iterable. Đó là cách mảng, chuỗi, Map và Set hoạt động bên dưới.
Symbol() và Symbol.for() khác nhau thế nào?
Symbol('x') luôn tạo ra một symbol hoàn toàn mới: hai lần gọi với cùng mô tả vẫn khác nhau. Symbol.for('x') tra cứu trong một registry toàn cục và trả về cùng một symbol cho cùng một khóa trong toàn bộ chương trình. Dùng Symbol.for khi cần chia sẻ symbol giữa các module hoặc realm; dùng Symbol() khi chỉ cần tính duy nhất cục bộ.