JavaScript: JavaScript Generators
Giới thiệu về JavaScript Generators
Trong JavaScript, một hàm thông thường được thực thi dựa trên mô hình run-to-completion (chạy cho đến khi hoàn thành). Nó không thể tạm dừng giữa chừng và tiếp tục từ nơi nó đã tạm dừng. Ví dụ:
function foo() { console.log('I'); console.log('cannot'); console.log('pause'); }
Trong JavaScript, hàm foo() được thực thi từ đầu đến cuối. Cách duy nhất để thoát khỏi hàm foo() là bằng cách trả về kết quả từ bên trong nó hoặc throw một lỗi. Nếu bạn gọi lại hàm foo(), nó sẽ bắt đầu thực thi từ đầu đến cuối.
foo();
Output:
I cannot pause
ES6 giới thiệu một loại hàm mới khác với hàm thông thường: hàm generator.
Một generator có thể tạm dừng giữa chừng và sau đó tiếp tục từ nơi nó tạm dừng. Ví dụ:
function* generate() { console.log('invoked 1st time'); yield 1; console.log('invoked 2nd time'); yield 2; }
Giải thích:
- Đầu tiên, bạn nhìn thấy dấu hoa thị (
*
) sau từ khóa function. Dấu hoa thị biểu thị rằng đâygenerate()
là một hàm generator chứ không phải một hàm bình thường. - Thứ hai, câu lệnh yield trả về một giá trị và tạm dừng việc thực thi hàm.
Đoạn mã sau gọi generator generate():
let gen = generate();
Khi bạn gọi generator generate()
:
- Đầu tiên, bạn không thấy gì trong console. Nếu
generate()
là một hàm thông thường, bạn sẽ thấy một số kết quả hiện lên trên console. - Thứ hai, bạn nhận được thứ gì đó là giá trị được trả về khi gọi generate().
Hãy hiển thị giá trị trả về đó trên console:
console.log(gen);
Output:
Object [Generator] {}
Vì vậy, một generator trả về một đối tượng Generator mà không thực thi phần thân của nó khi nó được gọi.
Đối tượng Generator trả về một đối tượng khác có hai thuộc tính: done
và value
. Nói cách khác, một đối tượng Generator có thể được lặp qua (iterable).
Đoãn mã sau đây gọi phương thức next() trên đối tượng Generator:
let result = gen.next(); console.log(result);
Output:
invoked 1st time { value: 1, done: false }
Như bạn có thể thấy, đối tượng Generator thực thi phần thân của nó để xuất thông báo 'invoked 1st time'
ở dòng 1 và trả về giá trị 1 ở dòng 2.
Như vậy Câu lệnh yield
trả về 1 và tạm dừng generator ở dòng 2.
Tương tự, đoạn mã sau gọi phương thức next()
của dối tượng Generator lần thứ hai:
result = gen.next(); console.log(result);
Output:
invoked 2nd time { value: 2, done: false }
Lần này Generator tiếp tục thực hiện từ dòng 3 xuất ra thông báo 'invoked 2nd time'
và trả về 2.
Tiếp tục gọi phương thức next() của đối tượng generator lần thứ ba, kết quả sẽ là:
{ value: undefined, done: true }
Vì generator có thể lặp lại (iterable) nên bạn có thể sử dụng vòng lặp for...of:
for (const g of gen) { console.log(g); }
Output:
invoked 1st time 1 invoked 2nd time 2
Thêm ví dụ về JavaScript generator
Ví dụ sau minh họa cách sử dụng trình tạo để tạo một đoạn số không bao giờ kết thúc:
function* forever() { let index = 0; while (true) { yield index++; } } let f = forever(); console.log(f.next()); // 0 console.log(f.next()); // 1 console.log(f.next()); // 2
Mỗi lần bạn gọi phương thức next() của forever
generator, nó sẽ trả về số tiếp theo của đoạn số, bắt đầu từ 0.
Sử dụng generator để triển khai các iterators
Khi bạn triển khai một iterator, bạn phải xác định phương thức next() theo cách thủ công. Trong phương thức next() này, bạn cũng phải lưu thủ công trạng thái của phần tử hiện tại.
Vì các iterator có thể lặp lại nên chúng có thể giúp bạn đơn giản hóa mã để triển khai trình iterator dễ hơn.
Sau đây là một iterator Sequence được tạo trong hướng dẫn về Iterators
class Sequence { constructor( start = 0, end = Infinity, interval = 1 ) { this.start = start; this.end = end; this.interval = interval; } [Symbol.iterator]() { let counter = 0; let nextIndex = this.start; return { next: () => { if ( nextIndex < this.end ) { let result = { value: nextIndex, done: false } nextIndex += this.interval; counter++; return result; } return { value: counter, done: true }; } } } }
Và đây là iterator Sequence mới sử dụng generator:
class Sequence { constructor( start = 0, end = Infinity, interval = 1 ) { this.start = start; this.end = end; this.interval = interval; } * [Symbol.iterator]() { for( let index = this.start; index <= this.end; index += this.interval ) { yield index; } } }
Như bạn có thể thấy, phương thức Symbol.iterator đơn giản hơn nhiều bằng cách sử dụng generator.
Đoạn mã sau đây sử dụng Sequence iterator để tạo một chuỗi các số lẻ từ 1 đến 10:
let oddNumbers = new Sequence(1, 10, 2); for (const num of oddNumbers) { console.log(num); }
Output:
1 3 5 7 9
Sử dụng generator để triển khai cấu trúc dữ liệu dạng Bag
Bag là cấu trúc dữ liệu có khả năng lưu lại các phần tử và lặp qua các phần tử. Nó không hỗ trợ loại bỏ các phần tử.
Đoạn mã sau đây triển khai cấu trúc dữ liệu Bag:
class Bag { constructor() { this.elements = []; } isEmpty() { return this.elements.length === 0; } add(element) { this.elements.push(element); } * [Symbol.iterator]() { for (let element of this.elements) { yield element; } } } let bag = new Bag(); bag.add(1); bag.add(2); bag.add(3); for (let e of bag) { console.log(e); }
Output:
1 2 3
Tổng kết
- Generator được tạo bởi hàm tạo
function* f(){}
. - Generator không thực thi phần thân của nó ngay lập tức khi chúng được gọi.
- Generator có thể tạm dừng giữa chừng và tiếp tục thực thi ở nơi chúng đã bị tạm dừng. Câu lệnh yield tạm dừng quá trình thực thi của generator và trả về một giá trị.
- Generator có thể lặp lại nên bạn có thể sử dụng chúng với vòng lặp for...of.