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 ...
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:
1 2 3 4 |
const str = 'Hello' + '!'.repeat(3); console.log('System.out.println("'+str+'")'); |
Đ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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Base level const obj = { hello() { console.log('Hello!'); } }; // Meta level for (const key of Object.keys(obj)) { console.log(key); } => hello |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const target = {}; const handler = { /** Intercepts: getting properties */ get(target, propKey, receiver) { console.log(`GET ${propKey}`); return 123; }, /** Intercepts: checking whether properties exist */ has(target, propKey) { console.log(`HAS ${propKey}`); return true; } }; const proxy = new Proxy(target, handler); |
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).
1 2 3 4 5 6 7 8 9 10 |
proxy.foo => GET foo 123 'hello' in proxy => HAS hello true |
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:
1 2 3 4 5 6 7 8 9 10 |
const obj = { multiply(x, y) { return x * y; }, squared(x) { return this.multiply(x, x); }, }; |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function traceMethodCalls(obj) { const handler = { get(target, propKey, receiver) { const origMethod = target[propKey]; return function (...args) { const result = origMethod.apply(this, args); console.log(propKey + JSON.stringify(args) + ' -> ' + JSON.stringify(result)); return result; }; } }; return new Proxy(obj, handler); } |
Kết quả:
1 2 3 4 5 6 7 8 9 10 11 12 |
const tracedObj = traceMethodCalls(obj); tracedObj.multiply(2,7) => multiply[2,7] -> 14 14 tracedObj.squared(9) => multiply[9,9] -> 81 squared[9] -> 81 81 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
"use strict"; var obj = { multiply: function multiply(x, y) { return x * y; }, squared: function squared(x) { return this.multiply(x, x); } }; |
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à squared. target[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 has và deleteProperty trong handler như sau.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const handler = { deleteProperty(target, propKey) { console.log('DELETE ' + propKey); return delete target[propKey]; }, has(target, propKey) { console.log('HAS ' + propKey); return propKey in target; }, // Other traps: similar } |
Đâ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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const handler = { deleteProperty(target, propKey) { consoavascriptavascriptle.log('DELETE ' + propKey); return Reflect.deleteProperty(target, propKey); }, has(target, propKey) { console.log('HAS ' + propKey); return Reflect.has(target, propKey); }, // Other traps: similar } |
Và sau khi rút gọn do cách viết các trap là tương tự nhau:
1 2 3 4 5 6 7 8 9 10 11 12 |
const handler = new Proxy({}, { get(target, trapName, receiver) { // Return the handler method named trapName return function (...args) { console.log(trapName.toUpperCase()+' '+args.slice(1)); // Forward the operation return Reflect[trapName](...args); } } }); |
Ứ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:
- Scope và Closure trong Javascript
- Javascript Prototype là gì?
- JavaScript – The Core – Object & Prototype
Xem thêm việc làm JavaScript Developer trên TopDev
TopDev via viblo.asia