Proxy
Đối tượng Proxy được sử dụng để thay đổi hành vi mặc định của một số hoạt động (như tìm kiếm thuộc tính, gán giá trị, liệt kê, gọi hàm, v.v.), tương đương với việc thay đổi ngôn ngữ lập trình ở mức độ ngôn ngữ, do đó nó thuộc về một loại siêu lập trình (Meta Programming), tức là lập trình cho ngôn ngữ lập trình.
Proxy có thể hiểu là một đại diện được đặt trước đối tượng mục tiêu, mọi truy cập từ bên ngoài vào đối tượng đó đều phải thông qua lớp đại diện này, do đó cung cấp một cơ chế để lọc và thay đổi truy cập từ bên ngoài.
Từ “Proxy” có nghĩa là ủy quyền, được sử dụng ở đây để đại diện cho một số hoạt động, có thể dịch là đại diện.
target
: Đối tượng được ảo hóa bởi Proxy, thường được sử dụng làm lưu trữ đại diện, kiểm tra các invariants về không thể mở rộng hoặc thuộc tính không thể cấu hình của đối tượng (duy trì ý nghĩa không thay đổi)handler
: Đối tượng chứa các trình bắt (Trap), có thể dịch là đối tượng xử lýtraps
: Cung cấp các phương thức truy cập thuộc tính, tương tự như khái niệm trình bắt trong hệ điều hành
Cách sử dụng:
ES6 cung cấp Proxy constructor để tạo ra một instance của Proxy.
Tất cả các cách sử dụng của đối tượng Proxy đều có cùng cú pháp như trên, chỉ khác nhau ở cách viết tham số handler
. Trong đó, new Proxy()
đại diện cho việc tạo ra một instance của Proxy, tham số target
đại diện cho đối tượng mục tiêu mà chúng ta muốn chặn, tham số handler
cũng là một đối tượng, được sử dụng để tùy chỉnh hành vi chặn.
Sử dụng cơ bản
Đoạn mã trên tạo ra một lớp đại diện cho một đối tượng trống, và định nghĩa lại hành vi đọc (get) và gán giá trị (set) của thuộc tính. Ở đây tạm thời chưa giải thích cú pháp cụ thể, chỉ xem kết quả chạy. Khi đọc hoặc gán giá trị cho thuộc tính của đối tượng đại diện proxy
đã được định nghĩa hành vi chặn, kết quả nhận được sẽ như sau:
Đoạn mã trên cho thấy, Proxy thực tế là nạp chồng (Overload) toán tử dấu chấm, tức là sử dụng định nghĩa của chính nó để ghi đè định nghĩa gốc của ngôn ngữ.
Vấn đề về ngữ cảnh tham chiếu của Proxy
Mặc dù Proxy có thể đại diện cho việc truy cập vào đối tượng mục tiêu, nhưng nó không phải là một đại diện trong suốt cho đối tượng mục tiêu, tức là không thể đảm bảo hành vi tương tự với đối tượng mục tiêu khi không có bất kỳ sự chặn nào. Nguyên nhân chính là khi Proxy đại diện, từ khóa this
bên trong đối tượng mục tiêu sẽ trỏ đến Proxy đại diện.
Trong đoạn mã trên, khi proxy
đại diện cho target.foo
, từ khóa this
bên trong target.foo
sẽ trỏ đến proxy
, chứ không phải target
.
Hỗ trợ lồng nhau
Proxy cũng không hỗ trợ lồng nhau, điều này giống với Object.defineProperty()
. Do đó, để giải quyết vấn đề này, chúng ta cần duyệt qua từng cấp độ. Cách viết Proxy là sử dụng đệ quy trong get
và trả về một Proxy mới.
Proxy và Object.defineProperty
ES5 cung cấp phương thức Object.defineProperty
, phương thức này cho phép định nghĩa một thuộc tính mới trên một đối tượng, hoặc sửa đổi một thuộc tính hiện có trên một đối tượng và trả về đối tượng đó.
Tuy nhiên, Object.defineProperty
có ba vấn đề chính:
- Không thể theo dõi sự thay đổi của mảng, Vue sử dụng Hack để ghi đè tám phương thức mảng để thực hiện điều này.
- Chỉ có thể can thiệp vào thuộc tính của đối tượng, do đó, các thuộc tính cần liên kết hai chiều phải được định nghĩa rõ ràng.
- Phải duyệt qua đối tượng lồng nhau một cách sâu để thực hiện can thiệp.
Khác biệt giữa Proxy và Object.defineProperty:
- Proxy có thể trực tiếp theo dõi sự thay đổi của mảng.
- Proxy có thể trực tiếp theo dõi đối tượng chứ không chỉ là thuộc tính.
- Proxy có thể can thiệp vào toàn bộ đối tượng và trả về một đối tượng mới, vượt trội so với
Object.defineProperty
cả về tính tiện lợi và chức năng cơ bản. - Proxy có tới 13 phương thức chặn, không giới hạn ở
apply
,ownKeys
,deleteProperty
,has
, v.v., trong khiObject.defineProperty
không có.
Nhược điểm của Proxy:
Nhược điểm của Proxy là sự tương thích, không thể sử dụng Polyfill để giải quyết vấn đề tương thích.
Ứng dụng
Pipeline
Trong các đề xuất ECMAScript mới nhất, đã xuất hiện toán tử pipeline |>
, có khái niệm tương tự trong RxJS và Node.js.
Sử dụng Proxy, chúng ta cũng có thể thực hiện chức năng pipe
, chỉ cần sử dụng get
để chặn việc truy cập thuộc tính, đặt tất cả các phương thức truy cập vào mảng stack
, và khi truy cập cuối cùng vào execute
, trả về kết quả.
Nạp chồng toán tử
Toán tử in
được sử dụng để kiểm tra xem một thuộc tính cụ thể có trong một đối tượng cụ thể hoặc chuỗi nguyên mẫu của nó hay không, nhưng nó cũng là toán tử được nạp chồng tinh vi nhất về cú pháp. Ví dụ này định nghĩa một hàm range
để so sánh các số trong một khoảng liên tục.
Khác với Python, Python sử dụng các generator để so sánh với các chuỗi số nguyên hữu hạn, phương pháp này hỗ trợ so sánh số thập phân và có thể mở rộng để hỗ trợ các khoảng số khác.
Mặc dù trường hợp sử dụng này có thể không giải quyết các vấn đề phức tạp, nhưng nó cung cấp mã nguồn sạch, dễ đọc và có thể tái sử dụng.
Ngoài toán tử in
, chúng ta cũng có thể nạp chồng delete
và new
.
Tìm kiếm đối tượng cụ thể trong mảng thông qua thuộc tính
Đoạn mã dưới đây mở rộng một số tiện ích cho một mảng thông qua Proxy. Như bạn có thể thấy, với Proxy, chúng ta có thể linh hoạt định nghĩa các thuộc tính mà không cần sử dụng phương thức Object.defineProperties
. Ví dụ dưới đây có thể được sử dụng để tìm kiếm một hàng trong bảng dựa trên một ô cụ thể.
Mở rộng hàm khởi tạo
Phương thức đại diện có thể dễ dàng mở rộng một hàm khởi tạo hiện có thông qua một hàm khởi tạo mới.
Sử dụng ví dụ:
Hiệu ứng phụ
Chúng ta có thể sử dụng Proxy để tạo ra hiệu ứng phụ khi đọc và ghi thuộc tính. Ý tưởng là khi truy cập hoặc ghi vào một số thuộc tính cụ thể, chúng ta có thể kích hoạt một số hàm.
Khi thuộc tính status
được ghi và giá trị là 'complete'
, hàm hiệu ứng phụ dosomething()
sẽ được kích hoạt.
Bộ nhớ cache
Tận dụng khả năng can thiệp vào việc đọc và ghi thuộc tính của đối tượng, chúng ta có thể tạo ra một bộ nhớ cache dựa trên bộ nhớ, chỉ trả về giá trị khi nó chưa hết hạn:
Ở đây, chúng ta tạo một hàm và trả về một Proxy. Trước khi truy cập vào thuộc tính của target
, handler
của Proxy này sẽ kiểm tra xem đối tượng target
có hết hạn không, và dựa trên điều này, chúng ta có thể thiết lập kiểm tra hết hạn cho mỗi khóa giá trị bằng cách sử dụng TTLs hoặc cơ chế khác.
Đối tượng Cookie
Nếu bạn đã từng làm việc với Cookie, bạn sẽ phải làm việc với document.cookie
. Đây là một API khá đặc biệt, vì API này là một chuỗi, nó đọc tất cả các Cookie và phân tách chúng bằng dấu chấm phẩy.
document.cookie
là một chuỗi có dạng như sau:
Đơn giản mà nói, việc xử lý document.cookie
khá phức tạp và dễ gây lỗi. Một cách để làm việc với nó là sử dụng một framework Cookie đơn giản, có thể được thực hiện bằng cách sử dụng Proxy.
Hàm này trả về một đối tượng key-value, nhưng Proxy sẽ xử lý tất cả các thay đổi về Cookie.
Trong 11 dòng mã này, việc sửa đổi Cookie được thực hiện một cách tốt hơn, mặc dù trong môi trường sản xuất, bạn cần thêm các tính năng bổ sung như chuẩn hóa chuỗi.
Nhật ký và thống kê
Trong việc phát triển phía máy chủ, chúng ta có thể sử dụng Proxy để làm proxy cho các hàm và nhật ký số lần gọi trong một khoảng thời gian nhất định.
Điều này có thể hữu ích khi phân tích hiệu năng sau này:
Hoặc:
Như ví dụ trên, bạn có thể xác định chính xác khi nào thuộc tính của một đối tượng đã được thay đổi và bạn cũng có thể sử dụng các phương thức như console.trace
để xác định nơi nó đã được thay đổi.
Mở rộng đến các loại handler khác, sau khi bạn bọc một đối tượng trong một Proxy, bạn có thể biết khi nào và ở đâu thuộc tính của nó được đọc, được gọi, được khởi tạo, bị xóa, được truy cập vào thuộc tính, v.v.
Nghe có vẻ việc xác định vấn đề với các loại động có thể trở nên đơn giản hơn. Nếu có một thư viện giám sát đối tượng phổ biến, nhà phát triển chỉ cần nhập thư viện đó và bọc đối tượng cần giám sát để có thể in ra toàn bộ lịch sử hoạt động của đối tượng đó.
Đại diện động
Đại diện đơn giản:
Cách sử dụng có thể là:
Trong design pattern, có một mô hình trung gian gọi là mô hình trung gian (Mediator pattern), trong mô hình này, Proxy có thể được coi là trung gian trong giao tiếp giữa các đối tượng. Trong trường hợp này, chúng ta không cần xác định quan hệ giữa các đối tượng khác nhau, chỉ cần Proxy đảm bảo trải nghiệm nhất quán với bên ngoài.
Một ví dụ khác là trong tương lai, thông qua Proxy, chúng ta cũng có thể triển khai các kịch bản nạp lại nóng, chúng ta có thể thay thế phiên bản cũ bằng mã mới được yêu cầu bằng cách chỉ định Proxy để ẩn điều này khỏi nhà phát triển.