Giới thiệu proxy ES2015

Addy Osmani
Addy Osmani

Các proxy ES2015 (trong Chrome 49 trở lên) cung cấp JavaScript với một API trung gian, cho phép chúng ta chặn hoặc chặn tất cả thao tác trên một đố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 công dụng, bao gồm:

  • Cắt bóng
  • ảo hoá đối tượng
  • Quản lý tài nguyên
  • Lập hồ sơ 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 Hàm tạo proxy nhận đối tượng mục tiêu được chỉ định và đố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 do handler kiểm soát. Trình xử lý này có thể sửa đổi hành vi ban đầu của đối tượng target theo một số cách khá 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 đơn giản và thêm một số phần mềm trung gian chặn vào đối tượng đó bằng cách sử dụng Proxy API. Hãy nhớ rằng tham số đầu tiên được truyền đến 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 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);

Chạy mã trên trong Chrome 49, chúng ta có kết quả như sau:

get was called for: power  
"Flight"

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

Nếu muốn, hàm trap 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). Điều này thực sự sẽ xảy ra theo mặc định nếu không chỉ định bẫy. Ví dụ: dưới đây là 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 proxy các đối tượng thuần tuý, nhưng cũng có thể dễ dàng proxy một đối tượng hàm, trong đó hàm là mục tiêu. 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 nhận dạng của một proxy bằng các toán tử đẳng thức JavaScript (=====). Như chúng ta đã biết, khi áp dụng cho 2 đố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. Thao tác so sánh 2 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 bất kỳ proxy nào:

// Continuing previous example

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

Tốt nhất là bạn không nên phân biệt được một proxy với một đối tượng không có proxy để việc đặt một proxy vào đúng vị trí sẽ 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 cung cấp 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 mọi thao tác trên các đố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 thuộc tính ở trên, chẳng hạn như kiểm soát quyền truy cập và lập hồ sơ nằm trong Trình bao bọc chung: proxy bao bọc các đối tượng khác trong cùng một địa chỉ "dấu cách". Ả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 đó ở trong cùng một không gian địa chỉ. Ví dụ: các đối tượng từ xa (mô phỏng các đối tượng trong các không gian khác) và tương lai trong suốt (mô phỏng nhữ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à để kiểm tra xác thực hoặc kiểm soát quyền truy cập trước khi thực hiện thao tác trên đối tượng được bao bọc. Chỉ khi kiểm tra thành công thì hoạt động mới được chuyển tiếp. Ví dụ xác thực bên dưới 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 trình xử lý proxy hoạt động khác nhau có thể chặn. Hãy hình dung việc triển khai phải sao chép mẫu kiểm tra và chuyển tiếp hoạt động truy cập trong mỗi bẫy.

Điều này có thể phức tạp để dễ dàng tóm tắt, vì mỗi hoạt động có thể phải được chuyển tiếp theo cách khác nhau. Trong trường hợp hoàn hảo, nếu tất cả hoạt động có thể được chuyển đồng nhất qua một bẫy, thì trình xử lý sẽ chỉ cần thực hiện kiểm tra xác thực một lần trong một 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, điều này nằm ngoài phạm vi của bài viết này.

Mở rộng đối tượng

Một trường hợp sử dụng phổ biến khác cho 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 nhật ký hoạt động, thông báo cho người quan sát, gửi ngoại lệ thay vì trả về dữ liệu không xác định hoặc chuyển hướng hoạt động đến các mục tiêu khác để 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 việc 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 hiệu quả khác cho 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, người dùng có thể truyền proxy của đối tượng được bao bọc trong một loại màng bảo vệ. Khi cho rằng mã không đáng tin cậy đã hoàn thành một nhiệm vụ cụ thể, ứng dụng có thể thu hồi tệp đối chiếu đã tách proxy khỏi mục tiêu. Màng này sẽ mở rộng đệ quy sự tách rời này cho tất cả các đối tượng có thể tiếp cận từ mục tiêu ban đầu đã được xác định.

Sử dụng phản chiếu bằng proxy

Reflect là một đối tượng mới được tích hợp sẵn, cung cấp các phương thức cho hoạt động 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 chiếu cũng giống như các phương thức của trình xử lý proxy.

Từ lâu, các ngôn ngữ nhập tĩnh như Python hoặc C# đã cung cấp API phản chiếu, nhưng JavaScript thực sự không cần 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ác tính năng này sẽ được coi là phản chiếu trong các ngôn ngữ khác. ES2015 ra mắt một Reflection API chứa các phương thức trong tương lai cho danh mục này, giúp dễ hiểu hơn. Điều này rất hợp lý vì Object là một nguyên mẫu cơ sở chứ không phải là bộ chứa cho các phương thức phản chiếu.

Bằng cách sử dụng tính năng Phản chiếu, chúng ta có thể cải thiện ví dụ về Siêu anh hùng trước đó để chặn trường đúng cách khi thu thập và đặt bẫy 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ả:

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 một người nên:

  • Gói định nghĩa proxy bên trong một hàm khởi tạo tuỳ chỉnh để tránh việc 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 (giả sử 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ề API Phản ánh, hãy xem bài viết về Uỷ quyền ES6 của Tagtree.

Polyfilling Object.observe()

Mặc dù chúng tôi sẽ nói tạm biệt với Object.observe(), nhưng giờ đây bạn có thể thay thế polyfill bằng cách sử dụng Proxy ES2015. Simon Blackwell đã viết một Object.observe() shim dựa trên Proxy, mà bạn nên dùng thử. Erik Arvidsson cũng đã viết một phiên bản hoàn thiện về thông số kỹ thuật từ năm 2012.

Hỗ trợ trình duyệt

Các proxy ES2015 được hỗ trợ trong Chrome 49, Opera, Microsoft Edge và Firefox. Safari nhận đượ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 và đang trong quá trình phát triển cho Microsoft Edge.

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

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