Tìm hiểu về Meta programming trong Javascript

1044
Tìm hiểu về Meta programming trong Javascript

Người viết: Nguyen Thanh Tung B

Mở đầu

Trong lập trình chúng ta có thể chia ra 2 mức độ:

  • Base level: code xử lí những dữ liệu mà user đưa vào và đưa ra kết quả.
  • Meta level: code để xử lí những base-level code ở trên.

Thuật ngữ meta-programming thì lần đầu tiên mình nghe thấy là trong ngôn ngữ lập trình ruby. Nói 1 cách dễ hiểu là tư tưởng code sinh ra code. Lần này mình tò mò xem trong Javascript thì meta-programming nó như thế nào.

Hãy xem xét 1 ví dụ đơn giản:

Đoạn code trên là 1 ví dụ về meta programming với base programming là ngôn ngữ java còn meta programming bằng javascript (ngôn ngữ của base programming và meta programming có thể khác nhau).

Tuy nhiên các ví dụ trong bài viết này sẽ tập trung vào trường hợp base programming và meta programming đều là ngôn ngữ Javascript.

Ví dụ đầu tiên khá trực quan và dễ hiểu về meta programming. Tuy nhiên, có những lúc các xử lí trông không có vẻ giống meta programming trên thực tế lại đang làm nhiệm vụ của meta programming.

Do sự mập mờ giữa programming constructs và data structures trong Javascript mà đoạn code trên trông không giống meta programming nhưng trên thực tế bản thân chương trình đã thực thi cấu trúc dữ liệu của nó trong quá trình chạy nên có thể coi đó là 1 kiểu meta programming.

Các kiểu meta programming

Có thể chia meta programming ra làm 3 loại:

  • Introspection: chỉ truy cập để đọc cấu trúc chương trình.
  • Self-modification: thay đổi cấu trúc.
  • Intercession: thay đổi ngữ nghĩa 1 số toán tử của ngôn ngữ lập trình.

Ví dụ thứ 2 trong phần mở đầu chính là ví dụ cho loại Introspection mà cụ thể hơn là lời gọi Object.keys() đã thực hiện việc truy cập cấu trúc.

Trong ES6 thì Javascript cung cấp Proxy để có thể tùy chỉnh các toán tử được thực hiện trong object và đây chính là 1 trong những feature của metaprogramming.

Các ví dụ tiếp dưới sẽ tập trung vào khai thác tính năng của Proxy.

Proxy

Proxy làm tác vụ gì

Proxy được tạo ra với 2 tham số đầu vào là handler và target.

Target chính là object mà chúng ta sẽ thực hiện việc customize các toán tử, còn handler có thể coi như nơi cho phép chúng ta định nghĩa việc customize như thế nào, nơi chúng ta viết code can thiệp vào tác vụ của toán tử. Nhưng method can thiệp này được gọi là trap.

Toán tử in của Javascript sẽ trigger has trong handler còn các lời gọi truy cập thuộc tính của object sẽ trigger get trong handler (tên của các trigger get và has là do Proxy quy ước sẵn).

Sau khi wrap object với handler thì mỗi khi thực hiện 1 toán tử nào đó thì trigger tương ứng trong handler sẽ được kích hoạt.

Trong ví dụ này chúng ta đơn giản là thêm log và set kết quả trả về về 1 giá trị cố định (thực tế thì việc này sẽ khá nguy hiểm bởi như vậy thì object sẽ xác nhận là có mọi thuộc tính).

Function-specific traps

Nếu target của proxy là 1 function, có 2 toán tử mà chúng ta có thể can thiệp vào:

  • apply: thực hiện function call, được trigger khi thực hiện:
    • proxy(…)
    • proxy.call(…)
    • proxy.apply(…)
  • construct: thực hiện constructor call được trigger khi gọi:
    • new proxy(…)

Can thiệp vào method calls

Nếu bạn muốn can thiệp vào method call, bạn cần can thiệp vào 2 quá trình:

  • get để lấy thông tin về cấu trúc của method.
  • apply để thực hiện lời gọi method.

Dưới đây sẽ là ví dụ về việc can thiệp vào function call.

Trước tiên chúng ta có 1 object với 2 function multiply và squared:

Bây giờ công việc sẽ là viết 1 proxy để can thiệp vào quá trình gọi 2 function này, xuất ra trace log khi thực hiện function.

Kết quả:

Hãy phân tích 1 chút function traceMethodCalls. Khi gọi function multiply hay squared đầu tiên chúng ta sẽ phải chạy qua get trong handler để lấy thông tin về function.

Đoạn code định nghĩa 2 function multiply và squared nếu convert sang ES5 sẽ như sau:

Như vậy có thể thấy multiply và squared chình là thuộc tính của obj.

Trong ví dụ này tham số target chính là obj còn popKey chình là tên của 2 function multiply và squaredtarget[propKey] sẽ trả về cho chúng ta function cần gọi và lưu lại vào origMethod.

Để function multiply và squared hoạt động bình thường thì ta cần trả về kết quả sau khi apply origMethod với các tham số truyền vào.

Tại bước này chúng ta sẽ thêm trace log cho function và kết quả thu được là trace log được ghi ra mối khi function được gọi.

Kĩ thuật forward với proxy

Giả sử chúng ta muốn can thiệp vào các toán tử in hay delete trong Javascript bằng cách sử dụng các trap hasdeleteProperty trong handler như sau.

Đây là 1 kiểu can thiệp hay được sử dụng, đó là thay vì thay đổi hẳn chức năng của 1 toán tử nào đó thường thì chúng ta muồn bổ sung thêm tính năng (ví dụ như ghi lại log) và giữ nguyên kết quả trả về của các toán tử.

Do đó trong các trap deleteProperty và has, lần lượt chúng ta phải gọi return delete target[propKey]; cũng như return propKey in target; để kết quả trả về sau khi chạy qua trap trong handler không thay đổi so với kết quả thông thường.

ES6 cung cấp Reflect để làm điều này đơn giản hơn.

Khi sử dụng Reflect đoạn code sẽ trở thành thê này:

Và sau khi rút gọn do cách viết các trap là tương tự nhau:

Ứng dụng của proxy

Proxy có thể ứng dụng vào nhiều tình huống để giúp việc lập trình dễ dàng hơn:

  • Trace các thuộc tính được truy cập.
  • Cảnh bào, đưa ra exception cho việc truy cập các thuộc tính không được định nghĩa của object.
  • Mở rộng phạm vi tính toán của toán tử (ví dụ cho phép sử dụng chỉ số âm trong mảng khi gọi [] thay vì phải sử dụng method khác của Javascript).

Kết luận

Như vậy là mình đã giới thiệu cơ bản về meta programming trong JavaScript như thế nào. Hy vọng bài viết sẽ hữu ích với các bạn.

Cảm ơn các bạn đã theo dõi bài viết!

Có thể bạn quan tâm:

Xem thêm việc làm JavaScript Developer trên TopDev

TopDev via viblo.asia

  Nhìn qua một vài điểm về Javascript ES2019
  Method Chaining trong JavaScript là gì