Giới thiệu proxy ES2015

Addy Osmani
Addy Osmani

Proxy ES2015 (trong Chrome 49 trở lên) cung cấp cho JavaScript một API can thiệp, cho phép chúng ta chặn hoặc chặn tất cả các thao tác trên đối tượng mục tiêu và sửa đổi cách hoạt động của mục tiêu này.

Proxy có nhiều mục đích sử dụng, bao gồm:

  • Cắt bóng
  • Ảo hoá đối tượng
  • Quản lý tài nguyên
  • Phân tích tài nguyên hoặc ghi nhật ký để gỡ lỗi
  • Kiểm soát quyền truy cập và bảo mật
  • Hợp đồng để sử dụng đối tượng

Proxy API chứa một hàm khởi tạo Proxy nhận một đối tượng mục tiêu được chỉ định và một đối tượng trình xử lý.

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

Hành vi của proxy được kiểm soát bởi trình xử lý. Trình xử lý này có thể sửa đổi hành vi ban đầu của đối tượng mục tiêu theo một số cách hữu ích. Trình xử lý chứa các phương thức bẫy không bắt buộc (ví dụ: .get(), .set(), .apply()) được gọi khi thao tác tương ứng được thực hiện trên proxy.

Cắt bóng

Hãy bắt đầu bằng cách lấy một đối tượng thuần tuý và thêm một số phần mềm trung gian chặn vào đối tượng đó bằng Proxy API. Hãy nhớ rằng tham số đầu tiên được truyền vào hàm khởi tạo là mục tiêu (đối tượng được proxy) và tham số thứ hai là trình xử lý (chính proxy). Đây là nơi chúng ta có thể thêm các hook cho phương thức getter, setter hoặc hành vi khác.

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

Khi chạy mã trên trong Chrome 49, chúng ta sẽ nhận được kết quả sau:

get was called for: power  
"Flight"

Như chúng ta có thể thấy trong thực tế, việc thực hiện đúng cách thuộc tính get hoặc thuộc tính set trên đối tượng proxy đã dẫn đến lệnh gọi cấp meta đến bẫy tương ứng trên trình xử lý. Các thao tác của trình xử lý bao gồm đọc thuộc tính, gán thuộc tính và áp dụng hàm, tất cả đều được chuyển tiếp đến bẫy tương ứng.

Hàm bẫy có thể triển khai một thao tác tuỳ ý (ví dụ: chuyển tiếp thao tác đến đối tượng mục tiêu) nếu chọn. Đây thực sự là điều sẽ xảy ra theo mặc định nếu bạn không chỉ định một cái bẫy. Ví dụ: sau đây là một proxy chuyển tiếp không hoạt động chỉ thực hiện việc này:

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

Chúng ta vừa xem xét việc tạo proxy cho các đối tượng thuần tuý, nhưng chúng ta cũng có thể dễ dàng tạo proxy cho đối tượng hàm, trong đó hàm là mục tiêu của chúng ta. Lần này, chúng ta sẽ sử dụng bẫy handler.apply():

// Proxying a function object
function sum(a, b) {
    return a + b;
}

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

Xác định proxy

Bạn có thể quan sát danh tính của proxy bằng cách sử dụng toán tử bằng JavaScript (=====). Như chúng ta đã biết, khi áp dụng cho hai đối tượng, các toán tử này sẽ so sánh danh tính của đối tượng. Ví dụ tiếp theo minh hoạ hành vi này. Việc so sánh hai proxy riêng biệt sẽ trả về giá trị false mặc dù các mục tiêu cơ bản giống nhau. Tương tự như vậy, đối tượng mục tiêu khác với mọi proxy của đối tượng đó:

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

Lý tưởng nhất là bạn không thể phân biệt proxy với đối tượng không phải proxy để việc triển khai proxy không thực sự ảnh hưởng đến kết quả của ứng dụng. Đây là một lý do khiến Proxy API không bao gồm cách kiểm tra xem một đối tượng có phải là proxy hay không cũng như không cung cấp bẫy cho tất cả các thao tác trên đối tượng.

Trường hợp sử dụng

Như đã đề cập, Proxy có nhiều trường hợp sử dụng. Nhiều tính năng trong số đó, chẳng hạn như kiểm soát quyền truy cập và lập hồ sơ, thuộc Trình bao bọc chung: các proxy bao bọc các đối tượng khác trong cùng một "không gian" địa chỉ. Việc ảo hoá cũng được đề cập. Đối tượng ảo là các proxy mô phỏng các đối tượng khác mà không cần các đối tượng đó phải nằm trong cùng một không gian địa chỉ. Ví dụ: đối tượng từ xa (mô phỏng đối tượng trong các không gian khác) và tương lai minh bạch (mô phỏng kết quả chưa được tính toán).

Proxy làm trình xử lý

Một trường hợp sử dụng khá phổ biến cho trình xử lý proxy là thực hiện việc xác thực hoặc kiểm tra quyền truy cập trước khi thực hiện một thao tác trên đối tượng được gói. Thao tác chỉ được chuyển tiếp nếu quá trình kiểm tra thành công. Ví dụ về quy trình xác thực dưới đây minh hoạ điều này:

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

    // The default behavior to store the value
    obj[prop] = value;
    }
};

var person = new Proxy({}, validator);

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

Các ví dụ phức tạp hơn về mẫu này có thể tính đến tất cả các thao tác mà trình xử lý proxy có thể chặn. Bạn có thể tưởng tượng một cách triển khai phải sao chép mẫu kiểm tra quyền truy cập và chuyển tiếp thao tác trong mỗi bẫy.

Điều này có thể khó để dễ dàng trừu tượng, vì mỗi toán tử có thể phải được chuyển tiếp theo cách khác nhau. Trong trường hợp lý tưởng, nếu tất cả các thao tác đều có thể được chuyển qua một bẫy duy nhất một cách đồng nhất, thì trình xử lý chỉ cần thực hiện một lần kiểm tra xác thực trong bẫy duy nhất đó. Bạn có thể thực hiện việc này bằng cách triển khai chính trình xử lý proxy dưới dạng proxy. Rất tiếc, vấn đề này nằm ngoài phạm vi của bài viết này.

Tiện ích đối tượng

Một trường hợp sử dụng phổ biến khác của proxy là mở rộng hoặc xác định lại ngữ nghĩa của các thao tác trên đối tượng. Ví dụ: bạn có thể muốn một trình xử lý ghi lại các thao tác, thông báo cho trình quan sát, gửi ngoại lệ thay vì trả về không xác định hoặc chuyển hướng các thao tác đến các mục tiêu khác nhau để lưu trữ. Trong những trường hợp này, việc sử dụng proxy có thể dẫn đến kết quả rất khác so với khi sử dụng đối tượng mục tiêu.

function extend(sup,base) {

    var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");

    base.prototype = Object.create(sup.prototype);

    var handler = {
    construct: function(target, args) {
        var obj = Object.create(base.prototype);
        this.apply(target,obj, args);
        return obj;
    },

    apply: function(target, that, args) {
        sup.apply(that,args);
        base.apply(that,args);
    }
    };

    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

Kiểm soát quyền truy cập

Kiểm soát quyền truy cập là một trường hợp sử dụng khác phù hợp với Proxy. Thay vì truyền một đối tượng mục tiêu đến một đoạn mã không đáng tin cậy, bạn có thể truyền proxy của đối tượng đó được gói trong một loại màng bảo vệ. Khi ứng dụng cho rằng mã không đáng tin cậy đã hoàn thành một tác vụ cụ thể, ứng dụng có thể thu hồi tệp tham chiếu để tách proxy khỏi mục tiêu của nó. Màng sẽ mở rộng tính năng tách này theo đệ quy cho tất cả các đối tượng có thể truy cập được từ mục tiêu ban đầu đã được xác định.

Sử dụng tính năng phản chiếu với proxy

Reflect là một đối tượng tích hợp mới cung cấp các phương thức cho các thao tác JavaScript có thể chặn, rất hữu ích khi làm việc với Proxy. Trên thực tế, các phương thức phản ánh giống với các phương thức của trình xử lý proxy.

Các ngôn ngữ có kiểu tĩnh như Python hoặc C# từ lâu đã cung cấp API phản chiếu, nhưng JavaScript không thực sự cần API phản chiếu vì là một ngôn ngữ động. Có thể lập luận rằng ES5 đã có khá nhiều tính năng phản chiếu, chẳng hạn như Array.isArray() hoặc Object.getOwnPropertyDescriptor() được coi là phản chiếu trong các ngôn ngữ khác. ES2015 giới thiệu một API phản chiếu sẽ lưu trữ các phương thức trong tương lai cho danh mục này, giúp dễ dàng lý giải hơn. Điều này là hợp lý vì Đối tượng được dùng làm nguyên mẫu cơ sở thay vì một nhóm cho các phương thức phản chiếu.

Khi sử dụng Reflect, chúng ta có thể cải thiện ví dụ về Superhero trước đó để chặn trường thích hợp trên các bẫy get và set như sau:

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

Kết quả đầu ra:

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

Một ví dụ khác là khi bạn muốn:

  • Gói định nghĩa proxy bên trong một hàm khởi tạo tuỳ chỉnh để tránh tạo proxy mới theo cách thủ công mỗi khi chúng ta muốn làm việc với logic cụ thể.

  • Thêm khả năng "lưu" các thay đổi, nhưng chỉ khi dữ liệu đã thực sự được sửa đổi (theo giả thuyết là do thao tác lưu rất tốn kém).

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

Để biết thêm ví dụ về Reflect API, hãy xem Proxy ES6 của Tagtree.

Polyfilling Object.observe()

Mặc dù chúng ta sắp chia tay Object.observe(), nhưng giờ đây, bạn có thể polyfill các lớp này bằng cách sử dụng Proxy ES2015. Gần đây, Simon Blackwell đã viết một shim Object.observe() dựa trên Proxy rất đáng để bạn tham khảo. Erik Arvidsson cũng đã viết một phiên bản đầy đủ thông số kỹ thuật vào năm 2012.

Hỗ trợ trình duyệt

Proxy ES2015 được hỗ trợ trong Chrome 49, Opera, Microsoft Edge và Firefox. Safari đã có nhiều tín hiệu công khai trái chiều về tính năng này, nhưng chúng tôi vẫn lạc quan. Reflect có trong Chrome, Opera và Firefox, đồng thời đang được phát triển cho Microsoft Edge.

Google đã phát hành một polyfill có giới hạn cho Proxy. Bạn chỉ có thể sử dụng phương thức này cho trình bao bọc chung, vì phương thức này chỉ có thể proxy các thuộc tính đã biết tại thời điểm tạo Proxy.

Tài liệu đọc thêm