- vừa được xem lúc

Vài ghi chép về Iterator trong JavaScript

0 0 16

Người đăng: Huy Tran

Theo The Full Snack

Vài ghi chép về Iterator trong JavaScript

Một phút dành cho việc tra từ điển:

  • iterate: make repeated use of a mathematical or computational procedure, applying it each time to the result of the previous application; perform iteration - là từ chỉ một hành động lặp đi lặp lại từng lượt trong một quá trình xử lý/tính toán nào đó.
  • iteration: là từ dùng để nói đến mỗi lần iterate.
  • iterable: là từ dùng để nói đến khả năng iterate của một đối tượng nào đó.

Kể từ sau đây thì mình sẽ dùng những chữ này mà không dịch ra tiếng Việt nữa, lý do là dịch ra thì viết dài dòng, bài đã dài rồi, bớt được gì thì bớt lại chút.


Trong JavaScript, vòng lặp for...of có tác dụng iterate qua nội dung của các đối tượng iterable (iterable objects).

const text = "hello"; // String là một iterable object
for (const c of text) { console.log(c);
}
// Output: "h" "e" "l" "l" "o"

Một đối tượng được coi là iterable nếu như nó thõa mãn iterable protocol, theo đó, nó phải được implement phương thức @@iterator, hay còn có thể ghi là Symbol.iterator, như này:

a["@@iterator"] = ...
// hoặc
a[Symbol.iterator] = ...

Hồi xưa các cụ phải xài "@@iterator"Symbol chưa được support rộng rãi, tuy nhiên đến bây giờ thì hầu như ai cũng đã hỗ trợ Symbol, tất nhiên là trừ IE ra.

trên thực tế, chúng ta chỉ cần implement hàm words() như này:

String.prototype.words = function() { return this.split(/\s/);
}

Ví dụ: Python's range function

OK, giờ lấy ví dụ khác nhé.

Hẳn các bạn ai cũng đã từng dùng qua Python, hay cũng từng gặp hàm range() hay xrange() của Python. Nếu như các bạn không biết nó là gì, thì nói đơn giản, hàm này tạo ra Đây chỉ là một dạng đơn giản của hàm range(), trên thực tế hàm này còn có nhiều cách sử dụng khác nữa. một dãy số trong phạm vi của hai giá trị đầu vào, ví dụ range(5, 15) sẽ tạo ra dãy số [5, 6, 7,... 14].

Chúng ta sẽ mô phỏng hàm này trong JavaScript bằng iterator.

Hàm range() của chúng ta sẽ nhận vào hai tham số, fromto. Với mỗi lượt iterate của hàm next(), chúng ta sẽ tăng giá trị from lên một đơn vị cho tới khinào nó đụng giá trị to.

const range = (from, to) => { return { [Symbol.iterator]: function() { return this; }, next: function() { if (from < to) { from++; return { value: from - 1, done: false }; } return { done: true }; } }
};

Ở đây chúng ta cũng sử dụng cú pháp implement kiểu rút gọn như đã nói ở phần trước chứ không cần tạo class, vì ở ví dụ này cũng không phức tạp gì mấy, và mục đích của chúng ta là sử dụng hàm range() luôn nên cũng không cần quan tâm đến chuyện reuse code làm gì.

Và đây là cách sử dụng:

for (const i of range(0, 100)) { console.log(i);
}

Nếu để ý kĩ cách implement trên, có thể các bạn sẽ nhận ra là, hàm range() chỉ sinh ra giá trị tiếp theo chỉ khi nào nó được gọi, chứ không phải tạo sẵn ra luôn một dãy các số từ from tới to, đó cũng chính là đặc tính "laziness" của hàm xrange() trong Python.


Ví dụ: Lazy Evaluation

Call-by-need là một đặc điểm của Lazy Evaluation (đối lập với call-by-name, đặc tính chỉ các thanh niên nhanh nhẩu, chỉ cần gọi tên là xuất kích) - một khái niệm từ functional programming, giải thích một cách đơn giản không đầy đủ, thì đây là phương pháp xử lý mà khi ta có các biểu thức nối tiếp nhau, chỉ khi nào biểu thức phía sau cần sử dụng kết quả tính toán từ biểu thức phía trước, thì biểu thức trước mới được thực thi.

Giả sử, ta có một hàm evenNumbers() trả về một dãy các số chẵn bắt đầu từ 0, và ta muốn lấy ra n giá trị đầu tiên trong dãy số đó.

const list = evenNumbers();
console.log(list.slice(0, n));

Nếu evenNumbers() là một hàm call-by-name, thì ngay từ sau câu khai báo list, hàm này sẽ được thực thi ngay, và vì evenNumbers() là một hàm không có điểm dừng, chương trình trên sẽ không bao giờ chạy được đến dòng lệnh thứ 2, và nó sẽ treo.

Nếu evenNumbers() là một hàm call-by-need, thì nó sẽ chưa được gọi chừng nào giá trị trả về của nó vẫn chưa cần thiết được sử dụng, và trong trường hợp này, nó chỉ được sử dụng sau khi chúng ta kết thúc câu lệnh slice(), lúc chúng ta in kết quả ra màn hình. Và mặc cho logic của hàm evenNumbers() chạy không có điểm dừng, vì nó là lazy, nên nó chỉ trả về một lượng dữ liệu vừa đủ dùng, trong trường hợp này là n giá trị mà thôi.

Với iterator, ta có thể mô phỏng lại hoạt động của hàm evenNumbers() call-by-need trong JavaScript kiểu như sau, tất nhiên là không hoàn toàn đúng theo mô tả ở trên:

const evenNumbers = () => { let index = 0; return { [Symbol.iterator]: function() { return this; }, next: function() { index+=2; return { value: index, done: false }; }, take: function(n) { let values = []; for (let i = 0; i < n; i++) { values.push(this.next().value); } return values; } }
}; console.log(evenNumbers().take(5));

Có thể thấy, ta có thể viết theo cách mà người bình thường không ai điên gì mà làm, đó là code logic của hàm next() không cần có điểm dừng.

Để nói kĩ hơn về Lazy Evaluation, sẽ có một bài viết khác vào một ngày nào đó.


Mặc dù khá là hữu ích, nhưng iterator hiện nay vẫn còn một nhược điểm đó là không thể thực hiện các thao tác bất đồng bộ (asynchronous) được, ví dụ, bạn muốn thực hiện HTTP request trong mỗi iteration, hoặc bạn muốn implement một iterator làm nhiệm vụ đọc một file và trả về từng byte hoặc từng line của file đó. Giải pháp thay thế thì hiện đã có proposal, đó là Async Iteration, và nó đã ở stage 4.

Bình luận