Thứ sáu, 28/02/2020 | 00:00 GMT+7

Hiểu về Trình tạo trong JavaScript

Trong ECMAScript 2015 , trình tạo đã được giới thiệu với ngôn ngữ JavaScript. Trình tạo là một quá trình có thể bị tạm dừng và tiếp tục và có thể mang lại nhiều giá trị. Một trình tạo trong JavaScript bao gồm một hàm trình tạo , hàm này trả về một đối tượng Generator thể lặp lại.

Trình tạo có thể duy trì trạng thái, cung cấp một cách hiệu quả để tạo trình vòng và có khả năng xử lý các stream dữ liệu vô hạn, được dùng để triển khai cuộn vô hạn trên giao diện user của ứng dụng web, hoạt động trên dữ liệu sóng âm thanh, v.v. Ngoài ra, khi được sử dụng với Promises , trình tạo có thể bắt chước chức năng async/await , cho phép ta xử lý mã không đồng bộ theo cách dễ đọc và đơn giản hơn. Mặc dù async/await là một cách phổ biến hơn để giải quyết các trường hợp sử dụng không đồng bộ phổ biến, đơn giản, chẳng hạn như tìm nạp dữ liệu từ API, trình tạo có nhiều tính năng nâng cao hơn giúp việc học cách sử dụng chúng trở nên đáng giá.

Trong bài viết này, ta sẽ giới thiệu cách tạo chức năng máy phát điện, làm thế nào để lặp qua Generator đối tượng, sự khác biệt giữa yieldreturn bên trong một máy phát điện, và các khía cạnh khác làm việc với máy phát điện.

Chức năng của máy phát điện

Một chức năng máy phát điện là một hàm trả về một Generator đối tượng, và được xác định bởi các function từ khóa tiếp theo là một dấu sao ( * ), như thể hiện trong những điều sau đây:

// Generator function declaration function* generatorFunction() {} 

Đôi khi, bạn sẽ thấy dấu hoa thị bên cạnh tên hàm, trái ngược với từ khóa hàm, chẳng hạn như function *generatorFunction() . Điều này hoạt động tương tự, nhưng function* là một cú pháp được chấp nhận rộng rãi hơn.

Các hàm của trình tạo cũng có thể được định nghĩa trong một biểu thức, giống như các hàm thông thường:

// Generator function expression const generatorFunction = function*() {} 

Trình tạo thậm chí có thể là phương thức của một đối tượng hoặc lớp :

// Generator as the method of an object const generatorObj = {   *generatorMethod() {}, }  // Generator as the method of a class class GeneratorClass {   *generatorMethod() {} } 

Các ví dụ trong suốt bài viết này sẽ sử dụng cú pháp khai báo hàm trình tạo.

Lưu ý : Không giống như các hàm thông thường, trình tạo không thể được xây dựng bằng từ khóa new , cũng như không thể sử dụng chúng cùng với các hàm mũi tên .

Đến đây bạn đã biết cách khai báo các hàm của trình tạo, hãy xem các đối tượng Trình Generator thể lặp lại mà chúng trả về.

Đối tượng máy phát điện

Theo truyền thống, các hàm trong JavaScript chạy đến khi hoàn thành và việc gọi một hàm sẽ trả về một giá trị khi nó đến từ khóa return . Nếu từ khóa return bị bỏ qua, một hàm sẽ mặc nhiên trả về undefined .

Ví dụ: trong đoạn mã sau, ta khai báo một hàm sum() trả về một giá trị là tổng của hai đối số nguyên:

// A regular function that sums two values function sum(a, b) {   return a + b } 

Gọi hàm trả về một giá trị là tổng của các đối số:

const value = sum(5, 6) // 11 

Tuy nhiên, một hàm trình tạo không trả về một giá trị ngay lập tức và thay vào đó trả về một đối tượng Trình Generator thể lặp lại. Trong ví dụ sau, ta khai báo một hàm và cung cấp cho nó một giá trị trả về duy nhất, giống như một hàm chuẩn:

// Declare a generator function with a single return value function* generatorFunction() {   return 'Hello, Generator!' } 

Khi ta gọi hàm trình tạo, nó sẽ trả về đối tượng Generator , đối tượng mà ta có thể gán cho một biến:

// Assign the Generator object to generator const generator = generatorFunction() 

Nếu đây là một hàm thông thường, ta mong đợi trình generator cung cấp cho ta chuỗi được trả về trong hàm. Tuy nhiên, những gì ta thực sự nhận được là một đối tượng ở trạng thái suspended . Do đó, bộ generator gọi sẽ cung cấp kết quả tương tự như sau:

Output
generatorFunction {<suspended>} __proto__: Generator [[GeneratorLocation]]: VM272:1 [[GeneratorStatus]]: "suspended" [[GeneratorFunction]]: ƒ* generatorFunction() [[GeneratorReceiver]]: Window [[Scopes]]: Scopes[3]

Đối tượng Generator trả về bởi hàm là một trình lặp . Trình lặp là một đối tượng có sẵn phương thức next() , được sử dụng để lặp qua một chuỗi giá trị. Phương thức next() trả về một đối tượng có value và các thuộc tính done . value đại diện cho value được trả về và done cho biết liệu trình lặp đã chạy qua tất cả các giá trị của nó hay chưa.

Biết được điều này, hãy gọi next() trên trình generator của ta và nhận giá trị và trạng thái hiện tại của trình vòng lặp:

// Call the next method on the Generator object generator.next() 

Điều này sẽ cho kết quả sau:

Output
{value: "Hello, Generator!", done: true}

Giá trị được trả về khi gọi next()Hello, Generator! , và trạng thái donetrue , bởi vì giá trị này đến từ một kết quả return đóng trình vòng lặp. Kể từ khi trình lặp được thực hiện, trạng thái của chức năng trình tạo sẽ thay đổi từ suspended sang closed . Gọi lại trình generator sẽ đưa ra kết quả sau:

Output
generatorFunction {<closed>}

Ngay bây giờ, ta chỉ chứng minh cách một hàm trình tạo có thể là một cách phức tạp hơn để nhận giá trị return của một hàm. Nhưng các chức năng của máy phát điện cũng có những đặc điểm riêng biệt để phân biệt chúng với các chức năng bình thường. Trong phần tiếp theo, ta sẽ tìm hiểu về toán tử yield và xem cách trình tạo có thể tạm dừng và tiếp tục thực thi.

Điều hành yield

Trình tạo giới thiệu một từ khóa mới cho JavaScript: yield . yield có thể tạm dừng một chức năng của trình tạo và trả về giá trị theo sau yield , cung cấp một cách nhẹ nhàng để lặp qua các giá trị.

Trong ví dụ này, ta sẽ tạm dừng hàm trình tạo ba lần với các giá trị khác nhau và trả về một giá trị ở cuối. Sau đó, ta sẽ gán đối tượng Generator của ta cho biến generator .

// Create a generator function with multiple yields function* generatorFunction() {   yield 'Neo'   yield 'Morpheus'   yield 'Trinity'    return 'The Oracle' }  const generator = generatorFunction() 

Bây giờ, khi ta gọi next() trên hàm trình tạo, nó sẽ tạm dừng mỗi khi gặp phải yield . done sẽ được đặt thành false sau mỗi lần yield , cho biết rằng trình tạo chưa kết thúc. Một khi nó gặp phải return hoặc không có thêm yield gặp phải trong hàm, done sẽ chuyển thành true và trình tạo sẽ kết thúc.

Sử dụng phương thức next() bốn lần liên tiếp:

// Call next four times generator.next() generator.next() generator.next() generator.next() 

Chúng sẽ cung cấp bốn dòng kết quả sau theo thứ tự:

Output
{value: "Neo", done: false} {value: "Morpheus", done: false} {value: "Trinity", done: false} {value: "The Oracle", done: true}

Lưu ý trình tạo không yêu cầu return ; nếu bị bỏ qua, lần lặp cuối cùng sẽ trả về {value: undefined, done: true} , cũng như bất kỳ lệnh gọi tiếp theo nào tới next() sau khi trình tạo đã hoàn thành.

Lặp lại trên một máy phát điện

Sử dụng phương thức next() , ta đã lặp lại thủ công thông qua đối tượng Generator , nhận tất cả value và các thuộc tính đã done của đối tượng đầy đủ. Tuy nhiên, cũng giống nhưArray , MapSet , Trình Generator tuân theo giao thức lặp và có thể được lặp lại bằng for...of :

// Iterate over Generator object for (const value of generator) {   console.log(value) } 

Kết quả sẽ trả về như sau:

Output
Neo Morpheus Trinity

Toán tử spread cũng được dùng để gán các giá trị của Generator cho một mảng.

// Create an array from the values of a Generator object const values = [...generator]  console.log(values) 

Điều này sẽ cung cấp cho mảng sau:

Output
(3) ["Neo", "Morpheus", "Trinity"]

Cả spread và for...of sẽ không tính đến giá trị return (trong trường hợp này, nó sẽ là 'The Oracle' ).

Lưu ý : Mặc dù cả hai phương pháp này đều hiệu quả khi làm việc với trình tạo hữu hạn, nhưng nếu trình tạo đang xử lý stream dữ liệu vô hạn, sẽ không thể sử dụng spread hoặc for...of trực tiếp mà không tạo vòng lặp vô hạn.

Đóng máy phát điện

Như ta đã thấy, một trình tạo có thể đặt thuộc tính done của nó thành true và trạng thái của nó được đặt thành closed bằng cách lặp lại tất cả các giá trị của nó. Có hai cách bổ sung để hủy ngay trình tạo: với phương thức return() và với phương thức throw() .

Với return() , trình tạo có thể được kết thúc tại bất kỳ thời điểm nào, giống như khi một câu lệnh return nằm trong thân hàm. Bạn có thể chuyển một đối số vào return() hoặc để trống nó cho một giá trị không xác định.

Để chứng minh return() , ta sẽ tạo một trình tạo với một vài giá trị yield nhưng không có return trong định nghĩa hàm:

function* generatorFunction() {   yield 'Neo'   yield 'Morpheus'   yield 'Trinity' }  const generator = generatorFunction() 

next() đầu tiên next() sẽ cung cấp cho ta 'Neo' , với done đặt thành false . Nếu ta gọi một phương thức return() trên đối tượng Generator ngay sau đó, bây giờ ta sẽ nhận được giá trị đã truyền và done đặt thành true . Bất kỳ lệnh gọi bổ sung nào tới next() sẽ cung cấp phản hồi trình tạo đã hoàn thành mặc định với giá trị không xác định.

Để chứng minh điều này, hãy chạy ba phương pháp sau trên trình generator :

generator.next() generator.return('There is no spoon!') generator.next() 

Điều này sẽ cho ba kết quả sau:

Output
{value: "Neo", done: false} {value: "There is no spoon!", done: true} {value: undefined, done: true}

Phương thức return() buộc đối tượng Generator phải hoàn thành và bỏ qua bất kỳ từ khóa yield nào khác. Điều này đặc biệt hữu ích trong lập trình không đồng bộ khi bạn cần làm cho các chức năng có thể hủy được, chẳng hạn như ngắt một yêu cầu web khi user muốn thực hiện một hành động khác, vì không thể hủy trực tiếp một Lời hứa.

Nếu phần thân của hàm trình tạo có cách để bắt và xử lý lỗi, bạn có thể sử dụng phương thức throw() để đưa lỗi vào trình tạo. Thao tác này khởi động trình tạo, xử lý lỗi và kết thúc trình tạo.

Để chứng minh điều này, ta sẽ try...catch bên trong thân hàm của trình tạo và ghi lại lỗi nếu tìm thấy lỗi:

// Define a generator function with a try...catch function* generatorFunction() {   try {     yield 'Neo'     yield 'Morpheus'   } catch (error) {     console.log(error)   } }  // Invoke the generator and throw an error const generator = generatorFunction() 

Bây giờ, ta sẽ chạy phương thức next() , theo sau là throw() :

generator.next() generator.throw(new Error('Agent Smith!')) 

Điều này sẽ cho kết quả sau:

Output
{value: "Neo", done: false} Error: Agent Smith! {value: undefined, done: true}

Bằng cách sử dụng throw() , ta đã đưa một lỗi vào trình tạo, lỗi này đã bị bắt bởi try...catch và đăng nhập vào console .

Phương pháp và trạng thái đối tượng của Generator

Bảng sau đây hiển thị danh sách các phương thức được dùng trên các đối tượng Generator :

phương pháp Sự miêu tả
next() Trả về giá trị tiếp theo trong trình tạo
return() Trả về một giá trị trong trình tạo và kết thúc trình tạo
throw() Ném lỗi và kết thúc trình tạo

Bảng tiếp theo liệt kê các trạng thái có thể có của đối tượng Generator :

Trạng thái Sự miêu tả
suspended Máy phát điện đã tạm dừng thực thi nhưng chưa kết thúc
closed Trình tạo đã kết thúc do gặp lỗi, trả về hoặc lặp lại qua tất cả các giá trị

ủy quyền yield nhịn

Ngoài toán tử yield thông thường, trình tạo cũng có thể sử dụng biểu thức yield* để ủy quyền các giá trị khác cho trình tạo khác. Khi gặp phải yield* bên trong trình tạo, nó sẽ đi vào bên trong trình tạo được ủy quyền và bắt đầu lặp lại tất cả các yield cho đến khi đóng trình tạo đó. Điều này được dùng để tách các chức năng của trình tạo khác nhau để tổ chức mã của bạn theo ngữ nghĩa, trong khi vẫn có thể lặp lại tất cả yield của chúng theo đúng thứ tự.

Để chứng minh, ta có thể tạo hai hàm trình tạo, một trong số đó sẽ yield* hoạt động trên hàm kia:

// Generator function that will be delegated to function* delegate() {   yield 3   yield 4 }  // Outer generator function function* begin() {   yield 1   yield 2   yield* delegate() } 

Tiếp theo, hãy lặp lại qua hàm tạo begin() :

// Iterate through the outer generator const generator = begin()  for (const value of generator) {   console.log(value) } 

Điều này sẽ cung cấp các giá trị sau theo thứ tự chúng được tạo:

Output
1 2 3 4

Bộ tạo bên ngoài mang lại giá trị 12 , sau đó được ủy quyền cho bộ tạo khác với yield* , trả về 34 .

yield* cũng có thể ủy quyền cho bất kỳ đối tượng nào có thể lặp lại, chẳng hạn như Mảng hoặc Bản đồ. Ủy quyền lợi nhuận có thể hữu ích trong việc tổ chức mã, vì bất kỳ hàm nào trong trình tạo mà muốn sử dụng yield cũng sẽ phải là một trình tạo.

Luồng dữ liệu vô hạn

Một trong những khía cạnh hữu ích của máy phát điện là khả năng làm việc với các stream và tập hợp dữ liệu vô hạn. Điều này có thể được chứng minh bằng cách tạo một vòng lặp vô hạn bên trong một hàm trình tạo tăng từng số một.

Trong khối mã sau, ta xác định hàm trình tạo này và sau đó chạy trình tạo:

// Define a generator function that increments by one function* incrementer() {   let i = 0    while (true) {     yield i++   } }  // Initiate the generator const counter = incrementer() 

Bây giờ, hãy lặp lại các giá trị bằng cách sử dụng next() :

// Iterate through the values counter.next() counter.next() counter.next() counter.next() 

Điều này sẽ cho kết quả sau:

Output
{value: 0, done: false} {value: 1, done: false} {value: 2, done: false} {value: 3, done: false}

Hàm trả về các giá trị liên tiếp trong vòng lặp vô hạn trong khi thuộc tính done vẫn là false , đảm bảo nó sẽ không kết thúc.

Với trình tạo, bạn không phải lo lắng về việc tạo vòng lặp vô hạn, vì bạn có thể tạm dừng và tiếp tục thực thi theo ý muốn. Tuy nhiên, bạn vẫn phải thận trọng với cách bạn gọi trình tạo. Nếu bạn sử dụng spread hoặc for...of trên một stream dữ liệu vô hạn, bạn sẽ vẫn lặp lại một vòng lặp vô hạn cùng một lúc, điều này sẽ gây lỗi môi trường.

Đối với một ví dụ phức tạp hơn về stream dữ liệu vô hạn, ta có thể tạo một hàm tạo Fibonacci. Chuỗi Fibonacci, liên tục cộng hai giá trị trước đó với nhau, có thể được viết bằng cách sử dụng vòng lặp vô hạn trong trình tạo như sau:

// Create a fibonacci generator function function* fibonacci() {   let prev = 0   let next = 1    yield prev   yield next    // Add previous and next values and yield them forever   while (true) {     const newVal = next + prev      yield newVal      prev = next     next = newVal   } } 

Để kiểm tra điều này, ta có thể lặp qua một số hữu hạn và in dãy Fibonacci ra console .

// Print the first 10 values of fibonacci const fib = fibonacci()  for (let i = 0; i < 10; i++) {   console.log(fib.next().value) } 

Điều này sẽ cung cấp những điều sau:

Output
0 1 1 2 3 5 8 13 21 34

Khả năng làm việc với các tập dữ liệu vô hạn là một phần làm cho các máy phát điện trở nên mạnh mẽ. Điều này có thể hữu ích cho các ví dụ như triển khai cuộn vô hạn trên giao diện user của ứng dụng web.

Chuyển giá trị trong máy phát điện

Trong suốt bài viết này, ta đã sử dụng trình tạo làm trình vòng lặp và ta đã mang lại các giá trị trong mỗi lần lặp. Ngoài việc tạo ra các giá trị, máy phát điện cũng có thể sử dụng các giá trị từ next() . Trong trường hợp này, yield sẽ chứa một giá trị.

Điều quan trọng cần lưu ý là next() đầu tiên được gọi sẽ không chuyển một giá trị mà chỉ khởi động trình tạo. Để chứng minh điều này, ta có thể ghi giá trị của yield và gọi next() một vài lần với một số giá trị.

function* generatorFunction() {   console.log(yield)   console.log(yield)    return 'The end' }  const generator = generatorFunction()  generator.next() generator.next(100) generator.next(200) 

Điều này sẽ cho kết quả sau:

Output
100 200 {value: "The end", done: true}

Cũng có thể khởi tạo bộ tạo với giá trị ban đầu. Trong ví dụ sau, ta sẽ tạo một vòng lặp for và chuyển từng giá trị vào phương thức next() , nhưng cũng truyền một đối số cho hàm ban đầu:

function* generatorFunction(value) {   while (true) {     value = yield value * 10   } }  // Initiate a generator and seed it with an initial value const generator = generatorFunction(0)  for (let i = 0; i < 5; i++) {   console.log(generator.next(i).value) } 

Ta sẽ truy xuất giá trị từ next() và mang lại một giá trị mới cho lần lặp tiếp theo, giá trị này nhân với giá trị trước đó gấp 10 lần. Điều này sẽ cung cấp những điều sau:

Output
0 10 20 30 40

Một cách khác để giải quyết việc khởi động trình tạo là quấn trình tạo trong một hàm sẽ luôn gọi next() một lần trước khi làm bất cứ điều gì khác.

async / await với Trình tạo

Hàm không đồng bộ là một loại hàm có sẵn trong ES6 + JavaScript giúp làm việc với dữ liệu không đồng bộ dễ hiểu hơn bằng cách làm cho nó có vẻ đồng bộ. Các trình tạo có một loạt các khả năng mở rộng hơn các chức năng không đồng bộ, nhưng có khả năng sao chép các hành vi tương tự. Thực hiện lập trình không đồng bộ theo cách này có thể tăng tính linh hoạt cho mã của bạn.

Trong phần này, ta sẽ trình bày một ví dụ về tái tạo async / await với máy phát điện.

Hãy xây dựng một hàm không đồng bộ sử dụng API Tìm nạp để lấy dữ liệu từ API JSONPlaceholder (cung cấp dữ liệu JSON mẫu cho mục đích thử nghiệm) và ghi lại phản hồi vào console .

Bắt đầu bằng cách xác định một hàm không đồng bộ được gọi là getUsers tìm nạp dữ liệu từ API và trả về một mảng đối tượng, sau đó gọi getUsers :

const getUsers = async function() {   const response = await fetch('https://jsonplaceholder.typicode.com/users')   const json = await response.json()    return json }  // Call the getUsers function and log the response getUsers().then(response => console.log(response)) 

Điều này sẽ cung cấp dữ liệu JSON tương tự như sau:

Output
[ {id: 1, name: "Leanne Graham" ...}, {id: 2, name: "Ervin Howell" ...}, {id: 3, name": "Clementine Bauch" ...}, {id: 4, name: "Patricia Lebsack"...}, {id: 5, name: "Chelsey Dietrich"...}, ...]

Sử dụng trình tạo, ta có thể tạo ra một thứ gì đó gần như giống hệt nhau mà không sử dụng từ khóa async / await . Thay vào đó, nó sẽ sử dụng một chức năng mới mà ta tạo ra và yield giá trị thay vì những lời hứa await .

Trong khối mã sau, ta xác định một hàm được gọi là getUsers sử dụng hàm asyncAlt mới của ta (mà ta sẽ viết sau) để bắt chước async / await .

const getUsers = asyncAlt(function*() {   const response = yield fetch('https://jsonplaceholder.typicode.com/users')   const json = yield response.json()    return json })  // Invoking the function getUsers().then(response => console.log(response)) 

Như ta có thể thấy, nó trông gần giống với việc triển khai async / await , ngoại trừ việc có một hàm trình tạo được truyền vào đó tạo ra các giá trị.

Bây giờ ta có thể tạo một hàm asyncAlt tương tự như một hàm không đồng bộ. asyncAlt có một hàm tạo dưới dạng một tham số, đây là hàm của ta để mang lại các hứa hẹn mà fetch trả về. asyncAlt trả về chính một hàm và giải quyết mọi lời hứa mà nó tìm thấy cho đến lời hứa cuối cùng:

// Define a function named asyncAlt that takes a generator function as an argument function asyncAlt(generatorFunction) {   // Return a function   return function() {     // Create and assign the generator object     const generator = generatorFunction()      // Define a function that accepts the next iteration of the generator     function resolve(next) {       // If the generator is closed and there are no more values to yield,       // resolve the last value       if (next.done) {         return Promise.resolve(next.value)       }        // If there are still values to yield, they are promises and       // must be resolved.       return Promise.resolve(next.value).then(response => {         return resolve(generator.next(response))       })     }      // Begin resolving promises     return resolve(generator.next())   } } 

Điều này sẽ cung cấp kết quả tương tự như version async / await :

Output
[ {id: 1, name: "Leanne Graham" ...}, {id: 2, name: "Ervin Howell" ...}, {id: 3, name": "Clementine Bauch" ...}, {id: 4, name: "Patricia Lebsack"...}, {id: 5, name: "Chelsey Dietrich"...}, ...]

Lưu ý việc triển khai này là để chứng minh cách sử dụng máy phát điện thay cho async / await và không phải là thiết kế sẵn sàng production . Nó không có cài đặt xử lý lỗi, cũng như không có khả năng chuyển các tham số vào các giá trị được tạo ra. Mặc dù phương pháp này có thể thêm tính linh hoạt cho mã của bạn, nhưng thường async/await sẽ là lựa chọn tốt hơn, vì nó tóm tắt các chi tiết triển khai và cho phép bạn tập trung vào việc viết mã hiệu quả.

Kết luận

Trình tạo là các quy trình có thể tạm dừng và tiếp tục thực thi. Chúng là một tính năng mạnh mẽ, linh hoạt của JavaScript, mặc dù chúng không được sử dụng phổ biến. Trong hướng dẫn này, ta đã tìm hiểu về các hàm của trình tạo và đối tượng của trình tạo, các phương thức có sẵn cho trình tạo, toán tử yieldyield* và trình tạo được sử dụng với các tập dữ liệu hữu hạn và vô hạn. Ta cũng đã khám phá một cách để triển khai mã không đồng bộ mà không có lệnh gọi lại lồng nhau hoặc chuỗi lời hứa dài.

Nếu bạn muốn tìm hiểu thêm về cú pháp JavaScript, hãy xem phần Hướng dẫn Hiểu Điều này, Ràng buộc, Gọi và Áp dụng trong JavaScriptHiểu về Bản đồ và Đặt Đối tượng trong JavaScript của ta .


Tags:

Các tin liên quan

Triển khai Thành phần Tab từ Scratch trong Vanilla JavaScript
2020-02-24
Khám phá cây qua JavaScript
2020-02-23
Giới thiệu về danh sách được liên kết qua JavaScript - Phần 2: Triển khai
2020-02-23
Khám phá các và hàng đợi qua JavaScript
2020-02-23
Câu hỏi phỏng vấn JavaScript: Gotchas phổ biến
2020-02-21
Giới thiệu về danh sách được liên kết qua JavaScript - Phần 1: Tổng quan
2020-02-21
Hiểu Radix Sắp xếp Thông qua JavaScript
2020-02-18
Cách xây dựng PWA trong Vanilla JavaScript
2020-02-17
Hiểu về Sắp xếp nhanh qua JavaScript
2020-02-14
Hiểu bản đồ và thiết lập đối tượng trong JavaScript
2020-02-12