JavaScript có nhiều tính năng hữu ích mà hầu hết các nhà phát triển đều biết. Đồng thời, có một số viên ngọc ẩn có thể giải quyết các vấn đề thực sự khó khăn nếu bạn biết về chúng.
Lập trình meta trong JavaScript là một trong những khái niệm mà nhiều người trong chúng ta có thể không quen thuộc. Trong bài viết này, chúng ta sẽ tìm hiểu về Siêu lập trình và nó hữu ích như thế nào đối với chúng ta.
Với ES6 (ECMAScript 2015), chúng tôi có hỗ trợ cho Reflect
và Proxy
các đối tượng cho phép chúng ta thực hiện Siêu lập trình một cách dễ dàng. Trong bài viết này, chúng ta sẽ học cách sử dụng chúng với các ví dụ.
Metaprogramming
không có gì ít hơn so với ma thuật trong lập trình! Làm thế nào về việc viết một chương trình đọc, sửa đổi, phân tích và thậm chí tạo ra một chương trình? Không phải điều đó nghe có vẻ phù thủy và mạnh mẽ sao?

Wikipedia mô tả Siêu lập trình như thế này:
Metaprogramming
là một kỹ thuật lập trình trong đó các chương trình máy tính có khả năng coi các chương trình khác là dữ liệu của chúng. Điều này có nghĩa là một chương trình có thể được thiết kế để đọc, tạo, phân tích hoặc chuyển đổi các chương trình khác và thậm chí tự sửa đổi chính nó trong khi chạy.
Nói một cách đơn giản, Siêu lập trình liên quan đến việc viết mã có thể
- Tạo mã
- Thao tác cấu trúc ngôn ngữ trong thời gian chạy. Hiện tượng này được gọi là
Reflective Metaprogramming
hoặcReflection
.
Reflection
là một nhánh của Siêu lập trình. Reflection có ba nhánh con:
- nội quan: Mã có thể tự kiểm tra. Nó được sử dụng để truy cập các thuộc tính bên trong để chúng tôi có thể lấy thông tin cấp thấp trong mã của mình.
- Tự sửa đổi: Như tên gợi ý, mã có thể tự sửa đổi.
- cầu thay: Nghĩa đen của sự can thiệp là hành động thay mặt cho người khác. Trong siêu lập trình, sự can thiệp thực hiện chính xác như vậy bằng cách sử dụng các khái niệm như, bao bọc, bẫy, chặn.
ES6 cung cấp cho chúng tôi Reflect
đối tượng (còn gọi là Reflect API) để đạt được Introspection
. Các Proxy
đối tượng của ES6 giúp chúng tôi với Intercession
. Chúng ta sẽ không nói quá nhiều về Self-Modification
vì chúng tôi muốn tránh xa nó càng nhiều càng tốt.
Đợi một chút! Nói rõ hơn, Lập trình meta không được giới thiệu trong ES6. Thay vào đó, nó đã có sẵn trong ngôn ngữ ngay từ đầu. ES6 đã làm cho nó dễ sử dụng hơn rất nhiều.
Bạn có nhớ eval
? Chúng ta hãy xem nó đã được sử dụng như thế nào:
const blog = {
name: 'freeCodeCamp'
}
console.log('Before eval:', blog);
const key = 'author';
const value="Tapas";
testEval = () => eval(`blog.${key} = '${value}'`);
// Call the function
testEval();
console.log('After eval magic:', blog);
Như bạn có thể nhận thấy, eval
đã giúp tạo mã bổ sung. Trong trường hợp này, đối tượng blog
đã được sửa đổi với một thuộc tính bổ sung tại thời điểm thực hiện.
Before eval: {name: freeCodeCamp}
After eval magic: {name: "freeCodeCamp", author: "Tapas"}
nội quan
Trước khi đưa vào Reflect object
trong ES6, chúng tôi vẫn có thể xem xét nội tâm. Dưới đây là một ví dụ về cách đọc cấu trúc của chương trình:
var users = {
'Tom': 32,
'Bill': 50,
'Sam': 65
};
Object.keys(users).forEach(name => {
const age = users[name];
console.log(`User ${name} is ${age} years old!`);
});
Ở đây chúng ta đang đọc users
cấu trúc đối tượng và ghi lại khóa-giá trị trong một câu.
User Tom is 32 years old!
User Bill is 50 years old!
User Sam is 65 years old!
Tự sửa đổi
Hãy lấy một đối tượng blog có phương thức tự sửa đổi:
var blog = {
name: 'freeCodeCamp',
modifySelf: function(key, value) {blog[key] = value}
}
Các blog
đối tượng có thể tự sửa đổi bằng cách thực hiện điều này:
blog.modifySelf('author', 'Tapas');
cầu thay
Intercession
trong siêu lập trình có nghĩa là hành động hoặc thay đổi mọi thứ thay mặt cho ai đó hoặc cái gì khác. Trước ES6 Object.defineProperty()
phương pháp có thể thay đổi ngữ nghĩa của một đối tượng:
var sun = {};
Object.defineProperty(sun, 'rises', {
value: true,
configurable: false,
writable: false,
enumerable: false
});
console.log('sun rises', sun.rises);
sun.rises = false;
console.log('sun rises', sun.rises);
Đầu ra:
sun rises true
sun rises true
Như bạn có thể thấy, các sun
đối tượng đã được tạo như một đối tượng bình thường. Sau đó, ngữ nghĩa đã được thay đổi để nó không thể ghi được.
Bây giờ chúng ta hãy bắt đầu tìm hiểu Reflect
và Proxy
đồ vật với công dụng tương ứng của chúng.
Trong ES6, Reflect là một tính năng mới Global Object
(như Toán học) cung cấp một số chức năng tiện ích. Một số chức năng này có thể thực hiện chính xác điều tương tự như các phương thức từ Object
hoặc Function
.
Tất cả các hàm này đều là các hàm Introspection, nơi bạn có thể truy vấn một số chi tiết nội bộ về chương trình trong thời gian chạy.
Dưới đây là danh sách các phương pháp có sẵn từ Reflect
vật.
// Reflect object methods
Reflect.apply()
Reflect.construct()
Reflect.get()
Reflect.has()
Reflect.ownKeys()
Reflect.set()
Reflect.setPrototypeOf()
Reflect.defineProperty()
Reflect.deleteProperty()
Reflect.getOwnPropertyDescriptor()
Reflect.getPrototypeOf()
Reflect.isExtensible()
Nhưng chờ đã, đây là một câu hỏi: Tại sao chúng ta cần một đối tượng API mới khi những đối tượng này có thể đã tồn tại hoặc có thể được thêm vào Object
hoặc Function
?
Bối rối? Hãy cố gắng tìm ra điều này.
Tất cả trong một không gian tên
JavaScript đã hỗ trợ phản chiếu đối tượng. Nhưng các API này không được tổ chức dưới một không gian tên. Kể từ ES6, họ hiện đang ở dưới Reflect
.
Tất cả các phương thức của đối tượng Reflect đều có bản chất tĩnh. Điều đó có nghĩa là bạn không phải khởi tạo đối tượng Reflect bằng cách sử dụng new
từ khóa.
Đơn giản để sử dụng
Các introspection
phương pháp của Object
ném một ngoại lệ khi họ không hoàn thành thao tác. Đây là một gánh nặng bổ sung cho người tiêu dùng (lập trình viên) để xử lý ngoại lệ đó trong mã.
Bạn có thể thích xử lý nó như một boolean(true | false)
thay vì sử dụng xử lý ngoại lệ. Đối tượng Reflect giúp bạn làm điều đó.
Đây là một ví dụ với Object.defineProperty:
try {
Object.defineProperty(obj, name, desc);
} catch (e) {
// Handle the exception
}
Và với API phản ánh:
if (Reflect.defineProperty(obj, name, desc)) {
// success
} else {
// failure (and far better)
}
Ấn tượng về chức năng First-Class
Chúng ta có thể tìm thấy sự tồn tại của một thuộc tính cho một đối tượng như (prop in obj). Nếu chúng ta cần sử dụng nó nhiều lần trong mã của mình, chúng ta phải tạo một hàm bằng cách gói mã này.
Trong ES6, Reflect API giải quyết vấn đề này bằng cách giới thiệu một chức năng hạng nhất, Reflect.has(obj, prop)
.
Hãy xem một ví dụ khác: Xóa thuộc tính đối tượng.
const obj = { bar: true, baz: false};
// We define this function
function deleteProperty(object, key) {
delete object[key];
}
deleteProperty(obj, 'bar');
Với API phản ánh:
// With Reflect API
Reflect.deleteProperty(obj, 'bar');
Một cách đáng tin cậy hơn để sử dụng phương thức apply()
Các apply()
phương thức trong ES5 giúp gọi một hàm với ngữ cảnh của một this
giá trị. Chúng ta cũng có thể truyền các đối số dưới dạng một mảng.
Function.prototype.apply.call(func, obj, arr);
// or
func.apply(obj, arr);
Điều này kém tin cậy hơn vì func
có thể là một đối tượng đã xác định riêng của mình apply
phương pháp.
Trong ES6, chúng tôi có một cách giải quyết vấn đề này đáng tin cậy và thanh lịch hơn:
Reflect.apply(func, obj, arr);
Trong trường hợp này, chúng tôi sẽ nhận được một TypeError
nếu func
không gọi được.
Giúp các loại suy tư khác
chúng tôi sẽ thấy điều này có nghĩa là gì khi chúng ta tìm hiểu về Proxy
vật. Các phương thức Reflect API có thể được sử dụng với Proxy trong nhiều trường hợp sử dụng.
ES6 Proxy
đối tượng giúp trong intercession
.
Như tên cho thấy, một proxy
đối tượng giúp hành động thay mặt cho một cái gì đó. Nó thực hiện điều này bằng cách ảo hóa một đối tượng khác. Ảo hóa đối tượng cung cấp các hành vi tùy chỉnh cho đối tượng đó.
Ví dụ: sử dụng đối tượng proxy, chúng ta có thể ảo hóa việc tra cứu thuộc tính đối tượng, gọi hàm, v.v. Chúng ta sẽ thấy một số trong số này chi tiết hơn dưới đây.
Dưới đây là một vài thuật ngữ hữu ích bạn cần nhớ và sử dụng:
- Các
target
: Một đối tượng mà proxy cung cấp các hành vi tùy chỉnh. - Các
handler
: Nó là một đối tượng có chứa bẫy. - Các
trap
: Trap là một phương thức cung cấp quyền truy cập vào các thuộc tính của đối tượng đích. Điều này đạt được bằng cách sử dụng các phương pháp Reflect API. Mỗi phương thức bẫy được ánh xạ với các phương thức từ Reflect API.
Bạn có thể tưởng tượng nó một cái gì đó như thế này:

Một người xử lý với một trap
chức năng nên được xác định. Sau đó, chúng ta cần tạo một đối tượng Proxy bằng cách sử dụng trình xử lý và đối tượng đích. Đối tượng Proxy sẽ có tất cả các thay đổi với các hành vi tùy chỉnh được áp dụng.
Hoàn toàn ổn nếu bạn chưa hiểu rõ từ mô tả ở trên. Chúng ta sẽ nắm được nó thông qua mã và các ví dụ trong một phút.
Cú pháp tạo đối tượng Proxy như sau:
let proxy = new Proxy(target, handler);
Có nhiều bẫy proxy (chức năng xử lý) có sẵn để truy cập và tùy chỉnh đối tượng mục tiêu. Đây là danh sách của họ.
handler.apply()
handler.construct()
handler.get()
handler.has()
handler.ownKeys()
handler.set()
handler.setPrototypeOf()
handler.getPrototypeOf()
handler.defineProperty()
handler.deleteProperty()
handler.getOwnPropertyDescriptor()
handler.preventExtensions()
handler.isExtensible()
Lưu ý rằng mỗi cái bẫy có một ánh xạ với Reflect
các phương thức của đối tượng. Điều này có nghĩa là bạn có thể sử dụng Reflect
và Proxy
cùng nhau trong nhiều trường hợp sử dụng.
Cách nhận các giá trị thuộc tính đối tượng không khả dụng
Hãy xem xét một ví dụ về một employee
đối tượng và cố gắng in một số thuộc tính của nó:
const employee = {
firstName: 'Tapas',
lastName: 'Adhikary'
};
console.log(employee.firstName);
console.log(employee.lastName);
console.log(employee.org);
console.log(employee.fullName);
Đầu ra dự kiến như sau:
Tapas
Adhikary
undefined
undefined
Bây giờ, hãy sử dụng đối tượng Proxy để thêm một số hành vi tùy chỉnh vào employee
vật.
Bước 1: Tạo Trình xử lý sử dụng bẫy get
Chúng tôi sẽ sử dụng một cái bẫy gọi là get
cho phép chúng tôi có được một giá trị thuộc tính. Đây là xử lý của chúng tôi:
let handler = {
get: function(target, fieldName) {
if(fieldName === 'fullName' ) {
return `${target.firstName} ${target.lastName}`;
}
return fieldName in target ?
target[fieldName] :
`No such property as, '${fieldName}'!`
}
};
Trình xử lý trên giúp tạo giá trị cho fullName
tài sản. Nó cũng thêm thông báo lỗi tốt hơn khi thiếu thuộc tính đối tượng.
Bước 2: Tạo đối tượng proxy
Như chúng ta có mục tiêu employee
đối tượng và trình xử lý, chúng ta sẽ có thể tạo một đối tượng Proxy như thế này:
let proxy = new Proxy(employee, handler);
Bước 3: Truy cập thuộc tính trên đối tượng Proxy
Bây giờ chúng ta có thể truy cập các thuộc tính của đối tượng employee bằng cách sử dụng đối tượng proxy, như sau:
console.log(proxy.firstName);
console.log(proxy.lastName);
console.log(proxy.org);
console.log(proxy.fullName);
Đầu ra sẽ là:
Tapas
Adhikary
No such property as, 'org'!
Tapas Adhikary
Lưu ý cách chúng tôi đã thay đổi mọi thứ một cách kỳ diệu cho employee
vật!
Proxy để xác thực các giá trị
Hãy tạo một đối tượng proxy để xác thực một giá trị số nguyên.
Bước 1: Tạo trình xử lý sử dụng bẫy đã đặt
Trình xử lý trông như thế này:
const validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if(!Number.isInteger(value)) {
throw new TypeError('Age is always an Integer, Please Correct it!');
}
if(value < 0) {
throw new TypeError('This is insane, a negative age?');
}
}
}
};
Bước 2: Tạo đối tượng proxy
Tạo một đối tượng proxy như thế này:
let proxy = new Proxy(employee, validator);
Bước 3: Gán một giá trị không nguyên cho một thuộc tính, chẳng hạn như tuổi
Hãy thử làm điều này:
proxy.age="I am testing a blunder"; // string value
Đầu ra sẽ như thế này:
TypeError: Age is always an Integer, Please Correct it!
at Object.set (E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:28:23)
at Object.<anonymous> (E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:40:7)
at Module._compile (module.js:652:30)
at Object.Module._extensions..js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)
at Function.Module.runMain (module.js:693:10)
at startup (bootstrap_node.js:188:16)
at bootstrap_node.js:609:3
Tương tự, hãy thử làm điều này:
p.age = -1; // will result in error
Cách sử dụng Proxy và Reflect cùng nhau
Dưới đây là một ví dụ về trình xử lý mà chúng tôi sử dụng các phương thức từ API Reflect:
const employee = {
firstName: 'Tapas',
lastName: 'Adhikary'
};
let logHandler = {
get: function(target, fieldName) {
console.log("Log: ", target[fieldName]);
// Use the get method of the Reflect object
return Reflect.get(target, fieldName);
}
};
let func = () => {
let p = new Proxy(employee, logHandler);
p.firstName;
p.lastName;
};
func();
Một vài trường hợp sử dụng Proxy khác
Có một số trường hợp sử dụng khác mà khái niệm này có thể được sử dụng.
- Để bảo vệ TÔI trường của một đối tượng khỏi bị xóa (bẫy: deleteProperty)
- Để theo dõi Quyền truy cập thuộc tính (bẫy: nhận, đặt)
- Đối với ràng buộc dữ liệu (bẫy: đặt)
- Với tham chiếu hủy bỏ
- để thao tác
in
hành vi của nhà điều hành
… và nhiều thứ khác nữa.
Trong khi khái niệm về Metaprogramming
mang lại cho chúng ta rất nhiều sức mạnh, đôi khi sự kỳ diệu của nó có thể đi sai hướng.

Hãy cẩn thận:
- Quá nhiều
magic
! Hãy chắc chắn rằng bạn hiểu nó trước khi bạn áp dụng nó. - Hiệu suất khả thi đạt được khi bạn biến điều không thể thành có thể
- Có thể được coi là gỡ lỗi truy cập.
Để tóm tắt,
Reflect
vàProxy
là những bổ sung tuyệt vời trong JavaScript để trợ giúp cho Lập trình siêu dữ liệu.- Rất nhiều tình huống phức tạp có thể được xử lý với sự giúp đỡ của họ.
- Hãy nhận biết những nhược điểm là tốt.
- ES6 Symbols cũng có thể được sử dụng với các lớp và đối tượng hiện có của bạn để thay đổi hành vi của chúng.
Tôi hy vọng bạn tìm thấy bài viết này sâu sắc. Tất cả mã nguồn được sử dụng trong bài viết này có thể được tìm thấy trong kho lưu trữ GitHub của tôi.
Hãy chia sẻ bài viết để những người khác cũng có thể đọc được. Bạn có thể @ tôi trên Twitter (@tapasadhikary) với các bình luận, hoặc thoải mái theo dõi tôi.