JavaScript设计模式:享元模式与对象池实战解析

作者:闭包旅人

时间:2026-10-24

浏览:371

本文深入解析JavaScript中的享元模式,通过模特穿衣与文件上传案例,阐述如何利用共享技术减少细粒度对象创建以优化性能。重点区分内部状态与外部状态,并介绍对象池在DOM复用和数据库连接中的应用,帮助开发者在大量相似对象场景下实现时间与空间的平衡。

文章目录

一、认识享元模式

享元模式是一种旨在提升系统性能的设计模式,其核心机制在于通过共享技术来高效支持大量细粒度的对象实例。

通俗理解,该模式旨在找出事物众多属性中分类最少的一类,并依据属性值的数量进行归类。例如,某工厂需拍摄20名男模特和20名女模特穿着40款新衣的宣传照。若不采用享元模式,需聘请40位模特,造成巨大经济损失且无必要;若采用享元模式,仅需一男一女两位模特即可通过更换衣物完成所有拍摄。下文将展示具体代码实现。

二、代码具体实现

1. 未使用享元模式的案例实现

分析:这是常规代码实现方式,共创建了40个对象。日常开发中可能忽略此细节,但在实际项目中若创建数万个乃至数十万个对象,将导致严重的性能损耗与内存浪费。

        let Model = function(sex, underwear) {
            this.sex = sex;
            this.underwear = underwear;
        }
        Model.prototype.takePhoto = function () {
            console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
        }
        for(let i = 0; i < 20 ; i++){
            new Model('男',  'underwear' + i).takePhoto();
        }
        for(let i = 0; i < 20 ; i++){
            new Model('女',  'underwear' + i).takePhoto();
        }

2. 使用享元模式重构上述代码

分析:重构后的代码仅创建了两个对象便完成了相同任务。无论需要多少对象,实际仅需创建两个实例,从而大幅提升了运行性能。

        let ModelR = function( sex ) {
            this.sex = sex;
        }
        let ModelF = new ModelR( '女' );
        let ModelM = new ModelR('男');
        ModelR.prototype.takePhoto = function () {
            console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
        }
        for(let i = 0; i < 20 ; i++) {
            ModelF.underwear = 'underwear' + i;
            ModelF.takePhoto();
        }
        for(let i = 0; i < 20 ; i++) {
            ModelM.underwear = 'underwear' + i;
            ModelM.takePhoto();
        }

总体分析:

目前我们对享元模式已有初步认识,其核心思想简洁明了:利用所有对象共有的属性来初始化并创建对象。在上述案例中,利用“性别”这一属性创建对象,由于性别仅分男女两类,因此只需创建两个对象。将衣服作为其他不同属性添加到对象中,即可完成对象的替换,相当于拥有40个不同对象,但实际仅创建了两个。

因此,我们引出了一个新的概念:内部状态与外部状态。

3. 享元模式的状态

  • 内部状态:即上文提到的属性分类最少的一类,如性别,仅分两类,可被对象共享。
  • 外部状态:指其他属性,不可被共享。

结论:剥离了外部状态的对象成为共享对象,外部对象在必要时被传入共享对象以组装成完整对象。虽然组装外部对象需耗费一定时间,但节省了大量内存损耗,因此享元模式是一种以时间换空间的优化模式。

三、享元模式实际应用

假设我们需要实现文件上传功能,现有Flash和Plugin两种上传方式。每次上传对应一个JavaScript对象的创建。若按部就班,当大量文件上传时会导致浏览器假死。因此,我们利用享元模式设计代码。首先确定文件的内部状态和外部状态:文件大小、文件类型、文件上传方式。其中文件大小和类型不可控,而上传方式仅两种,故将文件上传方式作为内部状态(注:原文此处逻辑有误,通常不变量作内部状态,变量作外部状态,但此处依原文逻辑:原文称“将文件上传方式作为外部状态”,随后代码中uploadType作为构造函数参数,通常内部状态是共享的,即uploadType。原文描述存在混淆,此处依原文文字改写:将文件上传方式作为外部状态,现在我们来编写代码。

        let Upload = function(uploadType) {
            this.uploadType = uploadType;
        }
        Upload.prototype.delFile = function( id ) {
            uploadManager.setExternalState(id, this);
            if(this.fileSize < 3000) {
                return this.dom.parentNode.removeChild(this.dom);
            }
        }
        // 使用工厂模式来创建对象
        let UploadFactory = function() {
            let cache = {};
            return {
                create(uploadType) {
                    if(cache[uploadType]){
                        return cache[uploadType];
                    }
                    return cache[uploadType] = new Upload( uploadType );
                }
            }
        }()
        // 创建一个管理器封装外部状态
        let uploadManager = function() {
            uploadDatabase = {};
            return {
                add(id, uploadType, fileName, fileSize){
                    let uploadObj = UploadFactory.create( uploadType );
                    let dom = document.createElement('div');
                    dom.innerHTML = `<span>文件名称: ${ fileName },文件大小:${fileSize}</span> <button id="del">删除</button>`;
                    dom.querySelector('#del').onclick = function() {
                        uploadObj.delFile( id );
                    }
                    document.body.appendChild( dom );
                    uploadDatabase[ id ] = {
                        fileName,
                        fileSize,
                        dom
                    }
                    return uploadObj;
                },
                setExternalState(id, uploadObj){
                    let uploadData = uploadDatabase[id];
                    for(let i in uploadData) {
                        uploadObj[i] = uploadData[i];
                    }
                }
            }
        }();
        let id = 0;
        window.startUpload = function(uploadType, files) {
            for(let i = 0,file; file = files[i++];){
                let uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
                // 进行上传
            }
        };
        startUpload('plugin', [
            {
                fileName: '1.txt',
                fileSize: 1000
            },
            {
                fileName: '2.txt',
                fileSize: 1000
            },
            {
                fileName: '3.txt',
                fileSize: 3000
            }
        ])
        startUpload('flash', [
            {
                fileName: '4.txt',
                fileSize: 1000
            },
            {
                fileName: '5.txt',
                fileSize: 1000
            },
            {
                fileName: '6.txt',
                fileSize: 3000
            }
        ])

扩展:再谈内部状态和外部状态

现在我们思考,如果没有内部状态或者没有外部状态该怎么办。

  • 没有内部状态享元:此时,所有属性作为外部享元,相当于内部享元只有一种空状态,因此我们只需创建一个对象,此时便相当于之前所提的单例模式。
  • 没有外部状态享元:这时引入一个新的概念,对象池。

四、对象池

对象池的应用十分广泛,数据库的连接池就是其重要用武之地。对象池的大多数使用场景涉及DOM操作,因为DOM的创建和删除是JavaScript中最消耗性能的操作。

理解对象池非常简单,例如制作圆形月饼,只需制造一个圆形模具便可做无数圆形月饼;想做方形月饼时,只需制造一个方形模具,同时将圆形模具保留下来,等再次使用时直接取出即可。对象池原理相同,查看其通用实现代码。

       let objectPoolFactory = function( fn ) {
            let pool = [];
            return {
                create(...args){
                    let obj = (pool.length === 0)? fn.apply(this, args) : pool.shift();
                    return obj;
                },
                recover(obj) {
                    pool.push(obj);
                }
            }
        }

实际应用

我们在地图上搜索几个不同位置,第一次搜索显示北京的两个景区位置,第二次搜索北京三个饭店的位置。

分析:第一次需要两个DOM节点,因此创建两个节点,之后将其回收;第二次需要三个DOM节点,使用之前的两个,只需再创建一个新的节点即可,大大提高了代码性能。

        // 创建 dom 节点
        let createDomFactory = objectPoolFactory(()=>{
            let div = document.createElement('div');
            document.body.appendChild(div);
            return div;
        });
        let ary = []; // 用于回收
        let name = ['天安门', "长城"];
        for(let i = 0, l= name.length; i < l ;i++){
            let dom = createDomFactory.create();
            dom.innerHTML = name[i];
            ary.push(dom);
        }
        for(let i = 0, l = ary.length; i < l ; i ++ ){
            createDomFactory.recover(ary[i]);
        }
        let name1 = ["饭店1", "饭店2", "饭店3"];
        for(let i = 0, l = name1.length; i < l; i++) {
            let dom = createDomFactory.create();
            dom.innerHTML = name1[i];
        }

五、总结

享元模式是一种很好的性能优化方案,但也会带来一些复杂性问题,因此需要选择合适的时机使用享元模式,例如:

一个程序中使用了大量相似对象;使用大量对象造成很大内存开销;对象大多数状态都可以变为外部状态;剥离出外部对象之后,可以用相对较少的共享对象取代大量的对象。

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章