本文深入解析JavaScript中的享元模式(Flyweight Pattern),探讨其通过共享细粒度对象来降低内存开销的核心机制。文章详细演示了如何在数据层利用内部状态与外部状态分离优化大量图书对象,以及在DOM层通过事件委托和单例封装提升页面性能。通过具体代码案例,展示了如何构建工厂管理与外部状态管理器,帮助开发者在实际项目中有效应用该模式。
享元模式概述
介绍
享元模式(Flyweight)旨在通过共享技术有效支持大量细粒度的对象,从而避免创建大量内容相似的小类所带来的内存开销,实现多个对象共享同一个类(元类)实例。
该模式能有效减少非常相似类的数量。在程序设计中,若需生成大量细粒度类实例来表示数据,且发现这些实例仅少数参数不同、其余开销基本相同时,可大幅减少需实例化的类数量。将可变参数移至类实例外部并在方法调用时传入,即可通过共享大幅降低单个实例的数量。
在JavaScript中应用享元模式主要有两种方式:一是应用于数据层,处理内存中大量相似对象;二是应用于DOM层,例如在中央事件管理器中使用享元模式,避免为父容器内的每个子元素单独附加事件句柄。
数据层中的享元模式应用
享元与数据层
Flyweight模式包含两个重要概念:内部状态(intrinsic)和外部状态(extrinsic)。内部状态由对象内部方法管理,而外部信息则可在外部进行删除或保存。
通俗来说,即先创建一个原始模型,随后根据不同场合和环境生成具有特定特征的具体模型。由于需要产生不同的新对象,Flyweight模式中常结合Factory模式。Flyweight的内部状态用于共享,而Flyweight工厂负责维护一个Flyweight池(模式池)来存放内部状态的对象。
使用享元模式
下面通过一个类库演示如何管理系统中的所有书籍,每本书的元数据暂定为以下内容:
代码如下:
ID
Title
Author
Genre
Page count
Publisher ID
ISBN此外,还需定义每本书的借出时间、借书人、退书日期以及是否可用的状态:
代码如下:
checkoutDate
checkoutMember
dueReturnDate
availability初始的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;
},
/*其它get方法在这里就不显示了*/
// 更新借出状态
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(bookID){
var currentDate = new Date();
return currentDate.getTime() > Date.parse(this.dueReturnDate);
}
};程序初期可能运行正常,但随着时间推移,图书数量大幅增加且每种图书有不同版本和数量,系统性能会逐渐下降。内存中数千个book对象将导致显著的性能瓶颈,此时需引入享元模式进行优化。
可将数据分为内部和外部两种状态。与book对象相关的数据(如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;
};
定义基本工厂
接下来定义一个基本工厂,用于检查之前是否已创建该book对象。若存在则直接返回,否则创建新对象并存储以便后续访问,确保每种书仅创建一个对象实例:
代码如下:
/* Book工厂 单例 */
var BookFactory = (function(){
var existingBooks = {};
return{
createBook: function(title, author, genre,pageCount,publisherID,ISBN){
/*查找之前是否创建*/
var existingBook = existingBooks[ISBN];
if(existingBook){
return existingBook;
}else{
/* 如果没有,就创建一个,然后保存*/
var book = new Book(title, author, genre,pageCount,publisherID,ISBN);
existingBooks[ISBN] = book;
return book;
}
}
}
});管理外部状态
外部状态的管理相对简单,除了封装好的book对象外,其他状态需在此处统一管理:
代码如下:
/*BookRecordManager 借书管理类 单例*/
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);
}
};
});通过这种方式,同一种图书的相同信息仅保存在一个bookmanager对象中,且只保留一份副本。相比之前的代码,这种优化显著节省了内存空间。
DOM层中的享元模式应用
享元模式与DOM
关于DOM的事件冒泡机制,此处不再赘述。以下通过两个示例进行说明。
例1:事件集中管理
假设有很多相似类型的元素或结构(如菜单,或ul下的多个li)都需要监控click事件,若对每个元素单独绑定事件,性能将大打折扣。结合事件冒泡原理,子元素触发事件后,事件会冒泡至父级元素。利用这一特性,可对相似元素的父级元素进行事件监控,再判断具体是哪个子元素触发了事件,进而执行相应操作。
此处结合jQuery的bind/unbind方法进行示例演示。
HTML:
代码如下:
<div id="container">
<div class="toggle" href="#">更多信息 (地址)
<span class="info">
这里是更多信息
</span></div>
<div class="toggle" href="#">更多信息 (地图)
<span class="info">
<iframe src="http://www.map-generator.net/extmap.php?name=London&address=london%2C%20england&width=500...gt;"</iframe>
</span>
</div>
</div>JavaScript:
代码如下:
stateManager = {
fly: function(){
var self = this;
$('#container').unbind().bind("click", function(e){
var target = $(e.originalTarget || e.srcElement);
// 判断是哪一个子元素
if(target.is("div.toggle")){
self.handleClick(target);
}
});
},
handleClick: function(elem){
elem.find('span').toggle('slow');
}
});
例2:应用享元模式提升性能
另一个与jQuery相关的例子。通常在事件回调函数中使用元素对象时会写成$(this),这会重复创建新jQuery对象。由于回调函数中的this已经是DOM元素自身,无需再次封装,应避免使用如下代码:
代码如下:
$('div').bind('click', function(){
console.log('You clicked: ' + $(this).attr('id'));
});
// 上面的代码,要避免使用,避免再次对DOM元素进行生成jQuery对象,因为这里可以直接使用DOM元素自身了。
$('div').bind('click', function(){
console.log('You clicked: ' + this.id);
});如果确实需要$(this)的形式,也可实现自定义的单实例模式,例如实现一个jQuery.single(this)函数以返回DOM元素自身:
代码如下:
jQuery.single = (function(o){
var collection = jQuery([1]);
return function(element) {
// 将元素放到集合里
collection[0] = element;
// 返回集合
return collection;
};
});
使用方法:
代码如下:
$('div').bind('click', function(){
var html = jQuery.single(this).next().html();
console.log(html);
});这样即可原样返回DOM元素自身,且避免创建新的jQuery对象。
总结
总结
Flyweight模式是一种提高程序效率和性能的设计模式,能显著加快程序运行速度。其应用场景广泛:例如从数据库读取一系列包含大量重复字符串时,可将这些字符串存储在Flyweight池(pool)中。
若应用程序使用了大量对象且造成巨大存储开销,应考虑使用享元模式;若对象的大部分状态可置于外部状态,删除外部状态后能用较少共享对象取代大量组对象,此时也适合使用享元模式。