The Flyweight Pattern in Javascript

     Flyweight Pattern,中文可译作享元模式。它的核心是分离对象的:内在属性和外部属性,然后共享内在属性,组装外在属性。看一个汽车的例子: 

/* Car class, un-optimized. */
var Car = function (make, model, year, owner, tag, renewDate) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.owner = owner;
    this.tag = tag;
    this.renewDate = renewDate;
};
Car.prototype = {
    getMake: function () {
        return this.make;
    },
    getModel: function () {
        return this.model;
    },
    getYear: function () {
        return this.year;
    },
    transferOwnership: function (newOwner, newTag, newRenewDate) {
        this.owner = newOwner;
        this.tag = newTag;
        this.renewDate = newRenewDate;
    },
    renewRegistration: function (newRenewDate) {
        this.renewDate = newRenewDate;
    },
    isRegistrationCurrent: function () {
        var today = new Date();
        return today.getTime() < Date.parse(this.renewDate);
    }
};

很OOP,但是当需要很多Car的实例时,浏览器可能就慢了。 分析Car的属性,前3者终生不变,并且可以大量复制,作为内在属性。改进如下:

/* Car class, optimized as a flyweight. */
var Car = function (make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
};
Car.prototype = {
    getMake: function () {
        return this.make;
    },
    getModel: function () {
        return this.model;
    },
    getYear: function () {
        return this.year;
    }
};

/* CarFactory singleton. */
var CarFactory = (function () {
    var createdCars = {};
    return {
        createCar: function (make, model, year) {
            if (createdCars[make + '-' + model + '-' + year]) {
                return createdCars[make + '-' + model + '-' + year];
            }else {
                var car = new Car(make, model, year);
                createdCars[make + '-' + model + '-' + year] = car;
                return car;
            }
        }
    };
})();

/* CarRecordManager singleton. */
var CarRecordManager = (function () {
    var carRecordDatabase = {};
    return {
        addCarRecord: function (make, model, year, owner, tag, renewDate) {
            var car = CarFactory.createCar(make, model, year);
            carRecordDatabase[tag] = {
                owner: owner,
                renewDate: renewDate,
                car: car
            };
            return carRecordDatabase[tag];
        },
        getCar: function (tag) {
            return carRecordDatabase[tag];
        },
        transferOwnership: function (tag, newOwner, newTag, newRenewDate) {
            var record = this.getCar(tag);
            record.owner = newOwner;
            record.tag = newTag;
            record.renewDate = newRenewDate;
        },
        renewRegistration: function (tag, newRenewDate) {
            this.getCar(tag).renewDate = newRenewDate;
        },
        isRegistrationCurrent: function (tag) {
            var today = new Date();
            return today.getTime() < Date.parse(this.getCar(tag).renewDate);
        }
    };
})();


// test
(function () {
    var car = CarRecordManager.addCarRecord("test make", "test model", "2011", "Ray", "JB001", "2012-09-29");
    var car2 = CarRecordManager.addCarRecord("test make", "test model", "2011", "Tina", "JB002", "2011-08-27");
    var car1 = CarRecordManager.getCar("JB001");
    console.log(car == car1);               // true
    console.log(car1.car == car2.car);      // true
    console.log(car2.owner);                // tina
})();   

可以看到,即便需要很多的car,有CarFactory的控制,每个make + model + year的组合的Car实例实质上分别只会存在一个。 而对于Car实例的全部操作均通过CarRecordManager来实现,它通吃汽车的内外在全部属性。 


     基于js的灵活性,将Car类的声明迁移到CarFactory类中,将CarFactory类声明迁移到CarRecordManager中: 

/* CarRecordManager singleton. */
var CarRecordManager = (function () {
    var carRecordDatabase = {};

    /* CarFactory singleton. */
    var CarFactory = (function () {
        var createdCars = {};

        /* Car class, optimized as a flyweight. */
        var Car = function (make, model, year) {
            this.make = make;
            this.model = model;
            this.year = year;
        };
        Car.prototype = {
            getMake: function () {
                return this.make;
            },
            getModel: function () {
                return this.model;
            },
            getYear: function () {
                return this.year;
            }
        };

        return {
            createCar: function (make, model, year) {
                if (createdCars[make + '-' + model + '-' + year]) {
                    return createdCars[make + '-' + model + '-' + year];
                } else {
                    var car = new Car(make, model, year);
                    createdCars[make + '-' + model + '-' + year] = car;
                    return car;
                }
            }
        };
    })();

    return {
        addCarRecord: function (make, model, year, owner, tag, renewDate) {
            var car = CarFactory.createCar(make, model, year);
            carRecordDatabase[tag] = {
                owner: owner,
                renewDate: renewDate,
                car: car
            };
            return carRecordDatabase[tag];
        },
        getCar: function (tag) {
            return carRecordDatabase[tag];
        },
        transferOwnership: function (tag, newOwner, newTag, newRenewDate) {
            var record = this.getCar(tag);
            record.owner = newOwner;
            record.tag = newTag;
            record.renewDate = newRenewDate;
        },
        renewRegistration: function (tag, newRenewDate) {
            this.getCar(tag).renewDate = newRenewDate;
        },
        isRegistrationCurrent: function (tag) {
            var today = new Date();
            return today.getTime() < Date.parse(this.getCar(tag).renewDate);
        }
    };
})();


// test
(function () {
    var car = CarRecordManager.addCarRecord("test make", "test model", "2011", "Ray", "JB001", "2012-09-29");
    var car2 = CarRecordManager.addCarRecord("test make", "test model", "2011", "Tina", "JB002", "2011-08-27");
    var car1 = CarRecordManager.getCar("JB001");
    console.log(car == car1);               // true
    console.log(car1.car == car2.car);      // true
    console.log(car2.owner);                // tina

    //var cc = new Car("test make", "test model", "2012");  //error
})();   

这时候,还是Flyweight,但是封装性更好了。 Car的用户看不到CarFactory,更看不到Car。即便你如倒数第二行那样声明var car = new Car(...),得到的也唯有error。同时,它完全不影响原有的正常使用。

    关于Flyweight,它的内在属性比较好管理,因为实例对象比较少。而外在属性怎么管理? Car的Flyweight实现,是将它们存储在一个内部的类数组对象:carRecordDataBase中。 发散一下,想到没有,一棵树,它利用Composite Pattern也可以完美的存储很多对象实例。 我们用它来实现一下Flyweight。看如下Calendar的例子: 

/* CalendarItem interface. */
var CalendarItem = new Interface('CalendarItem', ['display']);

/* CalendarYear class, a composite. */
var CalendarYear = function (year, parent) {
    this.year = year;
    this.element = document.createElement('div');
    this.element.style.display = 'none';
    parent.appendChild(this.element);
    function isLeapYear(y) {
        return (y > 0) && !(y % 4) && ((y % 100) || !(y % 400));
    }
    this.months = [];
    this.numDays = [31, isLeapYear(this.year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    for (var i = 0, len = 12; i < len; i++) {
        this.months[i] = new CalendarMonth(i, this.numDays[i], this.element);
    }
};
CalendarYear.prototype = {
    display: function () {
        for (var i = 0, len = this.months.length; i < len; i++) {
            this.months[i].display(); 
        }
        this.element.style.display = 'block';
    }
};

/* CalendarMonth class, a composite. */
var CalendarMonth = function (monthNum, numDays, parent) {
    this.monthNum = monthNum;
    this.element = document.createElement('div');
    this.element.style.display = 'none';
    parent.appendChild(this.element);
    this.days = [];
    for (var i = 0, len = numDays; i < len; i++) {
        this.days[i] = new CalendarDay(i, this.element);
    }
};
CalendarMonth.prototype = {
    display: function () {
        for (var i = 0, len = this.days.length; i < len; i++) {
            this.days[i].display();
        }
        this.element.style.display = 'block';
    }
};

/* CalendarDay class, a leaf. */
var CalendarDay = function (date, parent) {
    this.date = date;
    this.element = document.createElement('div');
    this.element.style.display = 'none';
    parent.appendChild(this.element);
};
CalendarDay.prototype = {
    display: function () {
        this.element.style.display = 'block';
        this.element.innerHTML = this.date;
    }
};

CalendarYear、CalendarMonth、CalendarDay均实现了CalendarItem接口,CalendarMonth中引用和组装CalendarDay,CalendarYear引用和组装CalendarMonth。初看起来很不多,但是想一想一年得有365天,我连续加载个10年,页面估计就得挂了。 

    利用Singleton Pattern将CalendarDay一步到位改进如下: 

/* CalendarDay class, a leaf. */
var CalendarDay = (function () {
    var innerDay = null;
    var InnerCalendarDay = function () { };
    InnerCalendarDay.prototype = {
        display: function (date, parent) {
            var element = document.createElement('div');
            element.innerHTML = date;
            element.style.display = "block";
            parent.appendChild(element);
        }
    };

    return {
        getInstance: function () {
            if (innerDay == null) {
                innerDay = new InnerCalendarDay();
            }

            return innerDay;
        }
    };
})();

可以看到,我们将它的date和parent作为外在属性,在调用display方法时通过参数传入。这时候CalendarDay内部实例仅有1份。基于这个变化,修改CalendarMonth如下: 

/* CalendarMonth class, a composite. */
var CalendarMonth = function (monthNum, numDays, parent) {
    this.monthNum = monthNum;
    this.element = document.createElement('div');
    this.element.style.display = 'none';
    parent.appendChild(this.element);
    this.days = [];
    for (var i = 0, len = numDays; i < len; i++) {
        this.days[i] = CalendarDay.getInstance();
    }
};
CalendarMonth.prototype = {
    display: function () {
        for (var i = 0, len = this.days.length; i < len; i++) {
            this.days[i].display(i, this.element);
        }
        this.element.style.display = 'block';
    }
};

至此,CalendarDay级别已经优化。 你可以参考这个思路继续。 另外,创建的那些DOM元素其实也可以根据需要来进行共享。 这里暂不进行。

    这里再列举Tooltip的例子。一般性的代码:


var Tooltip = function(targetElement, text) {
    this.target = targetElement;
    this.text = text;
    this.delayTimeout = null;
    this.delay = 1500;

    this.element = document.createElement('div');
    this.element.style.display = 'none';
    this.element.style.position = 'absolute';
    this.element.className = 'tooltip';
    document.getElementsByTagName('body')[0].appendChild(this.element);
    this.element.innerHTML = this.text;

    var that = this
    addEvent(this.target, 'mouseover', function(e) { that.startDelay(e); });
    addEvent(this.target, 'mouseout', function(e) { that.hide(); });
};
Tooltip.prototype = {
    startDelay: function (e) {
        if (this.delayTimeout == null) {
            var that = this;
            var x = e.clientX;
            var y = e.clientY;
            this.delayTimeout = setTimeout(function () {
                that.show(x, y);
            }, this.delay);
        }
    },
    show: function (x, y) {
        clearTimeout(this.delayTimeout);
        this.delayTimeout = null;
        this.element.style.left = x + 'px';
        this.element.style.top = (y + 20) + 'px';
        this.element.style.display = 'block';
    },
    hide: function () {
        clearTimeout(this.delayTimeout);
        this.delayTimeout = null;
        this.element.style.display = 'none';
    }
};

//test
var linkElement = $('link-id');
var tt = new Tooltip(linkElement, 'Lorem ipsum...');

 同样可能会导致N对类似对象被大量创建。改进如下: 

/* TooltipManager singleton, a flyweight factory and manager. */
var TooltipManager = (function () {
    var storedInstance = null;

    /* Tooltip class, as a flyweight. */
    var Tooltip = function () {
        this.delayTimeout = null;
        this.delay = 1500;
        this.element = document.createElement('div');
        this.element.style.display = 'none';
        this.element.style.position = 'absolute';
        this.element.className = 'tooltip';
        document.getElementsByTagName('body')[0].appendChild(this.element);
    };
    Tooltip.prototype = {
        startDelay: function (e, text) {
            if (this.delayTimeout == null) {
                var that = this;
                var x = e.clientX;
                var y = e.clientY;
                this.delayTimeout = setTimeout(function () {
                    that.show(x, y, text);
                }, this.delay);
            }
        },
        show: function (x, y, text) {
            clearTimeout(this.delayTimeout);
            this.delayTimeout = null;
            this.element.innerHTML = text;
            this.element.style.left = x + 'px';
            this.element.style.top = (y + 20) + 'px';
            this.element.style.display = 'block';
        },
        hide: function () {
            clearTimeout(this.delayTimeout);
            this.delayTimeout = null;
            this.element.style.display = 'none';
        }
    };

    return {
        addTooltip: function (targetElement, text) {
            // Get the tooltip object.
            var tt = this.getTooltip();
            // Attach the events.
            addEvent(targetElement, 'mouseover', function (e) { tt.startDelay(e, text); });
            addEvent(targetElement, 'mouseout', function (e) { tt.hide(); });
        },
        getTooltip: function () {
            if (storedInstance == null) {
                storedInstance = new Tooltip();
            }
            return storedInstance;
        }
    };
})();

/* Tooltip usage. */
TooltipManager.addTooltip($('link-id'), 'Lorem ipsum...');

全部源码download

      

原文地址:https://www.cnblogs.com/Langzi127/p/2708005.html