JS进阶学习笔记-设计模式

JS进阶学习笔记-设计模式

本文绝大多数概念是基于ES5之前的,本人认为这些都是学习或者掌握ES6必须要理解的,因为ES6其实就是对ES5之前的方法的包装,理解透彻ES5之前的概念,那么学ES6就极其简单。

1. 初窥

1.1 灵活的JS

JS极其灵活,对于同样一段业务代码,有的可以写的很复杂,有的却可以写的很简单。

例如:启动和停止一个动画

面向过程方式:

// 开始
function startAnimation() {
    console.log("start...")
}
// 结束
function stopAnimation(){
    console.log("stop...")
}
// 调用
startAnimation();
stopAnimation()

面向对象方式一:

// 创建一个对象
var Animation = function(){}
Animation.prototype.start = function(){
    console.log("start...")
}

Animation.prototype.stop = function(){
    console.log("stop...")
}

// 使用
var test = new Animation();
test.start();
test.stop();

面向对象方式二:

var Animation = function(){}
Animation.prototype = {
    start: function(){
        console.log("start...")
    },
    stop: function(){
        console.log("stop...")
    }
}

更进一步:

// 在Function上增加方法
Function.prototype.method = function(name, fn){
    this.prototype[name] = fn;
    return this; // 为了可以链式调用
}
var Animation = function(){};
Animation.method('start', function(){
    console.log("start...");
    return this;// 为了可以链式调用
})
Animation.method('stop',function(){
    console.log('stop...');
})

// 使用
var test = new Animation();
// 链式调用
test.start().stop();
new Animation().start().stop(); 

这里加了返回值return this,是便于链式调用,stop()中没有加,是为了考虑逻辑,即new Animation().stop().start(); `是错误的。

对于在原型prototype上增加的方法,如Function.prototype.method等,必须使用new关键字,才能调用。后续会讲解。

1.2 JS是弱类型语言

在js中定义变量,不像java,是不需要声明其类型。但是,这并不意味着js中没有类型,其实,js中变量在一初始化的时候就决定了变量的类型,再给变量赋予不同的类型时,变量对应的类型就随之改变。

也就是说,js中的变量会根据所赋予的值而改变类型

类型 分类 内存位置 typeof 输出
boolean布尔 基本数据类型(又称原始数据类型) boolean
number数字 基本数据类型(NAN也属于number) number
string字符串 基本数据类型 string
null空 基本数据类型 object
undefined未定义 基本数据类型 undefined
symbol独一无二的值 基本数据类型 symbol
Object对象 引用数据类型(function,array...) 堆,栈中保存引用的地址 function、object

一般,可以使用typeof来判断数据类型,但是并不准确。为了精确判断,建议使用这个方法:Object.prototype.toString.call(要判断的变量),它返回如:"[object Function]"

1.3 函数是一等公民

在JS中,函数是一等公民(First-class Function)。它可以存储在变量中,可以作为参数传给其他函数,可以作为返回值从其他函数传出,还可以运行时进行构造。

  • 匿名函数自执行(IIFE),又称立即调用函数表达式。

    // 无参
    (function(){
      var x = 3;
      var y = 7;
      console.log(x*y)
    })()
    // 有参
    (function(x,y){
      console.log(x*y)
    })(3,7)
    

    除了使用()包裹的方式,其实还有~function(){...}()!function(){...}()等方式,都可以实现自执行。一般建议使用括号的方式,安全易读。

1.4 对象的易变性

JS虽然不像java是强制面向对象编程语言,但是在JS中一切都是对象(基本数据类型,也可以包装成对象),而且所有的对象都是异变的。例如,给函数添加属性:

function test(){
    test.num++;
    console.log(test.num)
}
test.num = 10;
test(); // 输出 11

这意味着,我们可以对先前定义的类和实例化的对象进行修改,例如:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype = {
    getName: function(){
        return this.name
    },
    getAge: function(){
        return this.age
    }
}
// 实例化对象
var tom = new Person('Tom',12);
var jerry = new Person('jerry',12);
// 给对象新增方法
Person.prototype.sayHi = function() {
    return 'Hi, ' + this.getName() + '!';
}

jerry.sayHello = function(){
    return 'Hello, ' + this.getName() + '!';
}

tom.sayHi(); // Hi, Tom!
jerry.sayHi(); // Hi, Jerry!
jerry.sayHello(); // Hello, Jerry!
tom.sayHello(); // Cannot set property 'sayHello' of undefined

这里,我们在实例化Person对象之后新增了sayHi方法,并且成功调用,即我们可以扩展实例化之后的对象。通过prototype我们给所有的实例都新增了sayHi的方法。这里需要理解继承与原型链,后面会讲。

每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

1.5 继承

JS中使用的是基于对象的继承,即原型式(prototype)继承。比如,Function B继承Function A

  • 类型被定义在 .prototype
  • Object.create() 来继承
function A(){};
A.prototype = {
    varA: "AAA",
    doSomething: function(){
        console.log("AAA...")
    }
}

function B(){};
B.prototype = Object.create(A.prototype, {
    varB: {
        value: "BBB", 
        enumerable: true, 
        configurable: true, 
        writable: true 
    },
    doSomething : { 
    value: function(){ // override覆写
     // A.prototype.doSomething.apply(this, arguments); 
     console.log("BBB...")
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
})

B.prototype.constructor = B;


var b = new B();
b.doSomething(); // BBB...
console.log(b.varA); // AAA
console.log(b.varB); // BBB

还可以模仿基于类(class)的继承:ECMAScript6 引入了一套新的关键字用来实现 class,但JavaScript 仍然基于原型,这里只不过相当于语法糖,背后的实现还是原型式继承。这些新的关键字包括 class, constructorstaticextendssuper

"use strict";

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

class Student extends Person{
    // 构造方法
    constructor(name,age,score){
        // super可以认为指代父类
        super(name,age)
        // 扩展
        this.score = score
    }
    get getScore() {
        return this.score;
    }
    set setScore(score) {
        this.score = score
    }
}

这里只是简单的了解了一下JS中的继承,后面会深入研究。

1.6 JS中的设计模式

JS是如此的灵活,有如此的简单与复杂,为了更加方便维护代码,写出可重用的代码,还是得使用设计模式。

  • 可维护性,设计模式有利于降低模块间的耦合,便于开发和维护。

  • 易于表达,使用不同的设计模式,可以清晰的表达不同业务逻辑的实现意图。

  • 提升性能,使用设计模式,在某些程度上可以减少代码量。

2. 接口

“函数要面向接口,而不是实现” —— GoF《设计模式》

设计模式Java版

Design-Pattern

2.1 接口的概念

接口可以规定一个对象中应该具有哪些属性和方法,但不用具体实现这些方法,实现了这些接口的对象,必须实现这项属性或方法。也就是说接口的出现给对象增加了标准。

2.2 接口的优缺点

优点:

  • 增加类的重用性。比如A和B均实现了接口C,若我会使用A类,那么B类也很容易就会使用了;或者我熟悉C接口,不管谁实现了这个接口,我就能很容易去使用它。
  • 增加代码的可读性。比如我事先吧接口文档写好,然后实现了一些程序,当其他程序员来接手时,只需要看接口文档,就能很容易知道我的程序中类的特性,从而更容易更改程序以及实现自己的业务逻辑。
  • 增加代码的可维护性,接口可以降低对象间的耦合度。利用接口,各个组件之间只暴露少许接口共其他组件调用,如果修改了某个组件,并不会影响其他组件。
  • 测试和调试更方便。如果一些类都实现了某个接口,但是却没能完全继承接口中定义的标准,程序很容易就被发现;如果业务扩展,接口需要新增一个方法,那么实现了这些接口的类,如果忘了添加这个方法,也会报错。

缺点:

  • JS是弱类型语言,接口的使用原则会强化类型的作用,降低JS语言的灵活性。
  • 接口一旦被定义,如果再次改动,需要把所有实现了该接口的类改动,比如,某些类不需要这个方法,就会增加额外的开销。
  • JS没有提供对接口的内置支持,就算某些类实现了这个接口,如果不遵循接口的标准,并且不加严格的限制与检查,是无法保证接口的实现的。

2.3 JS模仿接口

2.3.1 用注释描述接口

用注释描述接口是最简单也是效果最差的方法。即使用interface 和 implements关键字表示接口,但是将他们写在注视中,因为JS并不认识,避免与法错误。

通俗地讲,注释中告知,该类需现了这些接口,需要定义这些方法:

/* 定义接口
接口一
interface Composite{
    function add(child);
    function remove(child);
    function getChild(index);
}
接口二
interface FormItem{
    function save();
}
*/

var CompositeForm = function(id, method, action){// implements Composite,FormItem
};
// 实现接口Composite
CompositeForm.prototype.add = function(child){console.log("add...")}
CompositeForm.prototype.remove = function(child){console.log("remove...")}  
CompositeForm.prototype.getChild = function(index){console.log("getChild...")}   
// 实现接口FormItem
CompositeForm.prototype.save = function(){console.log("save...")}        

呵,有点东施效颦的味道。这个方式不能保证完全实现了接口,就算不按照接口来,也不会报错,对测试和调试也无帮助。但是胜在简单,也确是实现了接口的意图。

2.3.2 用属性检查模仿接口

其实,这种方式就是在上面的方式基础上,增加了属性判断,判断类中实现了那些接口,稍微严谨一些。接口仍然写在注释中。

var CompositeForm = function(id,method,action){
    // 显示地申明,本类实现了如下接口
    this.implementsInterfaces = ['Composite', 'FormItem'];
}

// 定义一个添加实例的方法,并检测实例是否实现了接口
function addForm(formInstance) {
    // 判断实例是否实现了接口,否则抛出错误
    if(!implements(formInstance, 'Composite','FormItem')){
        throw new Error('Object does not implements a required interface!');
    }
}
// 定义判断是否实现接口的方法
function implements(object) {
    for (var i = 1; i < arguments.length; i++) {
        var interfaceName = arguments[i];
        var interfaceFound = false;
        // 遍历implementsInterfaces,看是否实现了对应的接口
        for (var j = 0; j < object.implementsInstances.length; j ++) {
            if (object.implementsInterfaces[j] == interfaceName) {
                interfaceFound = true; 
                break;
            }
        }
        if(!interfaceFound){
            return false; // 没有实现接口
        }
    }
    return true; // 实现了接口
}

这里CompositeForm说他实现了某某接口,那就判断一下,发现确实实现了,OK,否则就会跑出错误。但是这个简陋的判断并没有保证,完全实现接口中定义的方法,所以还是会埋下隐患。

2.3.3 用鸭式辩型模仿接口

“像鸭子一样走路并且嘎嘎叫的就是鸭子” —— James Whitcomb Riley鸭子类型

基于这种思想:类是否申明自己支持哪些接口并不重要,只要它具有这些接口中的方法就行。即如果对象具有与接口定义的方法同名的所有方法,那么就可以认为它实现了这个接口。

var Interface = function(name, methods) {
    // 保证传入方法名和方法集合,两个参数
    if (arguments.length !== 2) {
        throw new Error(
            "Interface constructor called with " + arguments.length + "arguments, but expected exactly 2."
        );
    }

    this.name = name;
    this.methods = [];
    for (var i = 0, len = methods.length; i < len; i++) {
        // 方法名类型string
        if (typeof methods[i] !== "string") {
            throw new Error(
                "Interface constructor expects method names to be passed in as a string."
            );
        }
        this.methods.push(methods[i]);
    }
};
// 定义static方法,可以直接通过类名调用而不用实例化
Interface.ensureImplements = function(object) {
    if (arguments.length < 2) {
        throw new Error(
            "Function Interface.ensureImplements called with " + arguments.length + " arguments, but expected at least 2."
        );
    }

    for (var i = 1, len = arguments.length; i < len; i++) {
        var interface = arguments[i];
        if (interface.constructor !== Interface) {
            throw new Error(
                "Function Interface.ensureImplements expects arguments two and above to be instance of Interface."
            );
        }
        for (var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
            var method = interface.methods[j];
            if (!object[method] || typeof object[method] !== "function") {
                // 抛出错误:没有实现接口以及方法没有找到
                throw new Error(
                    "Function interface.ensureImplements:object does not implements the " +
                        interface.name + "interface. Method " + method + " was not found"
                );
            }
        }
    }
};

上面的类定义了接口以及接口的检测,可以作为工具类使用。


// 使用:
// 定义接口,以及接口中的方法
var Composite = new Interface("Composite", ["add", "remove", "getChild"]);
var FormItem = new Interface("FormItem", ["save"]);

var CompositeForm = function(id, method, action) {
    // implements Composite,FormItem
};

var myForm = new CompositeForm();
// 实现接口
myForm.prototype.add = function(){}
myForm.prototype.remove = function(){}

// 检测实例是否完全实现了接口
function addForm(formInstance) {
    Interface.ensureImplements(formInstance, Composite, FormItem);
    // 其他操作
}
addForm(myForm);

Interface.ensureImplements方法会对实例严格检测,如果实例中没有完全实现对应的接口就会抛出错误。这样,我们就实现了接口的概念。

2.3.4 依赖于接口的设计模式

  • 工厂模式
  • 组合模式
  • 装饰者模式
  • 命令模式

3. 封装

封装(encapsulation)就是将不必要暴露的方法或属性隐藏起来,不让外部方法访问,以防止篡改或滥用,降低代码间的耦合度,提升安全性。

JS中没有像Java中的关键字private,实现封装的方法——闭包(closure)。

3.1 类中属性的私有化

在类中,一般使用下划线开头来定义私有属性,这是总所周知的命名规范,它表明一个属性(或方法)仅提供在对象内部使用。

注意,这种方式只是一种约定,并不能强制性决定外部不能访问。

// 定义一个三角形类,构造方法
var Triangle = function(x,y,z){
    this.setX(x);
    this.setY(y);
    this.setZ(z);
}

// 判断能否构成三角形,静态方法,通过类名调用,不用new实例化
Triangle.isTriangle = function(x,y,z){
    return x+y>z && x+z>y && y+z>x;
}
// 实例方法,必须new之后才能调用
Triangle.prototype = {
    setX: function(x){
        this._x = x;
    },
     setY: function(y){
        this._y = y;
    },
     setZ: function(z){
        this._z = z;
    },
    
    getX: function(){
        if(!Triangle.isTriangle(this._x,this._y,this._z)){
            throw new Error('不能构成三角形!')
        }
        return this._x;
    },
    getY: function(){
          if(!Triangle.isTriangle(this._x,this._y,this._z)){
            throw new Error('不能构成三角形!')
        }
        return this._y;
    },
    getZ: function(){
          if(!Triangle.isTriangle(this._x,this._y,this._z)){
            throw new Error('不能构成三角形!')
        }
        return this._z;
    } 
}

这样一来,虽然我们传进去的参数是x,y,z,我们是直接获取不到的,可以通过get方法获取。

var san = new Triangle(3,4,5);
san.x; // undefined;
san.getX(); // 3
san._x; // 3

但是,我们其实是可以通过_x获取的。加上下划线只是表明这些属性是私有的,最好不要直接访问。

3.2 作用域、嵌套函数和闭包

在JS中,只有函数具有作用域。即,在一个函数内部声明的变量在函数外部通常是无法访问的。

function outer(){
    var a=10;
    
    function inner(){
        a *= 2;
    }
    inner();
    return a;
}

outer(); // 20
outer(); // 20
a; // undefined

这里,在函数内部定义函数,内部函数访问了外部函数定义的变量a,函数外面是访问不到变量a的。即,函数内部有局部作用域。那么,如果想访问变量a呢?

function outer(){
    var a=10;
    // 将内部函数返回
    return function(){
        a *= 2;
        return a;
    };
}

var test = outer();
test(); // 20
test(); // 40
test(); // 60

var demo = outer();
demo(); // 20

这样就形成了闭包。闭包是函数和声明该函数的词法环境的组合。

闭包,即通过函数内部定义函数,在函数外面通过访问这个内部函数(可以访问外部函数的定义的变量)从而实现了操作函数内的局部变量的现象。

闭包就是将函数内部和函数外部连接起来的一座桥梁。通过闭包,可以访问函数内的局部变量。

var Triangle = (function(){
    // private static attributes 私有静态属性
    var id = 0;
    // private static method 私有静态方法
    function yy(){
        console.log('I am the best tiangle!')
    }
    yy();
    
    // return constructor 返回构造方法
    return function(x,y,z){
        // private attributes 私有属性
        var _x,_y,_z;
        console.log(this);
        // privileged methods 特权方法
        this.getX = function(){
            return _x;
        }
        this.setX = function(x){
            _x = x;
        }
        this.getY = function(){
            return _y;
        }
        this.setY = function(y){
            _y = y;
        }
        this.getZ = function(){
            return _z;
        }
        this.setZ = function(z){
            _z = z;
        }
        this.getId = function(){
            return id;
        }
        // Constructor code 构造区代码
        id++; // 统计Triangle实例化对象个数
        this.setX(x);
        this.setX(y);
        this.setX(z);
    }
})()

// public static method 公有静态方法
Triangle.isTriangle = function(x,y,z){
     return x+y>z && x+z>y && y+z>x; 
}

// public no-privileged methods 公有非特权方法
Triangle.prototype = {
    
}

// 判断能不能构成三角形
if(Triangle.isTriangle(3,4,5)){
   var t1 = new Triangle(3,4,5) 
   t1.getId(); // 1
    t1.getX(); // 3
    
    var t2 = new Triangle(3,4,5);
    t1.getId(); // 2
}

下面,我们来理解一下这些“新概念”:

var A = (function(){
    // 这里定义私有属性和方法
    var haha = "我没有鄙视你!反正你也看不到!";
    function hehe(){
        console.log(haha);
    }
    hehe();
    
    // 返回构造方法
    return function(x){
        // 定义私有属性
        var _x;
        // 定义特权方法,外部通过特权方法访问和操作内部私有属性
        this.getX = function(){
            return _x;
        }
        this.setX = function(x){
            _x = x;
        }
        // 通过特权方法,不仅可以访问内部函数的局部变量,还可以访问外部函数的局部变量
         this.getHaha = function(){
            console.log("自己暴露了自己!!!(⊙o⊙)…完蛋,被发现了!");
            return haha;
        }
        
       // 构造区代码块,只要实例化就会被调用
        this.setX(x);
        console.log("我被实例化了");
    }
})()

// 普通公有方法(实例属性),不能访问内部变量
A.prototype = {
    say: function(){
        console.log("我是公有方法,你需要new之后才能调用");
    },
    sayHi: function(){
        console.log("Hello world!");
    },
    hehe: "heheheheh"
};

A.staticMethod = function(){
    console.log("我是静态方法,你不要new,就可以通过类名调用");
}

这样,我们就实现了对类的私有属性以及方法的封装。需要理解的概念有闭包静态方法原型链new关键字作用域构造方法

4. 继承

4.1 类式继承

// 定义一个Person类
function Person(name) {
    this.name = name;
}

Person.prototype.getName = function(){
    return this.name;
}
Person.prototype = {
    className: "Person",
    getName: function(){
        return this.name
    },
}
    
// 使用new关键字实例化类
var p = new Person('Tom');
p.getName();

下面,通过原型链来实现继承:

// 定义一个Author类,继承Person
function Author(name, books) {
    // 1.调用父类构造方法
    Person.call(this, name);
    this.books = books;
}
// 2.将子类的原型指向父类
Author.prototype = new Person();
// 3.更新子类的构造方法
Author.prototype.constructor = Author;
// 多态,子类实现自己的属性
Author.prototype = {
    className: "Author",
    getBooks: function(){
        return this.books;
    }
}

// 使用new关键字实例化类
var a = new Author('Jerry', 'Tom and Jerry!');
a.getName(); // Jerry

这样就实现了Author类继承Person类的操作。三步走:

  • 1.子类创造构造函数,并在构造函数中调用父类的构造函数,将参数传给父类。Person.call(this,name)

  • 2.设置子类的原型链(prototype,要么指向另一个对象,要么指向null),使他指向父类对象。Author.prototype=new Author()

  • 3.将子类的构造方法更新为子类对象。Author.prototype.construtor=Author

    原型链查找:在访问对象的某个属性时(如name属性),首先在该类的实例属性上(Author.name)查找,没找到,就会在该类的原型上查找(Author.__proto__),即prototype所指向的对象;如果还没找到,继续向上查找(Author.__proto__.__proto__),直到直到找到这个属性或者查到原型链最顶端(Object.prototype最终指向null)。

4.2 封装继承的方法

为了简化类的声明,ES6中实现了extends关键字,如果我们自己实现一个extend函数呢?

/*extend function*/
function extend(subClass, superClass){
    // 空函数F,避免创建超类的实例
    var F = function(){};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
   
    subClass.prototype.constructor = subClass;
}

// 实现继承
function Author(name,books){
    // 更改父类的this指向,相当于ES6继承中的super
    Person.call(this,name);
    this.books=books;
}
// 继承
extend(Author,Person);

// 必须继承之后再新增方法
Author.prototype.getBooks = function(){
    return this.books;
}

上面可以实现继承,子类中需要直接调用父类的构造方法Person.call(this,name),来更新this指向,否则子类获取不到this对象。下面对这一部分进行改善:

/*extend function*/
function extend(subClass, superClass){
    // 空函数F,避免创建超类的实例
    var F = function(){};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;
    
    // superclass属性弱化子类和超类之间的耦合
    // 将父类保存起来
    subClass.superclass = superClass.prototype;
    if(superClass.prototype.constructor==Object.prototype.constructor){
        subClass.prototype.constructor = superClass;
    }
}

// 实现继承
function Author(name,books){
    // 改善
    Author.superclass.constructor.call(this,name);
    this.books=books;
}
extend(Author,Person);

Author.prototype.getBooks = function(){
    return this.books;
}

4.3 原型式继承

原型式继承和类式继承截然不同。原型式继承,要从对象的角度考虑。下面我们用对象的方式重新实现上面的Person、Author类的继承:

  • 1.用一个类声明定义对象的结构

    /* Person Prototype Object*/
    var Person = {
        name: 'person',
        getName: function(){
            return this.name;
        }
    }
    
  • 2.实例化该类创建一个新对象

    var Author = clone(Person);
    Author.name = 'author'
    Author.books = [];
    Author.getBooks = function(){
        return this.books;
    }
    
    // 每次实例化都要使用clone方法
    var author = clone(Author);
    author.name = 'zkk'
    author.books = ['JavaScript设计模式', '深入浅出ES6'];
    author.getName();
    author.getBooks();
    
  • 3.编写clone方法,实现继承

    // 创建一个以superClass为原型的空对象
    function clone(superClass) {
        function F(){};
        F.prototype = object;
        return new F;
    }
    

4.4 类式继承与原型式继承

类式继承的用法更广泛,一般封装的库,都是用这种方式。

原型式继承一般用得比较少,但是最好还是要理解这种方式。

原型式继承更能节省内存空间,并且更简洁。但类式继承因为是模仿传统语言的继承方式,更容易被理解,所以被广泛应用。

鉴于此,一般建议还是使用类式继承,但不要忘记还有原型式继承。

4.5 部分继承——掺元类(Mixin Class)

掺元类(或混合类 mixin class): 即通过扩充(augmentation)的方式拥有某些类中的方法而不用继承这些类,那么提供这些方法的类(被共享方法的类)就是掺元类。

  1. 定义mixin class提供方法:

     // 定义一个Mixin class 
    var Mixin = function(){};
    Mixin.prototype = {
        serialize: function(){
            var output = [];
            for(key in this){
                output.push(key + ': ' + this[key]);
            }
            return output.join(', ');
        },
        other: function(){};
    }
    
  2. 将mixin class中提供的函数扩充到其他类中:

    // 利用augemnt将Mixin中的serialize方法扩充到Author类中
    augment(Author, Mixin, 'serialize')
    var author = new Author('Joe', ['HTML','CSS']);
    var str = author.serialize();
    
  3. 实现扩充方法augment:

    // 参数:接受的类,提供的类,方法1,方法2,。。。(方法名为可选参数)
    function augment(receive, giveClass){
        if(arguments[2]){ // 明确要扩充的方法,那么只提供这些方法
            for(var i=2,len=arguments.length;i

    JS中一个对象只能有一个原型对象,所以不允许子类继承多个父类,但是可以通过掺元类的方式实现扩充多个类中的不同方法。

5. 链式调用

5.1 什么是链式调用?

例如,在Jquery项目中,有:

$("#id").addClass('active').siblings().removeClass('active');

这就是 链式调用,即利用.操作符,在目标对象上连续调用函数的方式。这种方式,可以减少代码量,但是会覆盖上一次函数的返回值。

5.2 链式调用原理

链式调用的核心技术就是在每个方法后面返回当前对象,即return this,比如:一个人,吃完饭,工作,然后睡觉,实现如下:

function Person(){};
Person.prototype = {
    eat: function(){
        console.log("eat...");
    },
    work: function(){
        console.log("work...")
    },
    sleep: function(){
        console.log("sleep...")
    }
}
// 调用
var p = new Person();
p.eat();
p.work();
p.sleep();

这是最基本的实现方式,如果用链式调用呢?

function Person(){};
Person.prototype = {
    eat: function(){
        console.log("eat...");
        return this;
    },
    work: function(){
        console.log("work...");
        return this;
    },
    sleep: function(){
        console.log("sleep...");
        return this;
    }
}
// 调用
var p = new Person();
p.eat().work().sleep();

// 或者
new Person().eat().work().sleep();

通过改造,在每个方法中都返回了当前对象,即return this,这样每次函数的返回值都是当前对象实例,就可以继续调用它的方法了。

5.3 使回调函数从链式调用中获取数据

发现一个问题,就是如果我想要方法返回数据,而不是this对象,那么怎么实现呢?

链式调用适合于赋值器方法,但对于取值器方法(返回特定数据)并不友好,但我们可以使回调函数获取所要的数据。或者在特定的get方法后面直接返回数据而不是当前对象。

function Person(name){
    var _name = name;
    // 特权方法
    this.setName = function(name){
        _name = name;
        return this;
    };
    this.getName = function(){
        // 这里没有返回this
        return _name;
    }
}

// 这里只能先调用setName,在链式调用get
new Person('zkk').setName('ZKKK').getName();
new Person('zkk').getName();
// 而不能先get在set
new Person('zkk').getName().steName();
var myname;
function getMyName(name){
    myname = name;
}

// 改造上面的getName
this.getName = function(callback){
    // 通过回调函数获取name
    callback.call(this, _name)
    return this;
};

// 这时,我就可以随意获取或者修改name了
new Person('zkk').getName(getMyName).setName('zkkk').getName(getMyName)...

5.4 设计自己的JS库——支持链式调用

// 定义自己的库
(function(window){
    // 给Function添加新的方法,使每个函数实例都能使用
Function.prototype.method = function(name, fn){
    this.prototype[name] = fn;
    return this;
}   
    
    function _zkk(name, age){
        this.name = name;
        this.age = age;
    };
    
    zkk.method('getName', function(){
        return this.name;
    });
    zkk.method('setName', function(name){
        this.name = name;
        return this;
    });
    
    // 挂载到window上
    window.zkk = function(name,age) {
        return new _zkk(name,age);
    }
})(window)  

6. 单例模式(Singleton)

6.1 基本概念

单体类,只有一个实例对象,即只会被实例化一次,所有的属性共享相同的资源。

通过对象字面量创建一个最简单的Singleton:

因为JS中普通的对象是不能通过new关键字来实例化的,所以它只有一个实例对象就是它本身。

/*Basic Singleton*/
var Singleton = {
    key1: "value1",
    key2: "value2",
    
    func1: function(){},
    func2: function(){}
}

6.2 基本用途

利用单例模式划分命名空间

比如:A.jsB.js中都有一个方法sayHi()

// A.js
function sayHi(){console.log('Hi! AAA')}
// B.js
function sayHi(){console.log('Hi! BBB')}

当两者都被引入同一个文件时,后引入的会覆盖县引入的,这是通过命名空间就可以解决这个问题。

/* use namespace*/
var A = {
    sayHi:function(){
        console.log('Hi! AAA')
    }
};

var B = {
    sayHi:function(){
        console.log('Hi! BBB')
    }
};
// 通过命名空间调用,不会产生覆盖的问题很容易区分
A.sayHi(); 
B.sayHi();

6.3 私有属性的单例——闭包

// 创建拥有私有变量的单例---闭包
var ss  = (function() {
    // private attribute
    var name = "我是private属性"
    // public attribute
    return {
        key1: "我是lublic属性;
        ",
        key2: "value2",
        
        // public访问私有属性
        func1: function() {
            console.log(name);
        },
        func2: function() {}
    };
})();

// 测试
var a = ss;
var b = ss;
a.key1; // 我是lublic属性;
b.key1; // 我是lublic属性;

a.key1 = "aaa";
b.key1; // aaa;

a===b; // true

单例类一旦被实例化之后,所有的实例(其实就只有一个实例化对象)都共享相同的资源,改变其中一个,另一个也会鞥这改变。利用这个特性我们可以使多个对象共相同的全局资源。

6.4惰性实例化

单体对象都是在被一加载时实例化的,能不能在我需要的时候再实例化呢?这样可以节省资源,这就是惰性实例化,即懒加载技术(lazy loading)。懒加载单体的关键是借助一个静态方法(static method),调用Singleton.getInstance().methodName()来实现。getInstance方法会检测单体是否已经被实例化,如果还没有就创建并返回实例。

  • 1.把单体的所有代码移到private方法constructor
  • 2.通过public方法getInstance调用private方法constructor来控制实例的创建
  • 3.通过private属性uniqueInstance来检测单例是否已经实例化
var Singleton = (function() {
    // 保存单体实例化对象
    var uniqueInstance;
    // 将所有的单体内容包裹在这个私有方法里面
    function constructor() {
        // private attribute
        var name = "我是private属性";
        // public attribute
        return {
            key1: "我是lublic属性",
            key2: "value2",

            // public访问私有属性
            func1: function() {
                console.log(name);
            },
            func2: function() {}
        };
    }
    
    return {
        // 通过这个public方法暴露出去
        getInstance: function(){
            if(!uniqueInstance){ // 检测是否已经实例化
                uniqueInstance = constructor();
            }
            return uniqueInstance;
        }
    }
})();

// 测试
Singleton.getInstance().func1();

简单梳理一下,结构如下:

var singleton = (function(){
    // 保存单体类对象
    var uniqueInstance;
    function constructor(){
        // 单体类主体
        return {}
    }
    return {
        getInstance: function(){
            // 控制单体类实例化
            if(!uniqueInstance){
                uniqueInstance = constructor();
            }
            return uniqueInstance;
        }
    }
})();

6.5 分支技术

分支(branching)是一种用来把浏览器见得差异封装到在运行期间进行设置的动态方法中的技术。比如创建XHR对象,大多数浏览器使用new XMLHttpRequest()创建,低版本IE确是用new ActiveXObject("Microsoft.XMLHTTP")来创建,如果不使用分支技术,每次调用的时候,浏览器都要检测一次。可以利用分支技术,在初始化的时候加载特定的代码,再次调用的时候就不要再次检测了。

大概逻辑如下:

var s = 'A';
var Singleton = (function(str){
    var A = {
        func1: function(){}
    };
    var B = {
        func1: function(){}
    };
    
    return str === 'A'? A: B;
})(s)

封装好的创建XHR对象的函数:

function createXMLHttpRequest() {
    try {
        // 主流浏览器
        return new XMLHttpRequest();
    } catch (e) {
        try {
            // IE6
            return new ActiveXObject("Msxml2.XMLHTTP");
        } catch(e) {
            try {
                // IE5.5及以下
                return new ActiveXObject("Microsoft.XMLHTTP");
            } catch (e) {
                // 未知
                throw e;
            }
        }
    }
}

下面使用分支技术创建XHR对象:

var XHRFactory = (function() {
    var standard = {
        createXMLHttpRequest: function() {
            return new XMLHttpRequest();
        }
    };
    var activeXNew = {
        createXMLHttpRequest: function() {
            return new ActiveXObject("Msxml2.XMLHTTP");
        }
    };
    var activeXOld = {
        createXMLHttpRequest: function() {
            return new ActiveXObject("Microsoft.XMLHTTP");
        }
    };

    try {
        standard.createXMLHttpRequest();
        return standard;
    } catch (e) {
        try {
            activeXNew.createXMLHttpRequest();
            return activeXNew;
        } catch (e) {
            try {
                activeXOld.createXMLHttpRequest();
                return activeXOld;
            } catch (e) {
                throw e;
            }
        }
    }
})();

这样只需调用XHRFactory.createXMLHttpRequest()就可以获取特定浏览器环境的XML对象。这里并不是说用分支创建的XHR方式更好,而是强调怎么使用分支技术来实现按需加载。

7. 工厂模式(Factory)

如果一个类中有成员属性是另一个类的实例,常规操作是用new关键字类构造方法创建实例,这样一来,就会导致两个类产生依赖性,即耦合度,为了降低耦合度,可以考虑使用工厂模式

7.1 初探工厂模式

比如超市提供各色商品,顾客去超市买食物:

var Market = function() {};
Market.prototype = {
    getFood: function(name) {
        var food;
        switch (name) {
            case 'milk':
                // 超市自己制造的食品
                food = new Milk();
                break;
            case 'bread':
                food = new Bread();
                break;
            default:
                food = null;
        }
        // food通过检查了吗?
        return food;
    },
};

function Milk(){};
function Bread(){};

new Market().getFood('milk');

很显然,超市不应该可以制造食物,而是提供食物,食物应该交由食品加工厂制造,而且还要确保制造的食物是健康的、可食用的,即校验合格的。

下面,将食品加工交给食品加工厂,并且要校验合格:

// 食物标准:健康、绿色、可食用
var Food = new Interface('Food', ['isHealth', 'isGreen', 'isEdible']);

// 食物加工厂
var FoodFactory = {
    createFood: function(name) {
        var food;
        switch (name) {
            case "milk":
                food = new Milk();
                break;
            case "bread":
                food = new Bread();
                break;
            default:
                food = null;
        }
        // 食物检测
        Interface.ensureImplements(food,Food);
        return food;
    }
};


function Milk(){};
// 实现标准
Milk.prototype = {
    isHealth: function(){},
    isGreen: function(){},
    isEdiable: function(){}
}

var Market = function() {};
Market.prototype = {
    getFood: function(name) {
        var food = FoodFactory.createFood(name);
        food.isHealth();
        food.isGreen();
        food.isEdible();
        // 现在可以放心出售了
        return food;
    }
};

这里使用接口定义了食物标准,并且在工厂类中检测是否实现了这些标准。Interface类的具体实现请看第2章-接口2.2.3。

这样,我们把食物制造工作从超市移交给食品加工厂了,完成了解耦操作。这是只是最简单的工厂模式,真正的工厂模式比这更复杂。

7.2 深入工厂模式

工厂是一个将其成员对象的实例化通过子类来实现。

继续用超市的例子来理解,现在超市盈利了,开办了自己的食品超市A专门自产自销食品,食品加工不再借由外面的加工厂了,而是自己的食品超市自己加工(自产自销)。

var Market = function(){};
Market.prototype = {
    getFood: function(name)
        // 这里将成员对象实例化交给抽象类
        var food = this.createFood(name);
        
        food.isHealth();
        food.isGreen();
        food.isEdible();
        
        return food;
    },
    // 这是一个抽象类,需要子类来实现它
    createFood: function(name){
        throw new Error('Unsupported operation on an abstract class.');
    }
}

上面 定义抽象类来实现成员实例化方法,但是抽象类是不能直接调用的,它需要通过子类继承才能被实例化。

var FoodMarketA = function() {};
// 实现继承
extend(FoodMarketA, Market);
// 实现抽象类方法
FoodMarketA.prototype.createFood = function(name) {
    var food;
    switch (name) {
        case "milk":
            food = new Milk();
            break;
        case "bread":
            food = new Bread();
            break;
        default:
            food = null;
    }
    // 食物检测
    Interface.ensureImplements(food, Food);
    return food;
};

// 这样顾客就可以到专门的食品超市购买食物
var foodMarketA = new FoodMarketA();
var milk = foodMarketA.getFood('milk');

FoodMarketA继承了Market,并且实现了抽象方法,Market就相当于一个的连锁超市总部,可以在全开连锁店,不仅可以有FoodMarketA,还可以扩张FoodMarketB等,只需要实现继承就可以了。

这里的继承extend方法具体实现请参考第4节-继承4.2。

7.3 工厂模式利弊

  • 利:弱化对象间的耦合,防止代码的重复。通过工厂模式,可以先创建一个抽象的父类,然后子类实现继承实现具体的方法,从而把成员对象的创建交由专门的类去创建。关键技术:接口继承,即制定标准和实现标准。
  • 弊:逻辑复杂,有可能使问题复杂化,代码不够清晰,阅读代码需要查接口以及继承关系。

8. 桥接模式(Bridge)

8.1 桥接模式概述

“将抽象与其实现隔离开来,以便二者独立变化。” —— GOF

在实现API的时候,桥接模式非常有用。再设计一个Javascript API的时候,可以与整个模式来弱化它与所有使用它的类和对象之间的耦合

在一个类中,你可能定义有许多获取方法(getter)、设置方法(setter)以及其他方法,无论是用来创建Web服务API还是普通的取值器(accessor)方法还是赋值器(mutator)方法,都借助桥接模式可以简洁代码。

8.2 用桥接模式联结多个类

在现实中通过媒介可以把多个不同的事物联结起来,例如:人用电脑打字,简单实现 如下:

var People = function(name,age){
    this.name = name;
    this.age = age;
};
People.prototype = {
    type: function(){
        console.log('type...');
    }
};

var Computer = function(brand) {
    this.brand = brand;
};

var Word = function(text){
    this.text = text;
};

var Print = function(name,age,brand,text){
    this.people = new People(name,age);
    this.computer = new Computer(brand);
    this.word = new Word(text);
    
    this.print = function(){
        console.log('一个名叫' + this.people.name + this.people.age + '岁的人,用' + this.computer.brand + '电脑,打印出了一段话:' + this.word.text);
    } 
}

// 
var p = new Print('zkk', 24, 'Apple', 'Hello World!');
p.print();

这里定义了三个类:PeopleComputerWord,通过桥接类Print将它们联系在一起。Print不用关注具体的人、电脑、词语是怎么产生的,只需调用即可。

8.3 接模式应用

比如手动实现一个最简单的forEach遍历数组:

Array.prototype.myForEach = function(fn) {
    for(var i=0,len=this.length;i

这里forEach不用关注回调函数fn的具体实现,这里只是调用一下。回调函数fn想怎么实现就怎么实现,是压根不影响forEach函数的。

还有比如,事件监听回调、借助特权函数访问私有数据的手段,等等都有用到桥接模式。

记住,桥接模式的核心在于将抽象与具体实现分离,完成解耦操作。

8.4 桥接模式利弊

  • 利:将抽象与实现分离,独立管理各个部分的逻辑,降低耦合,有利于代码的维护。
  • 弊:提高了系统的复杂度。

9. 组合模式(composite)

9.1 理解组合模式

组合模式又称部分-整体模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。掌握组合模式的重点是要理解清楚 “部分/整体” 还有 ”单个对象“ 与 "组合对象" 的含义。

组合模式主要有三个角色:

  • 抽象组件(Component):抽象类,主要定义了参与组合的对象的公共接口
  • 子对象(Leaf):组成组合对象的最基本对象
  • 组合对象(Composite):由子对象组合起来的复杂对象
JS进阶学习笔记-设计模式_第1张图片
组合模式结构与二叉树类似

9.2 组合模式应用

比如DOM操作中最常用的就是给一个元素添加或删除一个样式:

var div = document.getElementById('div');
div.classList.add('red');
div.classList.remove('green');
// 再来一个
var p = document.getElementById('p');
p.classList.add('red')
p.classList.remove('green');

每次操作都要调用各种方法,很是繁琐,我想要一次操作大量的节点,就很是费力了。

var OperateClass = function(){
    this.domList = [];
};
OperateClass.prototype = {
    addDom: function(classNameOrId) {
        var dom = document.querySelector(classNameOrId);
        this.domList.push(dom);
    },
    removeClass: function(className) {
        for(var i=0,len=this.domList.length;i

通过定义一个组合对象,把每个Dom操作都统一起来,可以实现一次性对大量Dom进行添加或者删除className的操作。

总结一下,完整的模式如下:

// 1.抽象组件(Component)定义公共接口
var Component = new Interface('Component', ['func1', 'func2']);

// 2.组合对象(Composite)
var Composite = function(){
    this.leafList = [];
};
// 3.实现继承,并实现方法
extend(Composite, Component);
Composite.prototype = {
    add: function(leaf){
        this.leafList.push(leaf);
    },
    func1: function(){
        for(var i=0,len=this.leafList.length;i

这里定义了一个公共接口Component,所有的组件(CompositeLeaf)都继承了这个接口,然后把Leaf组件打包给Composite ,由Composite一次性完成对Leaf所有的操作。

上面代码中使用了两个函数interface和extend,具体实现请看第2节和第4节。

9.3 组合模式利弊

  • 利:在组合模式中,各个对象之间的耦合很松散,只要实现了相同的接口,相同的操作就可以很方便实现。

你可能感兴趣的:(JS进阶学习笔记-设计模式)