美文网首页javascript
JavaScript设计模式

JavaScript设计模式

作者: DHFE | 来源:发表于2018-11-07 02:16 被阅读13次

    简介

    JavaScript是一门灵活的语言,早些年被认为是玩具式的语言,只能做一点为网页涂脂抹粉的小差事。项目工程也不大,更无从说起设计模式在JavaScript的应用,但随着Node.js以及H5和web2.0的兴起,JavaScript本身变得越来越受重视。
    但是很多本该有的东西JavaScript没有,或者并没有作为正式的部分。这些年人们利用自己对计算机编程的思想,利用了很多晦涩的技巧实现了很多JavaScript设计者都未曾预料到的任务,比如设计模式,面对对象编程等。

    设计模式(Design Pattern)是一套被反复使用、思想成熟、经过分类和无数实战设计经验总结的代码构建模式。使用设计模式是为了让系统可重用、可扩展、可解耦,更容易被人理解且能保证代码可靠性。设计模式使代码开发真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。只有夯实地基搭好结构,才能盖好健壮的大楼,也是迈向高级开发人员必经的一步。

    设计原则
    设计模式存在的根本原因是为了代码复用,增加可维护性。有如下原则:

    • 【开闭原则】对扩展开放,对修改关闭。
    • 【里氏转换原则】子类继承父类,单独完全可以运行。
    • 【依赖倒转原则】引用一个对象,如果这个对象有底层类型,直接使用底层。
    • 【接口隔离原则】每一个接口应该是一种角色,相对独立。
    • 【合成 / 聚合复用原则】新的对象应使用一些已有的对象,使之成为新对象的一部分。
    • 【迪米特原则】一个对象应对其他对象有尽可能少的了解。

    1. 单例模式

    1.1 概念

    单例,保证一个类只有一个实例,实现的方法一般是先判断实例是否存在,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
    在JavaScript中,单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象。

    1.1.1 模式作用
    • 模块间通信。
    • 系统中某个类的对象只能存在一个。
    • 保护自己的属性和方法
    1.1.2 注意事项
    • 注意this使用
    • 闭包容易造成内存泄漏,不需要的尽快给予垃圾回收处理。
    • 注意new的成本。(继承)

    1.2 应用

    1.2.1 简单单例模式(透明)
    // 定义类
    function Singleton(name) {
        this.name = name;
        this.instance = null;
    }
    // 扩展类的一个方法
    Singleton.prototype.getName = function () {
        console.log(this.name);
    }
    
    // 获取类的实例
    Singleton.getIntance = function (name) {
        if (!this.instance) {
            this.instance = new Singleton(name);
        }
        return this.instance;
    }
    
    
    var a = Singleton.getIntance("Apple");
    var b = Singleton.getIntance("Orange");
    
    a.getName(); // Apple
    b.getName(); // Apple
    

    也可以用闭包实现

    // 定义类
    function Singleton(name) {
        this.name = name;
        this.instance = null;
    }
    // 扩展类的一个方法
    Singleton.prototype.getName = function () {
        console.log(this.name);
    }
    
    // 获取类的实例
    Singleton.getIntance = (function() {
        this.intance = null;
        return function(name) {
            if (!this.instance) {
                this.instance = new Singleton(name);
            }
            return this.instance;
        }
    })();
    
    var a = Singleton.getIntance("Apple");
    var b = Singleton.getIntance("Orange");
    
    
    a.getName(); // Apple
    b.getName(); // Apple
    

    运行后,Singleton.getIntance中的闭包引用着instance变量,使其没有被销毁,会一直保存着状态。第一次使用后(Singleton.getIntance("Apple")),instance就永久指向了这个实例对象。

    另一种

    // 创建私有类
    function Privatemath() {}
    Privatemath.prototype.sum = function(x,y) {console.log(x+y)}
    Privatemath.prototype.multiply = function(x,y) {console.log(x*y)}
    
    let someTool = (function() {
        var _instance = null;
    
        function init() {       // 构造函数,用于实例化对象
            // 私有变量
            let tool = new Privatemath();
    
            // 公用属性和方法
            this.name = "Privatemath";
            this.getName = function() {
                return this.name;
            }
            this.getSumFn = function(x,y) {
                return tool.sum(x,y);
            }
            this.getMultiplyFn = function(x,y) {
                return tool.multiply(x,y);
            }
        }
    
        return function() {         // 立即执行函数返回的是匿名函数用于判断实例是否创建
            if (!_instance) {
                _instance = new init();
            }
            return _instance;
        }
    })();
    
    // 只有当调用timeTool()时进行实例的实例化
    // 不在js加载时就进行实例化创建, 而是在需要的时候再进行单例的创建。 
    // 如果再次调用, 那么返回的永远是第一次实例化后的实例对象。
    let instance1 = someTool();
    let instance2 = someTool();
    
    console.log(instance1 === instance2);        // true
    instance1.getSumFn(1,2);                     // 3
    instance2.getMultiplyFn(10,10)               // 100
    
    1.2.2 单例模式的应用之一:遮罩层

    刚入门的前端(比如我:)一般都会直接写好div,使其display:none,这样做的的缺点是如果没有使用到这个div,那么就会平白无故浪费掉一个DOM节点,如果页面更多,对多个div处理的开销也会增加;另一种就是使用JS创建DOM节点,需要时将其引入到document中,不需要就删除。这样做对页面的性能也是一种消耗(重排)。

    //  <button id="btn">click it</button>
    
    function Singleton() {
        var div = document.createElement("div");
        div.id = "dialog";
        div.style.position = 'fixed';
        div.style.background = "#ccc";
        div.style.top = '0';
        div.style.right = '0';
        div.style.bottom = '0';
        div.style.left = '0';
        div.style.opacity = '';
        div.style.display = "block";
        document.body.appendChild(div);
        div.onclick = function () {
            this.style.display = "none";
        }
    
        this.div = div;
    };
    
    Singleton.getDom = (function() {
        return function() {
            if (!this.instance) {
                this.instance = new Singleton();
            }
    
            this.instance.div.style.display = "block";
            return this.instance;
        }
    })();
    
    
    document.getElementById("btn").onclick = function() {
        var maskLayer = Singleton.getDom();
    }
    

    或者直接用闭包。

    // <button id="btn">click it</button>
    var createDom = (function () {
        var div;
    
        return function () {
            if (!div) {
                div = document.createElement('div');
                div.id = "dialog";
                div.style.position = 'fixed';
                div.style.background = "#ccc";
                div.style.top = '0';
                div.style.right = '0';
                div.style.bottom = '0';
                div.style.left = '0';
                div.style.opacity = '';
                div.style.display = 'block';
                document.body.appendChild(div);
                div.onclick = function() {
                    div.style.display = "none";
                }
            }
    
            div.style.display = "block";    // 取消遮罩后DOM节点display:none,下次使用前需要更改为block
            return div;
        }
    
    })();
    
    document.getElementById('btn').onclick = function () {
        var test = createDom();
    }
    
    1.2.3 可以模块通信的单例

    抽象了一下

    // 独立的对象
    // 允许对象间通讯
    // 通讯通道
    
    var apple = (function(arg) {
    
        var createMessageAisle = function(message) {
            this.msg = message  // fromStrawberryMsg信息存储在通道中返回给
        }
    
        var messageAisle;
        var infomation = {
            sendMessage: function(fromStrawberryMsg) {
                
                if (!messageAisle) {    // 是否已创建信息通道,不存在则使用createMessageAisle创建
                    messageAisle =  new createMessageAisle(fromStrawberryMsg);
                }
                return messageAisle;        // 存在消息通道,直接返回
    
            }
        };
        return infomation;
    
    })();
    
    var strawberry = {
        callApple: function(message) {
            var _appleMsg = apple.sendMessage(message);
            console.log(_appleMsg.msg);
    
            _appleMsg = null;       // 闭包,使用结束后需要释放内存,等待垃圾回收
        }
    }
    
    strawberry.callApple("Hi! I'm strawberry~~~");      // Hi! I'm strawberry~~~
    
    
    从ES6重新认识JavaScript设计模式(一): 单例模式
    单例模式
    深入理解JavaScript系列(25):设计模式之单例模式
    [Javascript]单例模式(singleton )

    2. 构造函数模式

    2.1 概念

    构造函数用于创建特定类型的对象。不仅声明了使用的对象,构造函数还可以接受参数以便第一次创建对象的时候设置对象的成员值。你可以自定义自己的的构造函数,然后在里面声明自定义类型对象的属性或方法。

    2.1.1 模式作用
    • 用于创建特定类型的对象。
    • 第一次声明的时候给对象赋值。
    • 自己声明构造函数,赋予属性方法。
    2.1.2 注意事项
    • 声明函数的时候处理业务逻辑
    • 区分和单例的区别,配合单例实现初始化
    • 构造函数大写字母开头
    • 注意new的成本(继承)

    2.2 应用

    2.2.1

    基本用法:
    在JavaScript中,没有类的概念,但是有构造函数可以使用。

    function Car(name,year,model) {
        this.name = name;
        this.year = year;
        this.model = model;
        this.getName = function() {
            return this.name;
        }
    };
    
    var a = new Car("BMW",2000,"M3");
    var b = new Car("Benz",1995,"s300");
    
    console.log(a.getName());
    console.log(b.getName());
    

    但是对于公用方法,这些使用很浪费,可以这样。

    function getName() {
        return this.name;
    }
    

    但是我们有prototype,于是可以:

    Car.prototype.getName = function() {
        return this.name;
    }
    

    也可以使用new

    function Car(name,year,model) {
        this.name = name;
        this.year = year;
        this.model = model;
    };
    Car.prototype.getName = function() {
        return this.name;
    }
    
    var a = new Car("BMW",2000,"M3");
    var b = new Car("Benz",1995,"s300");
    
    
    console.log(a.getName());
    console.log(b.getName());
    

    或者不使用new

    function Car(name,year,model) {
        this.name = name;
        this.year = year;
        this.model = model;
        this.__proto__ = Car.prototype;
    };
    Car.prototype.getName = function() {
        console.log(this.name);
    }
    
    var a = new Object(),
        b = new Object();
    
    
    Car.call(a,"BMW",2000,"M3");
    Car.call(b,"Benz",1995,"s300");
    
    a.getName();        // BMW
    b.getName();        // Benz
    

    单例 + 强制使用new

    function Car(newtype) {
    
        if (!(this instanceof Car)) {
            return new Car(newtype);
        }
        
        this.name = "BMW";
        this.color = "Black";
        this.type = "microcar";
    
        if (newtype) {
            this.type = newtype;
        }
    
        this.createCar = function () {
            console.log("【车名】" + this.name + " | " + "【颜色】" + this.color + " | " + "【类型】" + this.type)
        }
    }
    
    
    var car1 = new Car();
    var car2 = Car("SUV");
    
    car1.createCar();   // 【车名】BMW | 【颜色】Black | 【类型】microcar
    car2.createCar();   // 【车名】BMW | 【颜色】Black | 【类型】SUV
    
    car1 = Car("MPV");
    car1.createCar();   // 【车名】BMW | 【颜色】Black | 【类型】MPV
    
    

    3. 工厂模式

    3.1 概念

    工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型(抽象工厂)。

    由一个方法来决定到底要创建哪个类的实例, 而这些实例经常都拥有相同的接口. 这种模式主要用在所实例化的类型在编译期并不能确定, 而是在执行期决定的情况。 说的通俗点,就像公司茶水间的饮料机,要咖啡还是牛奶取决于你按哪个按钮。——TAT.svenzeng

    3.1.1 模式作用
    • 对象的构建十分复杂
    • 需要依赖具体的环境(需求)创建不同的实例
    • 处理大量具有相同属性的小对象
    3.1.2 注意事项
    • 不能滥用工厂模式,有时候只会徒增代码复杂度(复杂时写好注释)

    3.2 应用

    3.2.1
    // 在网页面里插入一些元素,而这些元素类型不固定,可能是图片,也有可能是连接,甚至可能是文本
    // 根据工厂模式的定义,我们需要定义工厂类和相应的子类,我们先来定义子类的具体实现
    var page = page || {};
    page.dom = page.dom || {};
    // 文本处理函数
    page.dom.Text = function () {
        this.insert = function (where) {
            var txt = document.createTextNode(this.url);
            where.appendChild(txt);
        };
    };
    // 链接处理函数
    page.dom.Link = function () {
        this.insert = function (where) {
            var link = document.createElement('a');
            link.href = this.url;
            link.appendChild(document.createTextNode(this.url));
            where.appendChild(link);
        };
    };
    // 图片处理函数
    page.dom.Image = function () {
        this.insert = function (where) {
            var im = document.createElement('img');
            im.src = this.url;
            where.appendChild(im);
        };
    };
    
    // 实例化工厂
    page.dom.factory = function(type) {
        return new page.dom[type]();
    }
    // 选择需要的"车间"
    var o = page.dom.factory("Link");
    o.url = "http://www.cnblogs.com";
    o.insert(document.body);
    

    function Fruits(property) {
        this.name = property.name || "apple";
        this.color = property.color || "red";
        this.origin = property.origin || "烟台";
    }
    function Cereal(property) {
        this.name = property.name || "spinach leaf";
        this.color = property.color || "green";    
        this.origin = property.origin || "大理";
    }
    function Vegetables(property) {
        this.name = property.name || "rice";
        this.color = property.color || "white";
        this.origin = property.origin || "五常";
    }
    
    function PlantFactory() {};
    PlantFactory.prototype.factoryClass = Fruits;   // 默认选择生产水果
    PlantFactory.prototype.selectFactory = function(property) {
        switch (property.factoryClass) {
            case "Fruits":
                this.Class = Fruits;
                break;
            case "Cereal":
                this.Class = Cereal;
                break;
            case "Vegetables":
                this.Class = Vegetables;
                break;
        }
        this.Class.prototype.getProperty = function() {
            console.log("Name: " + this.name + " | Color: " + this.color + " |  Origin: " + this.origin);
        }
        return new this.Class(property);
    }
    // 实例化工厂
    var Factory = new PlantFactory();
    
    var fruits1 = Factory.selectFactory({
        factoryClass: "Fruits",    // 选择创建哪个类的实例
        name: "Orange",
        origin: "桂林"
    })
    var cereal1 = Factory.selectFactory({
        factoryClass: "Cereal",
        name: "Cabbage",
        color: "white and green",
        origin: "河北"
    })
    
    cereal1.getProperty();      // Name: Cabbage | Color: white and green |  Origin: 河北
    console.log(cereal1 instanceof Cereal);     // true
    fruits1.getProperty();      // Name: Orange | Color: red |  Origin: 桂林
    console.log(fruits1 instanceof Fruits);     // true
    

    4. 模块模式

    概念

    模块是任何健壮的应用程序体系结构的组成部分,通常有助于保持项目的代码单元完全分离和组织。
    在JavaScript中,有几个实现模块的选项。这些包括:

    • 使用模块模式
    • 对象字面量
    • AMD modules
    • CommonJS modules
    • ECMAScript协调模块

    模块模式部分基于对象字面量,因此首先刷新对它们的认识是有意义的。

    对面字面量

    在对象字面量表示法中,对象被描述为一对由逗号分隔的键值对(key-value),它们包含在大括号{}中,对象内部的名称可以是字符串或者标识符......在最后一个键值对之后,不应该使用逗号,因为这会导致错误。

    var myObjectLiteral = {
     
        variableKey: "variableValue",
     
        functionKey: function () {
          // ...
        },      // 不要这样
    };
    

    对象字面量不需要使用new运算符进行实例化,但是不应该在语句开头使用,因为{可以被解释为的开头。
    在对象之外,可以使用以下赋值方法将新成员添加到对象中:myModule.property = "someValue";

    来看一个使用对象字面量定义的模块的完整例子:

    var myModule = {
    
        myProperty: "someValue",
    
        // 对象字面量可以包含属性和方法 | 例如,我们可以为模块配置定义另一个对象:
        myConfig: {
            useCaching: true,
            language: "en"
        },
        // 一个基本方法
        saySomething: function () {
            console.log("Where in the world is Paul Irish today?");
        },
        // 根据当前配置输出值
        reportMyConfig: function () {
            console.log("Caching is: " + (this.myConfig.useCaching ? "enabled" : "disabled"));
        },
    
        // 重写当前配置
        updateMyConfig: function (newConfig) {
            if (typeof newConfig === "object") {
                this.myConfig = newConfig;
                console.log(this.myConfig.language);
            }
        }
    };
     
    myModule.saySomething();    // Outputs: Where in the world is Paul Irish today?
    myModule.reportMyConfig();  // Outputs: Caching is: enabled  
    
    myModule.updateMyConfig({   // Outputs: fr
        language: "fr",
        useCaching: false
    });    
    
    myModule.reportMyConfig();  // Outputs: Caching is: disabled
    

    使用对象文字可以帮助封装和组织代码。
    如果我们使用这种技巧,我们可能也会对模块模式感兴趣。它仍然使用对象字面量,但仅作为作用域函数的返回值。

    模块模式

    模块模式最初被定义为:在传统软件工程中为类提供私有和公共封装的方法。

    在JavaScript中,Module模式用于进一步模拟类的概念,使得我们能够在单个对象中包含公共/私有方法和变量,从而将特定部分与全局范围隔离开来,这样函数名与其他脚本上定义的函数发生冲突的可能性会降低。

    模块模式使用闭包来封装私有方法、状态。它提供了一种包装公有/私有方法、变量的方法,保护其不会泄漏到全局作用域中,使用这种模式,只需返回一个公共API,闭包会将所有其他内容隐蔽起来。

    模块模式使用一个立即调用的函数表达式,返回一个公开对象。

    需要注意的是,JavaScript中没有真正意义上的“私有”概念,因为不像一些传统语言,它没有访问修饰符。因为闭包的关系,在模块模式中,声明的变量或方法只在模块本身内部可用。当然,在返回对象中定义的变量或方法对每个人都是公开的。

    从历史的角度来看,模块模式最初是由包括理查德·科尔福德在内的许多人在2003年开发的。后来道格拉斯·克罗克福德在他的讲座中推广了这种模式。另一个背景是,如果你曾经使用过雅虎的YUI库,它的一些功能可能看起来很熟悉,其原因是在模块模式中创建组件功能对YUI有很大影响。

    让我们通过创建一个自包含的模块来开始模块模式

    example

    var testModule = (function () {
    
        var counter = 0;
    
        return {
    
            incrementCounter: function () {
                return counter++;
            },
    
            resetCounter: function () {
                console.log("counter value prior to reset: " + counter);
                counter = 0;
            }
        };
    
    })();
    
    // Usage:
    
    // Increment our counter
    testModule.incrementCounter();
    
    // Check the counter value and reset
    testModule.resetCounter();  // Outputs: counter value prior to reset: 1
    

    变量counter在全局作用域下完全不可见,就像真正的私有变量一样。它仅存在于模块的闭包中,所以唯二能访问其作用域的是暴漏出来的两个函数。这其实是namespace方法。

    使用Module模式时,我们可能会发现,定义一个用于|开始使用它|的简单模板很有用(入口)。这是一个涵盖命名空间,公共和私有变量的方法:

    var myNamespace = (function () {
    
        var myPrivateVar, myPrivateMethod;
    
        // 私有变量
        myPrivateVar = 0;
    
        // A private function which logs any arguments
        myPrivateMethod = function (foo) {
            console.log(foo);
        };
    
        return {
            // 返回公有变量
            myPublicVar: "foo",
    
            // A public function utilizing privates
            myPublicFunction: function (bar) {
    
                // Increment our private counter
                myPrivateVar++;
    
                // Call our private method using bar
                myPrivateMethod(bar);
    
            }
        };
    })();
    
    
    console.log(myNamespace.myPublicVar);       // foo
    myNamespace.myPublicFunction("hello");      // hello
    

    再看另一个例子,模块本身在一个名为basketModule的全局变量中,模块中的变量和方法保持私有,因此作用域外都无法访问模块内部,只与模块的闭包一起存在,只有能够访问它的方法(addItem,getItemCount等)。

    var basketModule = (function () {
    
        // privates
        var basket = [];
    
        function doSomethingPrivate() {}
    
        function doSomethingElsePrivate() {}
    
        // Return an object exposed to the public
        return {
    
            // Add items to our basket
            addItem: function (values) {
                basket.push(values);
            },
    
            // Get the count of items in the basket
            getItemCount: function () {
                return basket.length;
            },
    
            // Public alias to a private function
            doSomething: doSomethingPrivate,
    
            // Get the total value of items in the basket
            getTotal: function () {
    
                var q = this.getItemCount(),
                    p = 0;
    
                while (q--) {
                    p += basket[q].price;
                }
    
                return p;
            }
        };
    })();
    
    // basketModule returns an object with a public API we can use
     
    basketModule.addItem({
        item: "bread",
        price: 0.5
    });
    
    basketModule.addItem({
        item: "butter",
        price: 0.3
    });
    
    
    console.log(basketModule.getItemCount());   // Outputs: 2
    console.log(basketModule.getTotal());       // Outputs: 0.8
    
    // However, the following will not work:
    console.log(basketModule.basket);           // undefined
    console.log(basket);                        // ReferenceError: basket is not defined
    
    • 拥有私有方法私有成员,没有暴露于外部作用域,真正的私有化。
    • 假设函数是正常声明命名的,那么当我们试图查找函数的异常时,在调试器中显示调用堆栈会更容易。

    变种模块模式
    Import mixins
    变化之后的模块模式演示了如何将全局变量(如Jquery,Underscore)作为参数传入模块中,这使得我们可以导入它们,并根据特定情况在本地更改。

    // Global module
    var myModule = (function (jQ, _) {
    
        function privateMethod1() {
            jQ(".container").html("test");
        }
    
        function privateMethod2() {
            console.log(_.min([10, 5, 100, 2, 1000]));
        }
    
        return {
            publicMethod: function () {
                privateMethod1();
            }
        };
    
        // Pull in jQuery and Underscore
    })(jQuery, _);
    
    myModule.publicMethod();
    
    优缺点

    优点:

    • 我们已经看到了为什么构造函数模式是有用的,但是为什么模块模式是一个好的选择呢?对于初学者来说,对于来自面向对象背景的开发人员来说,这比真正封装的思想要干净得多,至少从JavaScript的角度来看。
    • 其次,它支持私有数据,因此,在模块模式中,我们代码的pulicMethod可以接触类的私有部分和私有变量,但是外部作用域无法访问privateMethod

    缺点:

    • 模块模式的缺点是,当我们以不同的方式访问公共成员和私有成员时,当我们希望更改可见性时,我们实际上必须对使用该成员的每个位置进行更改。
    • 在稍后添加到对象的方法中,我们也不能访问私有成员(作用域链已经确定)。当然,在许多情况下,模块模式仍然非常有用,并且当正确使用时,肯定具有改进应用程序结构的潜力。
    • 其他缺点包括不能为私有成员创建自动化的单元测试以及当bug需要热修复时增加额外的复杂性。这很困难。值得记住的是,私有资源并不像它们最初看起来那么灵活。

    关于模块模式更多信息,可见article


    5. 混合模式

    概念

    在传统的编程语言 (如 c++ 和 lisp) 中, 混合模式是提供可由子类或子类组轻松继承以实现函数重用的类。

    Sub-classing

    对于不熟悉子类的开发人员, 我们将在进一步深入混合模式和装饰器模式之前, 对他们进行简短的初学者入门介绍。

    子类是指从基类或者超类对象继承新对象的属性和术语。在传统的面向对象编程中,类B能够扩展至另一个类A。这里我们考虑A是超类,BA的子类,B的所有实例都继承了A。当然,B仍然能够定义自己的方法,包括覆盖原来由A定义的方法的那些方法。

    如果B需要调用已被重写的方法,我们将其称为方法链接。如果B需要调用构造函数A(超类),我们称此构造函数链接。

    为了演示子类,我们首先需要一个可以创建新实例的基础对象。让我们围绕一个人的概念来模拟这个问题:

    var Person = function (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.gender = "male";
    };
    

    接下来,我们要指定一个新的类,它是现有的”人“对象的子类。让我们想象一下,我们想添加一个独特的属性来区分一个人和一个超级英雄,同时继承“超人”的属性。由于超级英雄与普通人有许多共同特征(如姓名、性别),这应该能说明子类是如何充分工作的。

    var Person = function (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.gender = "male";
    };
    
    // a new instance of Person can then easily be created as follows:
    var clark = new Person( "Clark", "Kent" );
    
    // Define a subclass constructor for for "Superhero":
    var Superhero = function( firstName, lastName, powers ){
    
        // Invoke the superclass constructor on the new object
        // then use .call() to invoke the constructor as a method of
        // the object to be initialized.
    
        Person.call( this, firstName, lastName );
    
        // Finally, store their powers, a new array of traits not found in a normal "Person"
        this.powers = powers;
    };
    
    Superhero.prototype = Object.create( Person.prototype );
    var superman = new Superhero( "Clark", "Kent", ["flight","heat-vision"] );
    
    // Outputs Person attributes as well as powers
    console.log(superman);  // Superhero {firstName: "Clark", lastName: "Kent", gender: "male", powers: Array(2)}
    
    

    Superhero构造函数创建了一个从Person下降的对象(???The Superhero constructor creates an object which descends from Person怎么翻译?)

    The Superhero constructor creates an object which descends from Person. Objects of this type have attributes of the objects that are above it in the chain and if we had set default values in the Person object, Superhero is capable of overriding any inherited values with values specific to it's object.

    Mixins

    在JavaScript中,我们可以从混合类中继承继承,以通过扩展来收集功能。我们定义的每个新对象都有一个原型,从中可以继承更多的属性。原型可以从其他对象原型继承,但更重要的是,可以为任意数量的对象实例定义属性。我们可以利用这一事实来促进功能的再利用。

    MIXIN允许对象以最小的复杂度向他们借用(或继承)功能。由于该模式与JavaScripts对象原型配合得很好,因此它为我们提供了一种相当灵活的方式,不仅从一个Mixin共享功能,而且通过多个继承有效地共享功能。

    它们可以被看作具有属性和方法的对象,这些属性和方法可以很容易地跨多个其他对象原型共享。假设我们在一个标准对象文字中定义一个包含Min的实用程序函数如下:

    var myMixins = {
    
        moveUp: function () {
            console.log("move up");
        },
    
        moveDown: function () {
            console.log("move down");
        },
    
        stop: function () {
            console.log("stop! in the name of love!");
        }
    
    };
    

    然后我们可以使用诸如Underscore.js(_.extend())方法之类的帮助我们轻松地扩展现有构造函数原型以包括此行为。

    // A skeleton carAnimator constructor
    function CarAnimator() {
        this.moveLeft = function () {
            console.log("move left");
        };
    }
    
    // A skeleton personAnimator constructor
    function PersonAnimator() {
        this.moveRandomly = function () { /*..*/ };
    }
    
    // Extend both constructors with our Mixin
    _.extend(CarAnimator.prototype, myMixins);
    _.extend(PersonAnimator.prototype, myMixins);
    
    // Create a new instance of carAnimator
    var myAnimator = new CarAnimator();
    
    myAnimator.moveLeft();  // move left
    myAnimator.moveDown();  // move down
    myAnimator.stop();      // stop! in the name of love!
    

    正如我们所看到的,这使得我们可以很容易地将常见行为混入对象构造中。

    在下一个示例中,我们有两个构造函数:CarMixin。我们要做的是扩充(另一种说法是扩展)Car,以便它可以继承Mixin中定义的特定方法,即driveForward()driveBackward()。这一次不会使用下划线。

    // 定义一个简单的Car构造函数
    var Car = function (settings) {
        this.model = settings.model || "no model provided";
        this.color = settings.color || "no colour provided";
    };
    
    // Mixin
    var Mixin = function () {};
    
    Mixin.prototype = {
        driveForward: function () {
            console.log("drive forward");
        },
    
        driveBackward: function () {
            console.log("drive backward");
        },
    
        driveSideways: function () {
            console.log("drive sideways");
        }
    };
    
    // 从另一个对象扩展现有对象的方法
    function augment(receivingClass, givingClass) {
        // 只提供某些方法
        if (arguments[2]) {
            for (var i = 2, len = arguments.length; i < len; i++) {
                receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
                // i = 2: Car.prototype["driveForward"] = Mixin.prototype["driveForward"]
                // i = 3: Car.prototype["driveBackward"] = Mixin.prototype["driveBackward"]
            }
        }
        // 提供所有方法
        else {
            for (var methodName in givingClass.prototype) { // 遍历givingClass.prototype的Key: driveForward / driveBackward / driveSideways
    
                // 检查以确保接收类没有与当前正在处理的方法同名的方法
                if (!Object.hasOwnProperty.call(receivingClass.prototype, methodName)) {
                    receivingClass.prototype[methodName] = givingClass.prototype[methodName];
                }
    
                // 另一种,检查receivingClass.prototype中是否存在同名的方法,类似。
                // if ( !receivingClass.prototype[methodName] ) {
                // receivingClass.prototype[methodName] = givingClass.prototype[methodName];
                // }
            }
        }
    }
    
    
    // 扩展Car的"driveForward"和"driveBackward"
    augment(Car, Mixin, "driveForward", "driveBackward");
    // 创建一个Car实例
    var myCar = new Car({
        model: "Ford Escort",
        color: "blue"
    });
    // 测试以确保我们现在可以访问这些方法
    myCar.driveForward();   // drive forward
    myCar.driveBackward();  // drive backward
    
    // ---------------------------------------------
    
    // 我们还可以增加Car,以包含来自mixin的所有功能
    augment(Car, Mixin);
    
    var mySportsCar = new Car({
        model: "Porsche",
        color: "red"
    });
    
    mySportsCar.driveSideways();    // // drive sideways
    

    优缺点:

    • Mixins有助于减少功能重复并增加系统中的函数复用。 如果应用程序需要跨对象实例的共享行为,我们可以专门在Mixin中维护此共享函数集,以避免任何重复,从而专注于实现我们系统中真正独有的功能。

    • 也就是说,Mixins的缺点是有争议的。 一些开发人员认为将功能注入对象原型是一个坏主意,因为它会导致原型污染和我们功能起源的不确定性。 在大型系统中,情况可能就是如此。

    • 我认为强大的文档可以帮助减少mixed的混乱,但是与每种模式一样,如果在实现过程中小心,我们应该没问题。


    5. 观察者模式(发布者—订阅者模式)

    此篇内容笔记全部来自《JavaScript设计模式》观察者模式章节。

    概念

    在事件驱动的环境中,比如浏览器这种持续寻求用户关注的环境中,观察者模式是一种管理人与其任务之间的关系(确切的讲,是对象及其行为和状态之间关系)的得力工具。用JavaScript的话说,这种模式的实质是你可以对程序中某个对象的状态进行观察,并且在其发生改变时能够得到通知
    观察者模式中存在两个角色:观察者和被观察者,或者称其为发布者和订阅者。这种模式在JavaScript中有几种不同的实现方式,首先要说明发布者和订阅者这两种角色。

    示例:报纸的投送

    在报纸行业中,发行和订阅的顺利进行有赖于一些关键性的角色和行为。首先是读者。他们都是订阅者(subscriber),是与你我一样的人。我们消费数据并且根据读到的消息做出反应。我们可以选择自己的居住地点,让报社把报纸送到自己家中。这个活动中的另一个角色是发行方(publisher)。他们负责出版报纸。
    确定了各方身份后,我们就可以分析每一方的职责所在。作为报纸的订阅者,我们有一些事要做。数据到来的时候我们收到通知。我们消费数据。然后我们根据做出反应。只要报纸到了订阅者手中,他们就可以自行处置。有些人读完之后就会将其扔在以便,有些人会向朋友或者家人转述其中的新闻,甚至还有一些人会把报纸送回去。总而言之,订阅者要从发行方接收数据。发行方则要发送数据。在本例中,发行方也是投送方(deliver)。一般来说,一个发行方很可能有许多订阅者,同样,一个订阅者也很可能会订阅多家报社的报纸。问题的关键在于,这是一种多对多关系,需要一种高级的抽象策略,以便订阅者能够彼此独立地发生改变,而发行方能够接受任何消费意向的订阅者。

    推与拉的比较

    对于报社来说,只为给几个订阅者投送报纸就满世界跑是不划算的。而纽约市的居民不可能特意飞到旧金山与拿自己的订的旧金山纪时报,要知道这份报纸可以直接投送到他们家门口。
    订阅者要想拿到报纸有两种投送方式可选:推或拉。在推环境中,发行方很可能会雇佣投送人员四处送报,他们把自己的报纸推出去,让订阅者收取。在拉环境中,规模较小的本地报社没有足够的资源进行大规模的投送,因此采用拉方案,让订阅者到当地的杂货店或自动售货机那里”拿“报,对于它们来说往往是个优化投送环节的好办法。
    抽象成:订阅者被动收取或者主动收取

    模式的实践

    在JavaScript中有多种方法可以实现发布者-订阅者模式。展示示例之前,我们先确保各种角色的扮演者(对象)及其行为(方法)都已就绪。

    • 订阅者可以订阅和退订,他们还要接收。他们可以在由人投送(being delivered to)和自己收取(being taken from)之间进行选择。
    • 发布者负责投送,他们可以在送出(giving)和由人取(being taken from)之间进行选择。

    对于发布者和订阅者的投送和接收动作中是相对的。

    下面是一个展示发布者和订阅者之间的互动过程的高层实例。它是Shells方法(Sellisan approach)的一个示范。这种技术类似于测试驱动的开发(TDD),不过它要求先实现代码,就像API已经写好了一样。为了让这些代码成为可运转的真正实现,程序员需要完成各种该做的工作,API由此形成:

    /*
    http://pluralsight.com/blogs/dbox/archive/2007/01/24/45864.aspx
    */
    /*
        * Publishers are in charge of "publishing" i.e. creating the event.
          出版商负责"发布"  (创建事件)
        * They're also in charge of "notifying" (firing the event).
          他们还负责"通知"  (触发事件)
    */
    var Publisher = new Observable;
    
    /*
        * Subscribers basically... "subscribe" (or listen).
          订阅者主要... (订阅或者侦听)
        * Once they've been "notified" their callback functions are invoked.
          一旦他们收到通知,就会触发回调函数
    */
    var Subscriber = function(news) {
        // news delivered directly to my front porch
        // 新闻直接送到入口处
    };
    Publisher.subscribeCustomer(Subscriber);
    
    /*
        * Deliver a paper:
          投递一篇文章
        * sends out the news to all subscribers.
          向所有订阅者发送新闻
    */
    Publisher.deliver('extre, extre, read all about it');
    
    /*
        * That customer forgot to pay his bill.
          有个顾客忘记"付账"了
    */
    Publisher.unSubscribeCustomer(Subscriber);
    

    在这个模型中,可以看出发布者处于明显的主导地位。它们负责登记其顾客,而且有权停止为其投送。最后,新的报纸出版后它们会将其投送给顾客。

    上面的代码创建了一个新的可观察(observable)对象。它有三个实例方法:subscribe-CustomerunSubscribeCustomerdeliver
    subscribeCustomer方法以一个代表订阅者的回调函数为参数。deliver方法在调用过程中,将通过这些回调函数把数据发送给每一个订阅者。

    下面的例子处理的是同一类问题,但发布者和订阅者之间的互动方式有所不同:

    /*
        * Newspaper Vendors
          报纸供应商
        * setup as new Publisher objects
          设置新的发行者对象
    */
    var NewsYorkTimes = new Publisher;
    var AustinHerald = new Publisher;
    var SfChronicle = new Publisher;
    
    /*
        * People who like to read
        * (Subscribers)
        * 
        * Each subscriber is set up as a callback method.
        * They all inherit from the Function prototype Object.
        * 每个用户被设置为一个回调方法。它们都继承自Function.prototype对象。
    */
    var Joe = function(from) {
        console.log('Delivery from ' +  from + ' to Joe');
    };
    var Lindsay = function(from) {
        console.log("Delivery from " + from + ' to Lindsay');
    };
    var Quadaras = function(from) {
        console.log("Delivery from " + from + ' to Quadaras');
    };
    
    /*
        * Here we allow them to subscribe to newspapers
        * which are the Publisher objects.
        * In this case Joe subscribes to NY Times
        * Austin Herald and Chronicle. And the Quadaras
        * respectfully subscribe to the Herald and the Chronicle  
        * 在这里,我们允许他们订阅Publisher对象的报纸。
        * 乔订阅纽约时报奥斯汀先驱报和纪事报。 
        * Quadaras则是“先驱报”和“纪事报”
    */
    Joe.
      subscribe(NewsYorkTimes).
      subscribe(SfChronicle);
    
    Lindsay.
      subscribe(AustinHerald).
      subscribe(SfChronicle).
      subscribe(NewsYorkTimes);
    
    Quadaras.
    subscribe(AustinHerald).
    subscribe(SfChronicle);
    
    /*
        * Then at any given time in our application, our publishers can send
        * off data for the subscribers to consume and react to.
        * 然后,在我们的应用中的任何时间,我们的发布者都可以发送数据,供订阅者使用和做出反应。
    */
    NewsYorkTimes.
        deliver("Here is your paper! Direct from the Big apple"),
    AustinHerald.
        deliver("News").
        deliver("Reviews").
        deliver("Coupons");
    SfChronicle.
        deliver("The weather is still chilly").
        deliver("Hi Mon! I\'m writing a book");
    

    在这个例子中,发布者的创建方式和订阅者接收数据的方式没有多少改变,但拥有订阅和退订权的一方变成了订阅者。当然,负责发送数据的还是发布者一方。

    构建观察者API

    另一种抽象

    发布和订阅这两个对象是松散耦合的联系在一起的,它们不用彼此熟悉内部的实现细节,但这不影响它们之间的通信,它们只要知道彼此需要做什么就行。当有新订阅者增加时,发布者不需要任何更改,同样的,当发布者改变时,订阅者也不会受到影响。

    JavaScript中的观察者模式
    在JavaScript中观察者模式的实现主要用于事件模型。

    document.body.addEventListener("click",function() {});
    
    /*等价于*/
    
    // 发布者
    var publisher = function(){};
    // 订阅者
    var subscriber = document.body;
    // 订阅者订阅
    subscriber.addEventListener("click",publisher,false);
    

    这就是一种观察者模式的实现。

    自定义事件
    function Publisher() {
        this.eventList = [];
    };
    Publisher.prototype.subscriber = function(id,handler) {
        if (!this.eventList[id]) {
            this.eventList[id] = [];
        }
        this.eventList[id].push(
            {
                handler:handler
            }
        );
    }
    Publisher.prototype.publish = function(id,arg) {
        if (!this.eventList[id] || this.eventList[id].length === 0) {
            console.log("没有任何订阅");
            return false;
        }
        // OR
        for (var i = 0; i < this.eventList[id].length; i++) {
            this.eventList[id][i].handler(arg);
        }
    }
    Publisher.prototype.unSubscriber = function(id,fn) {
        if (!this.eventList[id]) {
            console.log("没有订阅该事件,无法取消")
            return false;
        };
    
        if (!fn) {
            this.eventList[id] = [];
            console.log(id + " :该事件已被取消订阅")
        } else {
            for (var i = 0; i < this.eventList[id].length; i++) {
                if (this.eventList[id][i].handler === fn) {
                    this.eventList[id].splice(i,1);
                    console.log(id + " 事件的" + "\n" + fn + "\n" + "函数已被取消订阅");
                }
            }
        }
    }
    
    var eventA_fn1 = function(num) {
        console.log(++num);
    }
    var publisherA = new Publisher();
    publisherA.subscriber("eventA",eventA_fn1);
    publisherA.publish("eventA",10);                // 11
    
    
    var eventB_fn1 = function() {console.log("eventB publish eventB_fn1")};
    publisherA.subscriber("eventB",eventB_fn1);
    publisherA.unSubscriber("eventB");              // eventB :该事件已被取消订阅
    

    Reference

    设计模式
    Learning JavaScript Design Patterns
    深入理解JavaScript系列
    详解 Javascript十大常用设计模式

    相关文章

      网友评论

        本文标题:JavaScript设计模式

        本文链接:https://www.haomeiwen.com/subject/pqzaaftx.html