(渣渣小怪兽翻译,如果看翻译不开心可以看->)
封装意味着隐藏信息.意思是尽可能的隐藏对象内部的部分, 同时暴露出最小公共接口。
最简单和最优雅的方式创建一个封装是使用闭包.可以将闭包创建为具有私有状态的函数.当创建了许多的闭包共享相同的私有状态, 我们就会创建一个Object.
我将开始创建一些在我们开发应用中比较实用的Object: Stack, Queue, Event Emitter, and Timer.以上Object的都使用工厂函数创建。
让我们开始吧。
Stack
栈,是一种数据结构, 它有两个主要操作: push, 往这个集合里增加一个元素; pop, 移除最近添加的元素.其中,push和pop元素依据先进后出原则。
让我们看下一个例子:
let stack = Stack();stack.push(1);stack.push(2);stack.push(3);stack.pop(); //3stack.pop(); //2复制代码
让我们使用工厂函数实现一个stack:
function Stack(){ let list = []; function push(value){ list.push(value); } function pop(){ return list.pop(); } return Object.freeze({ push, pop });}复制代码
这个栈对象有两个公共方法push()和pop().内部的状态只能通过这些方法去改变.
stack.list; // undefined复制代码
我不能直接修改内部状态:
stack.list = 0; //Cannot add property list, object is not extensible复制代码
使用class实现Stack结构
如果我使用类完成相同的实现,则没有实现封装
let stack = new Stack();stack.push(1);stack.push(2);stack.list = 0; //corrupt the private stateconsole.log(stack.pop()); //this.list.pop is not a function复制代码
这是我使用class实现的stack
class Stack { constructor(){ this.list = []; } push(value) { this.list.push(value); } pop() { return this.list.pop(); }}复制代码
如果要看更深的对比(class和工厂函数), 可以看一下这篇
Queue
队列是一种数据结构, 有两个主要操作:入队和出队。
入队: 往我们的集合里增加元素。
出队: 移除在集合里最早加入的元素。
出队和入队操作遵循, 先进先出的原则。
这是使用队列的一个例子
let queue = Queue();queue.enqueue(1);queue.enqueue(2);queue.enqueue(3);queue.dequeue(); //1queue.dequeue(); //2复制代码
以下是队列的实现:
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
一个发布订阅机制是一个有发布和订阅的API的一个对象.它常用于一个应用里两个不同的部分的通信.
来看一个使用的例子:
// 初始化事件对象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事件添加了一个函数.当事件触发发布“updata”事件的时候, doSomething和doSomethingElse都会被调用.
这是事件对象的一个简单实现:
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 });}复制代码
订阅者的状态和通知方法是私有的。
Timer
众所周知, js中有两个计时器函数:setTimeout 和 setInterval.我想以面向对象的方式与计时器一起工作,最终将以如下的方式调用:
let timer = Timer(doSomething, 6000);timer.start();function doSomething(){}复制代码
但是setInterval函数有一些限制,在进行新的回调之前,它不会等待之前的回调执行完成。即使前一个尚未完成,也会进行新的回调。更糟糕的是,在AJAX调用的情况下,响应回调可能会出现故障。
递归setTimeout模式可以解决这个问题.使用这个模式, 一个新的回调形成只能等前一个回到完成之后。
让我们创建一个TimerObject:
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方法是公共,除此之外, 所有的方法和变量都是私有的.调用setTimeout(startRecursiveTimer,interval)时,没有this(指向问题)丢失的上下文问题,因为工厂函数不使用this。
计时器使用返回promise的回调。
现在, 我们可以很简单的实现,当浏览器的tab隐藏的时候, 计时器停止, 当浏览器的tab显示的时候, 计时器继续计时.
document.addEventListener("visibilitychange", toggleTimer);function toggleTimer(){ if(document.visibilityState === "hidden"){ timer.stop(); } if(document.visibilityState === "visible"){ timer.start(); }}复制代码
#总结 JavaScript提供了一种使用工厂函数创建封装对象的独特方法。对象封装状态。
Stack和Queue可以创建为基本数组功能的包装器。
事件对象是在应用程序中的不同部分之间进行通信的对象。
计时器对象易于使用。它有一个清晰的接口start()和stop()。您不必处理管理计时器标识(timerId)的内部部分。
您可以在我的一书中找到有关JavaScript中的功能和面向对象编程的更多内容。