我们在之前的系列文章中,陆续介绍了:
今天继续给大家带来“享元模式”的介绍。
享元模式(
Flyweight Pattern
)是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象。
享元模式用来尽可能减少内存使用量,以及与尽可能多的相似对象共享数据。
它适合用于当大量物件只是重复因而导致无法令人接受的使用大量内存。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。
享元模式区分了内部状态
和外部状态
,所以我们可以通过设置不同的外部状态,使得相同的对象可以具备一些不同的特性,而内部状态设置为相同部分。
如何利用享元模式呢?这里我们只需要将他们少部分不同的部分,当做参数移动到类实例的外部去,然后在方法调用的时候将他们传递过来就可以了。
那么如果在 JavaScript
中应用享元模式呢?
有两种方式:
享元模式在 JavaScript 中非常重要,它是用于性能优化的一种常见模式。
它依靠 减少创建对象实例的数量 以及 运用共享技术来有效支持大量细粒度的对象 这两种方式减少内存占用,以提高性能。
在 JavaScript
中,浏览器特别是移动端的浏览器,所占有的内存并不算多,因此合理利用享元模式,达到节省内存的目的,是一项非常有意义的优化。
在 Java 中有一个关键字:implements
,它用于接入接口 interfaces
,这在 JavaScript
语言中并不存在,但是我们仍然可以模拟一个:
Function.prototype.implementsFor = function (parentClassOrObject) {
if (parentClassOrObject.constructor === Function) {
// Normal Inheritance
this.prototype = new parentClassOrObject();
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject.prototype;
} else {
// Pure Virtual Inheritance
this.prototype = parentClassOrObject;
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject;
}
return this;
};
我们看:implementsFor
作用于一个构造函数,它接受一个父类(function
)或者一个 object
,并继承该父类构造函数(function
)或者指定的 object
。上段代码并不难理解,我们看一个应用实例:
// Flyweight object
var CoffeeOrder = {
// Interfaces
serveCoffee: function (context) {},
getFlavor: function () {},
};
// ConcreteFlyweight object that creates ConcreteFlyweight
// Implements CoffeeOrder
function CoffeeFlavor(newFlavor) {
var flavor = newFlavor;
// If an interface has been defined for a feature
// implement the feature
if (typeof this.getFlavor === 'function') {
this.getFlavor = function () {
return flavor;
};
}
if (typeof this.serveCoffee === 'function') {
this.serveCoffee = function (context) {
console.log(
'Serving Coffee flavor ' +
flavor +
' to table number ' +
context.getTable()
);
};
}
}
// Implement interface for CoffeeOrder
CoffeeFlavor.implementsFor(CoffeeOrder);
// Handle table numbers for a coffee order
function CoffeeOrderContext(tableNumber) {
return {
getTable: function () {
return tableNumber;
},
};
}
function CoffeeFlavorFactory() {
var flavors = {},
length = 0;
return {
getCoffeeFlavor: function (flavorName) {
var flavor = flavors[flavorName];
if (typeof flavor === 'undefined') {
flavor = new CoffeeFlavor(flavorName);
flavors[flavorName] = flavor;
length++;
}
return flavor;
},
getTotalCoffeeFlavorsMade: function () {
return length;
},
};
}
// Sample usage:
// testFlyweight()
function testFlyweight() {
// The flavors ordered.
var flavors = [],
// The tables for the orders.
tables = [],
// Number of orders made
ordersMade = 0,
// The CoffeeFlavorFactory instance
flavorFactory = new CoffeeFlavorFactory();
function takeOrders(flavorIn, table) {
flavors.push(flavorFactory.getCoffeeFlavor(flavorIn));
tables.push(new CoffeeOrderContext(table));
ordersMade++;
}
takeOrders('Cappuccino', 2);
takeOrders('Cappuccino', 2);
takeOrders('Frappe', 1);
takeOrders('Frappe', 1);
takeOrders('Xpresso', 1);
takeOrders('Frappe', 897);
takeOrders('Cappuccino', 97);
takeOrders('Cappuccino', 97);
takeOrders('Frappe', 3);
takeOrders('Xpresso', 3);
takeOrders('Cappuccino', 3);
takeOrders('Xpresso', 96);
takeOrders('Frappe', 552);
takeOrders('Cappuccino', 121);
takeOrders('Xpresso', 121);
for (var i = 0; i < ordersMade; ++i) {
flavors[i].serveCoffee(tables[i]);
}
console.log(' ');
console.log(
'total CoffeeFlavor objects made: ' +
flavorFactory.getTotalCoffeeFlavorsMade()
);
}
这个例子中,CoffeeFlavor
接入了 CoffeeOrder
的接口。接口的概念也许对于传统的 JavaScript
开发者有些陌生,我们再来看一个更加具有表现力的例子。
假设在图书管理系统中,每本书都有以下特性:
同时我们需要以下属性来追踪每一本书时,记录它是否可用、归还时间等:
那么 Book
这个类看上去就像:
var Book = function(id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability) {
this.id = id
this.title = title
this.author = author
this.genre = genre
this.pageCount = pageCount
this.publisherID = publisherID
this.ISBN = ISBN
this.checkoutDate = checkoutDate
this.checkoutMember = checkoutMember
this.dueReturnDate = dueReturnDate
this.availability = availability
}
Book.prototype = {
getTitle: function() {
return this.title
},
getAuthor: function() {
return this.author
},
getISBN: function() {
return this.ISBN
},
updateCheckoutStatus: function(bookID, newStatus, checkoutDate, checkoutMember, newReturnDate) {
this.id = bookID
this.availability = newStatus
this.checkoutDate = checkoutDate
this.checkoutMember = checkoutMember
this.dueReturnDate = newReturnDate
},
extendCheckoutPeriod: function(bookID, newReturnDate) {
this.id = bookID
this.dueReturnDate = newReturnDate
},
isPastDue: function() {
var currentDate = new Date()
return currentDate.getTime() > Date.parse(this.dueReturnDate)
}
}
这么看上去并没有什么问题,但是当图书增多时,对于系统的压力会逐渐增多。
为此我们将书的属性分为两种:本身固有的和外在特性。本身固有的属性包括 title
、author
等,外在特性包括 checkoutMember
、dueReturnDate
等。这样一来,我们简化书的构造函数为:
var Book = function (title, author, genre, pageCount, publisherID, ISBN) {
this.title = title;
this.author = author;
this.genre = genre;
this.pageCount = pageCount;
this.publisherID = publisherID;
this.ISBN = ISBN;
};
我们将外在特性删去,check-outs
等信息将会被移动到一个新的类中,一个新的工厂函数也将出现:
var BookFactory = (function () {
var existingBooks = {},
existingBook;
return {
createBook: function (title, author, genre, pageCount, publisherID, ISBN) {
// Find out if a particular book meta-data combination has been created before
// !! or (bang bang) forces a boolean to be returned
existingBook = existingBooks[ISBN];
if (!!existingBook) {
return existingBook;
} else {
// if not, let's create a new instance of the book and store it
var book = new Book(title, author, genre, pageCount, publisherID, ISBN);
existingBooks[ISBN] = book;
return book;
}
},
};
})();
在这个工厂函数中,我们将会检查当前需要创建的书籍是否已经存在,如果存在直接返回书实例;否则进行调用 Book
构造函数进行创建。这保证了所有的书都是唯一的,而不存在重复。
对于书的外在特性,我们创建 BookRecordManager
来维护每一本书的状态,并通过 bookId
与每一个本书进行关系创建。
var BookRecordManager = (function() {
var bookRecordDatabase = {}
return {
addBookRecord: function(id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability) {
var book = BookFactory.createBook(title, author, genre, pageCount, publisherID, ISBN)
bookRecordDatabase[id] = {
checkoutMember: checkoutMember,
checkoutDate: checkoutDate,
dueReturnDate: dueReturnDate,
availability: availability,
book: book
}
},
updateCheckoutStatus: function(bookID, newStatus, checkoutDate, checkoutMember, newReturnDate) {
var record = bookRecordDatabase[bookID]
record.availability = newStatus
record.checkoutDate = checkoutDate
record.checkoutMember = checkoutMember
record.dueReturnDate = newReturnDate
},
extendCheckoutPeriod: function(bookID, newReturnDate) {
bookRecordDatabase[bookID].dueReturnDate = newReturnDate
},
isPastDue: function(bookID) {
var currentDate = new Date()
return currentDate.getTime() > Date.parse(bookRecordDatabase[bookID].dueReturnDate)
}
}
})()
书目所有的外在特性都被从书本身的特性中抽离,对于书借入/借出的操作也移动到了 BookRecordManager
当中,因为这些方法需要直接操作书的外在特性。如此一来,比一本书拥有全部属性的实现方式更加高效,也更利于维护。
比如有 30 本一样的书,在现有的模式下,只需要存储了一个实例。
享元模式在前端还有更多的应用,比如事件代理就是一个很典型的体现:
<div id="container">
<div class="toggle" href="#">
<span class="info">1</span>
</div>
<div class="toggle" href="#">
<span class="info">2</span>
</div>
</div>
// 我们集中将事件处理放到父容器上
var stateManager = {
fly: function() {
var self = this
$('#container')
.unbind()
.on('click', 'div.toggle', function(e) {
self.handleClick(e.target)
})
},
handleClick: function(elem) {
$(elem)
.find('span')
.toggle('slow')
}
}
来总结下享元模式的优缺点。
优点:
缺点:
《前端面试题宝典》经过近一年的迭代,现已推出 小程序 和 电脑版刷题网站 (https://fe.ecool.fun/),欢迎大家使用~
同时,我们还推出了面试辅导的增值服务,可以为大家提供 “简历指导” 和 “模拟面试” 服务,感兴趣的同学可以联系小助手(微信号:interview-fe)进行报名。