Làm quen với Proxy object trong Javascript

Trong bài viết này mình và các bạn sẽ cùng tìm hiểu về Proxy object trong js và ứng dụng nó để làm magic method nhé 😸

I. Khái niệm

  • Proxy object được dùng để đóng gói (wrap) một object khác nhằm mục đích chính chỉnh sửa cách thức truy xuất (getter), gán giá trị thuộc tính (setter) của object đó
  • Cú pháp:
var p = new Proxy(target, handler);
  • Trong đó
    • target: Là object mình muốn wrap lại bằng Proxy
    • handler: Là object chứa các thuộc tính là các phương thức định nghĩa cách thức hoạt động của Proxy p. Các phương thức này gọi là traps. Vậy traps là gì
    • traps: Là các phương thức nhằm truy xuất các thuộc tính của object target.
  • Ví dụ thực tế như sau cho các bạn dễ hiểu.
    • target: Là cái két sắt đựng tiền
    • Proxy: Là ngôi nhà chứa cái két sắt và có các cổng (traps). Do đó muốn để vào nhà lấy tiền hay rút tiền trong két sắt thì phải qua thằng này.

II. Các loại traps và ví dụ

1. handler.getPrototypeOf(). A trap for Object.getPrototypeOf.

var obj = {};
var proto = {};
var handler = {
    getPrototypeOf(target) {
        console.log(target === obj);   // true
        console.log(this === handler); // true
        return proto;
    }
};

var p = new Proxy(obj, handler);
console.log(Object.getPrototypeOf(p) === proto);    // true

2. handler.setPrototypeOf(). A trap for Object.setPrototypeOf.

var handlerReturnsFalse = {
    setPrototypeOf(target, newProto) {
        return false;
    }
};

var newProto = {}, target = {};

var p1 = new Proxy(target, handlerReturnsFalse);
Object.setPrototypeOf(p1, newProto); // throws a TypeError
Reflect.setPrototypeOf(p1, newProto); // returns false

3. handler.isExtensible(). A trap for Object.isExtensible.

var p = new Proxy({}, {
  isExtensible: function(target) {
    return false;
  }
});

Object.isExtensible(p); // TypeError is thrown

4. handler.preventExtensions(). A trap for Object.preventExtensions.

var p = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(p); // TypeError is thrown

5. handler.getOwnPropertyDescriptor(). A trap for Object.getOwnPropertyDescriptor.

var p = new Proxy({ a: 20}, {
  getOwnPropertyDescriptor: function(target, prop) {
    console.log('called: ' + prop);
    return { configurable: true, enumerable: true, value: 10 };
  }
});

console.log(Object.getOwnPropertyDescriptor(p, 'a').value); // "called: a"  10

6. handler.defineProperty(). A trap for Object.defineProperty.

var p = new Proxy({}, {
  defineProperty: function(target, prop, descriptor) {
    console.log('called: ' + prop);
    return true;
  }
});

var desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, 'a', desc); // "called: a"

7. handler.has(). A trap for the in operator.

var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
  has: function(target, prop) {
    return false;
  }
});

'a' in p; // TypeError is thrown

8. handler.get(). A trap for getting property values.

var p = new Proxy({}, {
  get: function(target, prop, receiver) {
    console.log('called: ' + prop);
    return 10;
  }
});

console.log(p.a); // "called: a"  10

9. handler.set(). A trap for setting property values.

var p = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    target[prop] = value
    console.log('property set: ' + prop + ' = ' + value)
    return true
  }
})

console.log('a' in p)  // false

p.a = 10               // "property set: a = 10"
console.log('a' in p)  // true
console.log(p.a)       // 10

10. handler.ownKeys(). A trap for Object.getOwnPropertyNames.

var p = new Proxy({}, {
  ownKeys: function(target) {
    console.log('called');
    return ['a', 'b', 'c'];
  }
});

console.log(Object.getOwnPropertyNames(p)); // "called"
                                            // [ 'a', 'b', 'c' ]

11. handler.apply(). A trap for a function call.

var p = new Proxy(function() {}, {
  apply: function(target, thisArg, argumentsList) {
    console.log('called: ' + argumentsList.join(', '));
    return argumentsList[0] + argumentsList[1] + argumentsList[2];
  }
});

console.log(p(1, 2, 3)); // "called: 1, 2, 3"
                         // 6

12. handler.construct(). A trap for the new operator.

var p = new Proxy(function() {}, {
  construct: function(target, argumentsList, newTarget) {
    console.log('called: ' + argumentsList.join(', '));
    return { value: argumentsList[0] * 10 };
  }
});

console.log(new p(1).value); // "called: 1"
                             // 10

III. Thực hành sử dụng Proxy object

  • Mình sẽ sử dụng Proxy để wrap lại API Restful axios nhé. Ở đây mình dùng trap get là đủ rồi :slight_smile:
api.posts.then(console.log);
api.comments.then(console.log);
  • Trên chính là thành quả của chúng ta sau khi thực hiện. Ở đây chúng ta ko hề "fix cứng" posts và comments. Do đó mình có thể vác thằng api Proxy này cho mọi end point khác một cách thoải mái 😄

Bước 1: Bắt tay làm quen nào

const target = {};
const handler = {
  get(target, name) {
    console.log(name);
  },
};

const api = new Proxy(target, handler);

api.posts; // => 'posts'
api.comments; // => 'comments'

Nhìn có vẻ ok đấy. Chiến tiếp nào

Bước 2: Chuẩn bị wrap

import axios from 'axios';
axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com/';

const target = {};
const handler = {
  get(target, name) {
    return axios.get(name);
  }
};

const api = new Proxy(target, handler);
api.posts.then(console.log); // => axios response object

Bước 3: Mở rộng

Tuy nhiên mình muốn thêm body vào post method thì làm thế nào nhỉ 🤔

api.posts.post(body).then(console.log);

Và sau một chút custom

const handler = {
  get(target, name) {
    return {
      get() {
        return instance.get(name);
      },
      
      post(body) {
        return instance.post(name, body)
      }
    }
  }
};

Vậy đối mới endpoint URL phức tạp như này /posts/1/comments thì xử lý sao ta 🤔. Và đặc điểm ưu việt của thư viện axios đó là giúp chúng ta convert params object ra query string. Custom tiếp nào

const handler = {
  get(target, name) {
    return {
      get(url, params) {
        return instance.get(name + url, { params });
      },
      
      post(url, body, params) {
        return instance.post(name + url, body, { params })
      }
    }
  }
};

Và thêm một chút tricky để áp dụng cho các method khác. Thành quả cuối cùng

Khá là thú vị phải không các bạn. Chúc các bạn thành công! 😙


Tham khảo: