7 Июня 2018 JavaScript Перевод

Обзор некоторых JavaScript объектов, имеющих инкапсуляцию

img

Инкапсуляция – это скрытие информации. Речь идёт о том, чтобы скрыть как можно больше внутренних частей объекта и открыть минимальный публичный интерфейс.

Самый простой и элегантный способ создания инкапсуляции в JavaScript – это использование замыканий. Замыкание создаётся как функция с приватным состоянием. При создании множества замыканий, разделяющих одно и то же приватное состояние, мы создаём объект.

Я создам несколько объектов, которые могут быть полезны в приложениях: stack (стек), queue (очередь), event emitter (эмиттер событий) и timer (таймер).

Давайте начнём.

Stack (стек)

Stack – структура данных с двумя основными операциями: push для добавления элемента в коллекцию и pop для удаления последнего добавленного элемента. В stack можно добавлять и удалять элементы в соответствии с принципом Last In First Out (последним пришёл - первым ушёл).

Посмотрите на следующий пример:

let stack = Stack();
stack.push(1);
stack.push(2);
stack.push(3);
stack.pop(); //3
stack.pop(); //2

Давайте реализуем стек, используя фабричную функцию:

function Stack(){
 let list = [];
 
 function push(value){ list.push(value); }
 function pop(){ return list.pop(); }
 
 return Object.freeze({
   push,
   pop
 });
}

Объект stack содержит открытые методы push() и pop(). Внутреннее состояние изменяется только с помощью этим методов.

stack.list; //undefined

Я не могу напрямую изменить внутреннее состояние:

stack.list = 0;//Cannot add property list, object is not extensible

Реализация stack, используя class

Если я буду выполнять ту же реализацию с использованием class, то инкапсуляции не будет.

let stack = new Stack();
stack.push(1);
stack.push(2);
stack.list = 0; //corrupt the private state
console.log(stack.pop()); //this.list.pop is not a function

Вот реализация стека с помощью класса:

class Stack {
 constructor(){
   this.list = [];
 }
 
 push(value) { this.list.push(value); }
 pop() { return this.list.pop(); }
}

Queue (очередь)

Queue – структура данных с двумя основными операциями: enqueue для добавления элемента в коллекцию и dequeue для удаления первого элемента из коллекции. В queue можно добавлять и удалять элементы в соответствии с принципом First In First Out (первым пришёл — первым ушёл).

Вот пример использования queue:

let queue = Queue();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.dequeue(); //1
queue.dequeue(); //2

Ниже приведён пример реализации queue:

function Queue(){
 let list = [];
 
 function enqueue(value){ list.push(value); }
 function dequeue(){ return list.shift(); }
 
 return Object.freeze({
   enqueue,
   dequeue
 });
}

Как мы видели ранее, внутреннее состояние объекта не доступно извне:

queue.list; //undefined

Event emitter (эмиттер событий)

Event emitter – объект с API publish/subscribe. Он используется для связи между различными частями приложения.

Взгляните на следующий пример:

let eventEmitter = EventEmitter();
eventEmitter.subscribe("update", doSomething);
eventEmitter.subscribe("update", doSomethingElse);
eventEmitter.subscribe("add", doSomethingOnAdd);
eventEmitter.publish("update", {});
function doSomething(value) { }
function doSomethingElse(value) { }
function doSomethingOnAdd(value) { }

Выше, я подписываюсь на две функции для события update, и одну функция для события add. Когда event emitter вызывается через publish с событием update, срабатывают doSomething и doSomethingElse.

Вот реализация простого Event Emitter:

function EventEmitter(){
 let subscribers = [];
 
 function subscribe(type, callback){
   subscribers[type] = subscribers[type] || [];
   subscribers[type].push(callback); 
 }
 
 function notify(value, fn){
   try {
     fn(value);
   }
   catch(e) { console.error(e); }
 }
 
 function publish(type, value){
   if(subscribers[type]){
     let notifySubscriber = notify.bind(null, value);
     subscribers[type].forEach(notifySubscriber);
   }
 }
 
 return Object.freeze({
   subscribe,
   publish
 });
}

Subscribers(подписчики) состояния и метод notify (уведомление) являются закрытыми.

Timer (таймер)

JavaScript имеет две хорошо известные функции таймера: setTimeout и setInterval. Я хотел бы работать с таймерами объектно-ориентированным способом и делать что-то вроде:

let timer = Timer(doSomething, 6000);
timer.start();
function doSomething(){}

Функция setInterval имеет некоторые ограничения. Она не дожидается завершения предыдущего вызова перед тем, как совершить новый. Новый вызов выполнится, даже если предыдущий еще не закончен. Хуже того, в случае AJAX вызовов, ответы обратных вызовов могут выйти из строя.

Рекурсивный setTimeout шаблон решает эту ситуацию. При его использовании, новый вызов будет выполняться только тогда, когда закончен предыдущий (вот пример).

Давайте создадим объект Timer:

function Timer(fn, interval){
let timerId;
function startRecursiveTimer(){
 fn().then(function makeNewCall(){
   timerId = setTimeout(startRecursiveTimer, interval);
 });
}
function stop(){
 if(timerId){
   clearTimeout(timerId);
   timerId = 0;
 }
}
function start(){
 if(!timerId){
   startRecursiveTimer();
 }
}
return Object.freeze({
 start,
 stop
}); 
}
let timer = Timer(getTodos, 2000);
timer.start();

Публичными являются только методы start и stop. Все остальные приватные. Также, отсутствует проблема с this при вызове setTimeout (startRecursiveTimer, interval), поскольку фабричные функции не используют this.

Timer работает с функцией обратного вызова, которая возвращает promise.

Теперь, можно легко сделать что-то вроде остановки таймера, когда вкладка браузера скрыта, а затем запустить его обратно, когда вкладка будет видна:

document.addEventListener("visibilitychange", toggleTimer);
function toggleTimer(){
  if(document.visibilityState === "hidden"){
    timer.stop();
  }
  if(document.visibilityState === "visible"){
    timer.start();
  }
}

Выводы

JavaScript предлагает уникальный способ создания инкапсулированных объектов с использованием фабричных функций. Объекты инкапсулируют состояние.

Stack и Queue могут быть созданы как обёртки вокруг основных функций массива.

Event emitter – объект, который взаимодействует между различными частями приложения.

Объект timer прост в использовании. Он имеет чистый интерфейс start() и stop(). Вам не нужно иметь дело с внутренними частями управления таймера.

Оригинал: Here are some practical JavaScript objects that have encapsulation