Symbols, Iterators trong Javascript
Bài đăng này đã không được cập nhật trong 6 năm
Symbols
Trong ES2015, một kiểu dữ liệu mới được tạo ra có tên là symbol
.
Tại sao lại có kiểu dữ liệu này?
Có 3 lý do chính:
1 - Thêm một core-features mới với khả năng tương thích ngược
Đôi khi chúng ta cần thêm một thuộc tính mới vào đối tượng hiện tại mà không muốn gây ảnh hưởng tới vòng lặp for in
hay hàm Object.keys
.
Ví dụ: Cho một đối tượng: var myObject = {firstName:'raja', lastName:'rao'}
. Khi gọi hàm Object.keys(myObject)
, kết quả trả về là [firstName, lastName]
. Giờ nếu thêm một thuộc tính mới có tên là newProperty
vào myObject
và mong muốn kết quả trả về của hàm Object.keys(myObject)
vẫn phải là [firstName, lastName]
, chứ không phải [firstName, lastName, newProperty]
thì làm như thế nào?
Nếu newProperty
là một symbol thì Object.keys(myObject)
sẽ bỏ qua thuộc tính này và kết quả trả về vẫn là [firstName, lastName]
.
2 - Tránh xung đột về tên gọi
Symbol
đảm bảo rằng các thuộc tính mới được thêm vào một object là duy nhất.
Ví dụ: Khi thêm một phương thức tùy biến toUpperCase
vào Array.prototype
. Sau đó, nếu load một thư viện mới (hoặc trong ES2019 sắp tới) và nó có một phiên bản khác của Array.prototype.toUpperCase
thì phương thức tùy biến có thể hoạt động không chính xác vì xung đột tên.
Symbols
sẽ giải quyết vấn đề này bằng cách tạo ngầm một giá trị duy nhất khi thêm những thuộc tính mới.
3 - Tạo hooks đối với những phương thức core thông qua các symbol "phổ biến"
Giả sử chúng ta muốn String.prototype.search
gọi đến phương thức search
tùy biến bên trong một đối tượng myObject
: ‘somestring’.search(myObject);
và truyền ‘somestring’
như một tham số.
Điều này có thể được thực hiện thông qua các symbol toàn cục có tên là symbol phổ biến. Chúng cho phép tạo lời gọi đến các hàm bất kỳ bên trong các hàm core.
Tạo Symbols
Một symbol có thể được tạo bằng cách sử dụng từ khóa Symbol
. Cách khởi tạo này sẽ trả về một giá trị có kiểu dữ liệu là symbol
.
Lưu ý: dù cách khởi tạo như một đối tượng nhưng thực chất symbol là một giá trị nguyên thủy. Chúng bất biến và có tính duy nhất.
Symbol không thể tạo bằng từ khóa new
Bởi symbol không phải là object nên không thể sử dụng từ khóa new
để trả về một giá trị kiểu symbol
.
var mySymbol = new Symbol(); //throws error
Symbol có phần mô tả cho việc logging
//mySymbol variable now holds a "symbol" unique value
//its description is "some text"
const mySymbol = Symbol('some text');
Symbol có tính duy nhất
const mySymbol1 = Symbol('some text');
const mySymbol2 = Symbol('some text');
mySymbol1 == mySymbol2 // false
Symbol có thể được coi là một singleton nếu sử dụng phương thức Symbol.for
Thay vì khởi tạo một symbol
thông qua Symbol()
, một symbol có thể tạo thông qua Symbol.for(<key>)
. Trong đó, key
là một string bất kỳ. Nếu một symbol có key
đã tồn tại thì việc sử dụng phương thức trên sẽ chỉ trả về giá trị cũ.
var mySymbol1 = Symbol.for('some key'); //creates a new symbol
var mySymbol2 = Symbol.for('some key'); // **returns the same symbol
mySymbol1 == mySymbol2 //true
Mục đính thực sự của việc sử dụng .for
là để tạo Symbol ở một nơi và truy cập ở một nơi khác.
Lưu ý: Symbol.for
sẽ khiến symbol không còn tính duy nhất. Dó đó tránh việc sử dụng các key giống nhau.
Mô tả của symbol khác với key của symbol
Mô tả của symbol vẫn đảm bảo tính duy nhất. Còn Symbol.for
thì không.
Symbol có thể là tên thuộc tính của một đối tượng
Chính vì tính duy nhất nên một symbol có thể được gán như tên của một thuộc tính bên trong một đối tượng. Những thuộc tính này còn được gọi là keyed properties
Truy cập vào thuộc tính có tên sử dụng symbol
Thuộc tính sử dụng symbol chỉ có thể sử dụng thông qua ngoặc vuông và không thể sử dụng dấu chấm.
Nhìn lại 3 lý do chính cho việc sử dụng Symbol
1 - Symbol sẽ bị bỏ qua đối với các vòng lặp và một số hàm đọc thuộc tính
Trong ví dụ dưới đây, khi duyệt qua các thuộc tính của obj
thì các thuộc tính prop3
và prop4
sẽ bị bỏ qua vì chúng có kiểu dữ liệu symbol.
Tương tự với các phương thức Object.keys
và Object.getOwnPropertyNames
2 - Symbol có tính duy nhất
Giả sử chúng ta muốn thêm một tính năng mới vào đối tượng Array
có tên là includes
. Thay vì đặt một tên gọi kiểu như custom_includes
thì làm như nào để có thể thêm trực tiếp phương thức này mà tránh được việc xung đột với Array.prototype.includes
đã có sẵn ?
Đầu tiên, tạo một biến có tên chính xác là includes
và gán một symbol cho nó. Sau đố thêm biến này vào đối tượng Array
sử dụng giống ngoặc vuông và gán bất kỳ hàm nào mong muốn.
Cuối cùng sử dụng hàm trên bằng dấu ngoặc vuông. Nhưng chú ý là phải sử dụng đúng tên biến bên trong dấu ngoặc vuông arr[includes]()
, chứ không phải một chuỗi ký tự.
3 - Symbol "phổ biến" (symbol "toàn cục")
Javascript đã có sẵn một số biến symbol và gán chúng vào đối tượng toàn cục Symbol
. Ví dụ: Đối với object String
trong ES2015: Symbol.match
, Symbol.replace
, Symbol.search
, Symbol.iterator
và Symbol.split
.
Một ví dụ về Symbol: Symbol.search
Phương thức pubic String.prototype.search
được dùng để tìm kiếm trong chuỗi ký tự dựa vào từ khóa hoặc một biểu thức chính quy và trả về vị trí tìm thấy.
Trong ES2015, phương thức này sẽ kiểm tra xem có phương thức Symbol.search
đã được định nghĩa bên trong đối tượng RegExp. Nếu có thì sẽ gọi đến hàm này và ủy quyền cho nó. Do đó, thực tế thì trong phép tìm kiếm đầu tiên ở ví dụ trên, phương thức có symbol là Symbol.search
của đối tượng RegExp sẽ có nhiệm vụ chính trong việc tìm kiếm.
Cách thức hoạt động bên trong của Symbol.search (Mặc định)
- Phân tích
‘rajarao’.search(‘rao’);
- Chuyển "rajarao" thành một đối tượng String
new String(“rajarao”)
- Chuyển "rao" thành một đối tượng RegExp
new Regexp(“rao”)
- Gọi hàm
search
của String objectrajarao
- Hàm
search
gọi tới phương thứcSymbol.search
của đối tượng "rao" và gán việc tìm kiếm cho đối tượng "rao" với tham số truyền vào là "rajarao". Giống như sau:"rao"[Symbol.search]("rajarao")
"rao"[Symbol.search]("rajarao")
trả về vị trí (index) là 4 cho hàmsearch
và cuối cùng hàmsearch
lại trả về giá trị 4 cho đoạn code hiện tại. Nhưng điều hay ho ở đây là chúng ta có thể truyền vào bất cứ đối tượng nào vào hàmsearch
của đối tượng String, không chỉ có mỗi RegExp. Kết quả của hàmsearch
sẽ phụ thuộc vào việc định nghĩa phương thứcSymbol.search
của đối tượng được truyền vào.
Tùy biến lại hàm String.search theo một hàm bất kỳ
- Phân tích
‘barsoap’.search(soapObj);
- Chuyển "barsoap" thành một đối tượng String
new String(“barsoap”)
- Vì
soapObj
đã là một đối tượng rồi nên không cần bất kỳ phép chuyển đổi nào khác - Gọi hàm
search
của đối tượng String "barsoap" - Hàm
search
gọi tới phương thứcSymbol.search
của đối tượngsoapObj
và gán việc tìm kiếm cho đối tượng 'soapObj' với tham số truyền vào là "barsoap". Giống như sau:soapObj[Symbol.search]("barsoap")
- Kết quả trả về là
FOUND
Iterators và Iterables
Thông thường để duyệt qua một bộ dữ liệu của những đối tượng tiêu chuẩn như mảng, chuỗi hay map thì chúng ta có những cách như sử dụng vòng lặp for-of
hoặc toán tử spread __
. Tuy nhiên, những cách này không thể áp dụng được đối với các đối tượng bình thường.
Ở ví dụ sau, để duyệt qua đối tượng allUsers
, chúng ta phải sử dụng một phương thức tùy biến get
thay vì vòng lặp for-of
hay toán tử spread.
Nhưng nếu tất cả các object đều được định nghĩa theo những quy tắc sau thì việc sử dụng các hàm có sẵn là hoàn toàn có thể. Những đối tượng này sẽ được gọi là “iterables”.
Ví dụ về những quy tắc trên:
- Đối tượng chính sẽ lưu trữ dữ liệu nào đó
- Đối tượng chính phải định nghĩa một symbol "toàn cục"
symbol.iterator
cho phương thức ở quy tắc #3 và #6 - Phương thức
symbol.iterator
phải trả về một đối tượng "iterator" khác. - Đối tượng "iterator" phải có một phương thức có tên là
next
- Phương thức
next
sẽ truy cập đến dữ liệu được lưu ở quy tắc #1 6 Nếu gọiiteratorObj.next()
, sẽ trả về dữ liệu được lưu ở quy tắc #1: có thể là{value:<stored data>, done: false}
nếu vẫn còn dữ liệu và{done: true}
nếu đã hết dữ liệu. Nếu 6 quy tắc trên đươc áp dụng thì đối tượng chính được gọi là một “iterable” từ quy tắc #1. Các đối tượng được trả về được gọi là “iterator”.
Xem xét cách triển khai 6 quy tắc trên:
Chú ý quan trọng: Khi sử dụng vòng lặp for-of
hoặc toán tử spread trên một iterable
, chúng sẽ ngầm định gọi đến <iterable>[Symbol.iterator]()
để đọc iterator và sử dụng iterator để trích xuất dữ liệu.
Những quy tắc trên được coi là một cách tiêu chuẩn để trả về một đối tượng interator
** Lược dịch **
rajaraodv, JavaScript Symbols, Iterators, Generators, Async/Await, and Async Iterators — All Explained Simply, Medium
All rights reserved