Thứ năm, 19/12/2019 | 00:00 GMT+7

Xem xét tất cả 13 bẫy proxy JavaScript


Proxy là một tính năng JavaScript thực sự thú vị. Nếu bạn thích lập trình meta, bạn có thể đã quen thuộc với chúng. Trong bài viết này, ta sẽ không đi sâu vào các mẫu thiết kế lập trình hoặc lấy meta hoặc thậm chí hiểu cách hoạt động của proxy.

Thông thường các bài viết về bẫy luôn có các ví dụ giống nhau để đặt thuộc tính riêng với proxy. Đó là một ví dụ tuyệt vời. Tuy nhiên, ở đây ta sẽ xem xét tất cả các bẫy bạn có thể sử dụng. Những ví dụ này không phải là trường hợp sử dụng trong thế giới thực, mục đích là giúp bạn hiểu cách thức hoạt động của bẫy Proxy .

Bẫy? Gì? Nghe có vẻ đáng ngại rồi

Tôi không thực sự thích bẫy chữ. Tôi đã đọc ở khắp mọi nơi rằng từ này đến từ domain của hệ điều hành (ngay cả Brendan Eich cũng đề cập đến nó tại JSConfEU 2010). Tuy nhiên tôi không chắc chắn chính xác tại sao. Có thể là do các bẫy trong bối cảnh hệ điều hành đồng bộ và có thể làm gián đoạn quá trình thực thi bình thường của chương trình.

Bẫy là công cụ phát hiện phương pháp nội bộ. Khi nào bạn tương tác với một đối tượng, bạn đang gọi một phương thức nội bộ cần thiết . Proxy cho phép bạn chặn việc thực thi một phương thức nội bộ nhất định.

Vì vậy, khi bạn chạy:

const profile = {};
profile.firstName = 'Jack';

Bạn đang yêu cầu công cụ JavaScript của bạn gọi phương thức nội bộ [[SET]]. Vì vậy, các set bẫy sẽ gọi một chức năng để thực hiện trước khi profile.firstName được cài đặt để 'Jack' .

const kickOutJacksHandler = {
  set: function (target, prop, val) {
    if (prop === 'firstName' && val === 'Jack') {
      return false;
    }
    target[prop] = val;
    return true;
  }
}

Ở đây, cái bẫy set của ta sẽ từ chối bất kỳ chương trình nào cố gắng tạo profile với tên Jack .

const noJackProfile  = new Proxy ({}, kickOutJacksHandler);
noJackProfile.firstName = 'Charles';
// console will show {} 'firstName' 'Charles'
// noJackProfile.firstName === 'Charles'
//This won't work because we don't allow firstName to equal Jack

newProfileProxy.firstName = 'Jack';
// console will show {firstName: 'Charles'} 'firstName' 'Charles'
// noJackProfile.firstName === 'Charles'

Tôi có thể làm gì Proxy?

Bất cứ điều gì thỏa mãn:

typeof MyThing === 'object'

Điều này nghĩa là mảng, hàm, đối tượng và thậm chí…

console.log(typeof new Proxy({},{}) === 'object')
// logs 'TRUE' well actually just true... I got a bit excited...

GIỚI THIỆU! Bạn chỉ không thể proxy bất cứ thứ gì nếu trình duyệt của bạn không hỗ trợ vì không có các tùy chọn chuyển vị hoặc đa chức năng đầy đủ (thêm về điều đó trong một bài đăng khác).

Tất cả các bẫy proxy

Có 13 bẫy trong JavaScript! Tôi đã chọn không phân loại chúng, tôi sẽ trình bày chúng từ những gì tôi nghĩ là hữu ích nhất đến ít hữu ích hơn (đại loại). Nó không phải là một phân loại chính thức và vui lòng không đồng ý. Tôi thậm chí không bị thuyết phục bởi xếp hạng của chính mình.

Trước khi ta bắt đầu, đây là một cheat sheets nhỏ được lấy từ đặc tả ECMAScript :

Phương pháp nội bộ Phương pháp xử lý
[[Được]] được
[[Xóa bỏ]] deleteProperty
[[OwnPropertyKeys]] ownKeys
[[HasProperty]]
[[Gọi]] ứng dụng
[[DefineOwnProperty]] xác định
[[GetPrototypeOf]] getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf
[[Có thể mở rộng]] isExtensible
[[Dự phòng mở rộng]] ngăn chặn
[[GetOwnProperty]] getOwnPropertyDescriptor
[[Liệt kê]] liệt kê
[[Xây dựng]] xây dựng

Nhận, Đặt và Xóa: Siêu cơ bản

Ta đã thấy set , hãy xem xét getdelete . Lưu ý phụ: khi bạn sử dụng set hoặc delete bạn phải trả về true hoặc false để báo cho JavaScript engine biết khóa có nên được sửa đổi hay không.

const logger = []

const loggerHandler = {
  get: function (target, prop) {
    logger.push(`Someone  accessed '${prop}' on object ${target.name} at ${new Date()}`);
    return target[prop] || target.getItem(prop) || undefined;
  },
}

const secretProtectorHandler = {
  deleteProperty: function (target, prop) {
    // If the key we try to delete contains to substring 'secret' we don't allow the user to delete it
    if (prop.includes('secret')){
      return false;
    }
    return true;
  }
};

const sensitiveDataProxy = new Proxy (
  {name:'Secret JS Object', secretOne: 'I like weird JavaScript Patterns'},
  {...loggerHandler, ...secretProtectorHandler}
);

const {secretOne} = sensitiveDataProxy;
//logger = ['Someone tried to accessed 'secretOne' on object Secret JS Object at Mon Dec 09 2019 23:18:54 GMT+0900 (Japan Standard Time)']

delete sensitiveDataProxy.secretOne;
// returns false it can't be deleted!

// sensitiveDataProxy equals  {name: 'Secret JS Object', secretOne: 'I like weird JavaScript Patterns'}

Chơi với phím

Giả sử ta có một web server nhận một số dữ liệu ứng dụng đến tuyến đường của ta . Ta muốn giữ dữ liệu đó trong bộ điều khiển của ta . Nhưng có lẽ ta muốn đảm bảo nó không bị lạm dụng. Bẫy ownKeys sẽ kích hoạt một lần khi ta cố gắng truy cập vào các khóa của đối tượng.

const createProxiedParameters  = (reqBody, allowed) => {
  return new Proxy (reqBody, {
    ownKeys: function (target) {
      return Object.keys(target).filter(key => allowed.includes(key))
    }
  });
};

const allowedKeys = ['firstName', 'lastName', 'password'];

const reqBody = {lastName:'Misteli', firstName:'Jack', password:'pwd', nefariousCode:'MWUHAHAHAHA'};

const proxiedParameters = createProxiedParameters(reqBody, allowedKeys);

const parametersKeys =  Object.keys(proxiedParameters)
// parametersKeys equals ["lastName", "firstName", "password"]
const parametersValues = parametersKeys.map(key => reqBody[key]);
// parameterValues equals ['Misteli', 'Jack', 'pwd']

for (let key in proxiedParameters) {
  console.log(key, proxiedParameters[key]);
}
// logs:
// lastName Misteli
// firstName Jack
// password pwd

// The trap will also work with these functions
Object.getOwnPropertyNames(proxiedParameters);
// returns ['lastName', 'firstName', 'password']
Object.getOwnPropertySymbols(proxiedParameters);
// returns []

Trong một ứng dụng thực, bạn KHÔNG nên làm sạch các thông số của bạn như thế này. Tuy nhiên, bạn có thể xây dựng một hệ thống phức tạp hơn dựa trên proxy.

Quá tải trong Mảng

Bạn đã luôn mơ ước được sử dụng toán tử in với các mảng, nhưng luôn quá ngại ngùng không biết làm thế nào?

function createInArray(arr) {
  return new Proxy(arr, {
    has: function (target, prop) {
      return target.includes(prop);
    }
  });
};

const myCoolArray  =  createInArray(['cool', 'stuff']);
console.log('cool' in myCoolArray);
// logs true
console.log('not cool' in myCoolArray);
// logs false

Các has phương pháp bẫy chặn mà cố gắng để kiểm tra xem một tài sản tồn tại trong một đối tượng sử dụng in điều hành.

Chức năng Kiểm soát Tốc độ Cuộc gọi với Áp dụng

apply được sử dụng để chặn các cuộc gọi hàm. Ở đây ta sẽ xem xét một proxy bộ nhớ đệm rất đơn giản.

createCachedFunction nhận đối số func . 'CachedFunction' có một bẫy apply (hay còn gọi là [[Call]] ) được gọi mỗi khi ta chạy cachedFunction(arg) . Trình xử lý của ta cũng có một thuộc tính cache lưu trữ các đối số được sử dụng để gọi hàm và kết quả của hàm. Trong bẫy [[Call]] / apply ta kiểm tra xem hàm đã được gọi với đối số đó chưa. Nếu vậy, ta trả về kết quả được lưu trong bộ nhớ cache. Nếu không, ta tạo một mục mới trong bộ nhớ cache của ta với kết quả được lưu trong bộ nhớ cache.

Đây không phải là một giải pháp hoàn chỉnh. Có rất nhiều cạm bẫy. Tôi đã cố gắng diễn đạt ngắn gọn để dễ hiểu hơn. Giả định của ta là đầu vào và kết quả của hàm là một số hoặc một chuỗi duy nhất và hàm proxied luôn trả về cùng một kết quả cho một đầu vào nhất định.

const createCachedFunction = (func) => {
  const handler = {
    // cache where we store the arguments we already called and their result
    cache : {},
    // applu is the [[Call]] trap
    apply: function (target, that, args) {
      // we are assuming the function only takes one argument
      const argument = args[0];
      // we check if the function was already called with this argument
      if (this.cache.hasOwnProperty(argument)) {
        console.log('function already called with this argument!');
        return this.cache[argument];
      }
      // if the function was never called we call it and store the result in our cache
      this.cache[argument] = target(...args);
      return this.cache[argument];
    }
  }
  return new Proxy(func, handler);
};

// awesomeSlowFunction returns an awesome version of your argument
// awesomeSlowFunction resolves after 3 seconds
const awesomeSlowFunction = (arg) => {
  const promise = new Promise(function(resolve, reject) {
    window.setTimeout(()=>{
      console.log('Slow function called');
      resolve('awesome ' + arg);
      }, 3000);
    });
  return promise;
};

const cachedFunction = createCachedFunction(awesomeSlowFunction);

const main = async () => {
  const awesomeCode = await cachedFunction('code');
  console.log('awesomeCode value is: ' + awesomeCode);
  // After 3 seconds (the time for setTimeOut to resolve) the output will be :
  // Slow function called
  //  awesomeCode value is: awesome code

  const awesomeYou = await cachedFunction('you');
  console.log('awesomeYou value is: ' + awesomeYou);
    // After 6 seconds (the time for setTimeOut to resolve) the output will be :
  // Slow function called
  //  awesomeYou value is: awesome you

  // We are calling cached function with the same argument
  const awesomeCode2 = await cachedFunction('code');
  console.log('awesomeCode2 value is: ' + awesomeCode2);
  // IMMEDIATELY after awesomeYou resolves the output will be:
  // function already called with this argument!
  // awesomeCode2 value is: awesome code
}

main()

Đoạn mã này hơi khó nhai hơn các đoạn mã khác. Nếu bạn không hiểu mã, hãy thử sao chép / dán mã đó trong console dành cho nhà phát triển của bạn và thêm một số console.log() hoặc thử các chức năng bị trì hoãn của bạn .

DefineProperty

defineProperty thực sự tương tự như set , nó được gọi khi nào Object.defineProperty được gọi, nhưng cũng có thể khi bạn cố gắng đặt một thuộc tính bằng cách sử dụng = . Bạn nhận được một số chi tiết hơn với đối số descriptor bổ sung. Ở đây ta sử dụng defineProperty giống như một trình xác nhận. Ta kiểm tra rằng các thuộc tính mới không thể ghi hoặc liệt kê được. Ngoài ra, ta sửa đổi age tính đã xác định để kiểm tra xem tuổi có phải là một số hay không.

const handler = {
  defineProperty: function (target, prop, descriptor) {
    // For some reason we don't accept enumerable or writeable properties 
    console.log(typeof descriptor.value)
    const {enumerable, writable} = descriptor
    if (enumerable === true || writable === true)
      return false;
    // Checking if age is a number
    if (prop === 'age' && typeof descriptor.value != 'number') {
      return false
    }
    return Object.defineProperty(target, prop, descriptor);
  }
};

const profile = {name: 'bob', friends:['Al']};
const profileProxied = new Proxy(profile, handler);
profileProxied.age = 30;
// Age is enumerable so profileProxied still equals  {name: 'bob', friends:['Al']};

Object.defineProperty(profileProxied, 'age', {value: 23, enumerable: false, writable: false})
//We set enumerable to false so profile.age === 23

Xây dựng

apply và gọi là hai bẫy hàm. construct các lệnh chặn toán tử new . Tôi thấy ví dụ của MDN về phần mở rộng hàm tạo hàm thực sự thú vị. Vì vậy, tôi sẽ chia sẻ version đơn giản hóa của tôi về nó.

const extend = (superClass, subClass) => {
  const handler = {
    construct: function (target, args) {
      const newObject = {}
      // we populate the new object with the arguments from
      superClass.call(newObject, ...args);
      subClass.call(newObject, ...args);
      return newObject;
    },
  }
  return  new Proxy(subClass, handler);
}

const Person = function(name) {
  this.name = name;
};

const Boy = extend(Person, function(name, age) {
  this.age = age;
  this.gender = 'M'
});

const Peter = new Boy('Peter', 13);
console.log(Peter.gender);  // 'M'
console.log(Peter.name); // 'Peter'
console.log(Peter.age);  // 13

Đừng nói với tôi phải làm gì!

Object.isExtensible kiểm tra xem ta có thể thêm thuộc tính vào một đối tượng hay không và Object.preventExtensions cho phép ta ngăn các thuộc tính được thêm vào. Trong đoạn mã này, ta tạo một thủ thuật hoặc giao dịch xử lý. Hãy tưởng tượng một đứa trẻ đi đến một cánh cửa, yêu cầu đồ ăn vặt nhưng nó không biết số kẹo tối đa mà nó có thể nhận được là bao nhiêu. Nếu anh ta hỏi anh ta có thể nhận được bao nhiêu, tiền trợ cấp sẽ giảm xuống.

function createTrickOrTreatTransaction(limit) {
  const extensibilityHandler = {
    preventExtensions:  function (target) {
      target.full = true;
      // this will prevent the user from even changing the existing values
      return  Object.freeze(target);
    },
    set:  function (target, prop, val) {
      target[prop] = val;
      const candyTotal = Object.values(target).reduce((a,b) => a + b, 0) - target.limit;

      if (target.limit - candyTotal <= 0) {
        // if you try to cheat the system and get more that your candy allowance, we clear your bag
        if (target.limit - candyTotal < 0 )
          target[prop] = 0;
        // Target is frozen so we can't add any more properties

        this.preventExtensions(target);
      }  
    },
    isExtensible: function (target) {
      // Kids can check their candy limit 
      console.log( Object.values(target).reduce((a,b) => a + b, 0) - target.limit);
      // But it will drop their allowance by one
      target.limit -= 1;
      // This will return the sum of all our keys
      return Reflect.isExtensible(target);
    }
  }
  return new Proxy ({limit}, extensibilityHandler);
};

const candyTransaction = createTrickOrTreatTransaction(10);

Object.isExtensible(candyTransaction);
// console will log 10
// Now candyTransaction.limit = 9

candyTransaction.chocolate  = 6;

// The candy provider got tired and decided to interrupt the negotiations early
Object.preventExtensions(candyTransaction);
// now candyTransaction equals to {limit: 9, chocolate: 6, full: true}

candyTransaction.chocolate = 20;
//  candyBag equals to {limit: 9, chocolate: 6, full: true}
// Chocolates did not go change to 20 because we called freeze in the preventExtensions trap

const secondCandyTransaction = createTrickOrTreatTransaction(10);

secondCandyTransaction.reeses = 8;
secondCandyTransaction.nerds = 30;
// secondCandyTransaction equals to {limit: 10, reeses: 8, nerds: 0, full: true}
// This is because we called preventExtensions inside the set function if a kid tries to shove in extra candies

secondCandyTransaction.sourPatch = 30;
// secondCandyTransaction equals to {limit: 10, reeses: 8, nerds: 0, full: true}

GetOwnPropertyDescriptor

Muốn thấy điều gì đó kỳ lạ?

let candies = new Proxy({}, {
  // as seen above ownKeys is called once before we iterate
  ownKeys(target) {
    console.log('in own keys', target);
    return ['reeses', 'nerds', 'sour patch'];
  },
// on the other end getOwnPropertyDescriptor at every iteration
  getOwnPropertyDescriptor(target, prop) { 
    console.log('in getOwnPropertyDescriptor', target, prop);
    return {
      enumerable: false,
      configurable: true
    };
  }
});

const candiesObject = Object.keys(candies);
// console will log:
// in own keys {}
// in getOwnPropertyDescriptor {} reeses
// in getOwnPropertyDescriptor {} nerds
// in getOwnPropertyDescriptor {} sour patch
// BUT ! candies == {} and candiesObject == []

Điều này là do ta đặt có thể liệt kê là sai. Nếu bạn đặt enumerable thành true thì candiesObject sẽ bằng ['reeses', 'nerds', 'sour patch'] .

Lấy và Cài đặt Nguyên mẫu

Không chắc khi nào điều này sẽ có ích. Thậm chí không chắc khi nào setPrototypeOf trở nên hữu ích nhưng đây rồi. Ở đây ta sử dụng bẫy setPrototype để kiểm tra xem nguyên mẫu của đối tượng của ta có bị giả mạo hay không.

const createSolidPrototype = (proto) => {
  const handler = {
    setPrototypeOf: function (target, props) {
      target.hasBeenTampered = true;
      return false;
    },
    getPrototypeOf: function () {
      console.log('getting prototype')
    },
    getOwnProperty: function() {
      console.log('called: ' + prop);
      return { configurable: true, enumerable: true, value: 10 };
    }
  };
};

Liệt kê

Liệt kê cho phép ta chặn for...in , nhưng rất tiếc, ta không thể sử dụng nó kể từ ECMAScript 2016. Bạn có thể tìm thêm về quyết định đó trong ghi chú cuộc họp TC39 này.

Tôi đã thử nghiệm một tập lệnh trên Firefox 40 để bạn không nói rằng tôi đã nói dối bạn khi tôi hứa với 13 cái bẫy.

const alphabeticalOrderer = {
  enumerate: function (target) {
    console.log(target, 'enumerating');
    // We are filtering out any key that has a number or capital letter in it and sorting them
    return Object.keys(target).filter(key=> !/\d|[A-Z]/.test(key)).sort()[Symbol.iterator]();
  }
};

const languages = {
  france: 'French',
  Japan: 'Japanese',
  '43J': '32jll',
  alaska: 'American'
};

const languagesProxy = new Proxy (languages, alphabeticalOrderer);

for (var lang in languagesProxy){
  console.log(lang);
}
// console outputs:
// Object { france: 'French', japan: 'Japanese', 43J: '32jll', alaska: 'American' } enumerating
// alaska
// france

// Usually it would output
// france
// Japan
// 43J
// alaska

Bạn có thể nhận thấy rằng ta không sử dụng `Reflect` để làm cho mọi thứ đơn giản hơn. Ta sẽ đề cập đến reflect trong một bài đăng khác. Trong thời gian chờ đợi, tôi hy vọng bạn đã vui vẻ. Ta cũng sẽ xây dựng một phần mềm thực tế để thực hành nhiều hơn vào lần tới.

table {width: 100%; } table.color-names tr th, table.color-names tr td {font-size: 1.2rem; } <p> bảng {border-sập: sụp đổ; khoảng cách đường viền: 0; background: var (–bg); border: 1px solid var (–gs0); bố cục bảng: auto; margin: 0 auto} table thead {background: var (–bg3)} table thead tr th {padding: .5rem .625rem .625rem; kích thước phông chữ: 1.625rem; font-weight: 700; color: var (–text-color)} table tr td, table tr th {padding: .5625rem .625rem; kích thước phông chữ: 1.5rem; color: var (–text-color); text-align: center} table tr: nth-of-type (Even) {background: var (–bg3)} table tbody tr td, table tbody tr th, table thead tr th, table tr td {display: table-cell ; line-height: 2.8125rem}


Tags:

Các tin liên quan

Khám phá phương thức indexOf cho chuỗi và mảng trong JavaScript
2019-12-17
Thao tác DOM trong JavaScript với innerText và innerHTML
2019-12-14
Cách gói một gói JavaScript Vanilla để sử dụng trong React
2019-12-12
Cách phát triển một trình tải lên tệp tương tác với JavaScript và Canvas
2019-12-12
Cách sử dụng map (), filter () và Reduce () trong JavaScript
2019-12-12
Giải thích về lập trình chức năng JavaScript: Ứng dụng một phần và làm xoăn
2019-12-12
Giới thiệu về Closures và Currying trong JavaScript
2019-12-12
Bắt đầu với các hàm mũi tên ES6 trong JavaScript
2019-12-12
Cách đếm số nguyên âm trong một chuỗi văn bản bằng thuật toán JavaScript
2019-12-12
Cách sử dụng phép gán cấu trúc hủy trong JavaScript
2019-12-12