3
近期看了JavaScript设计模式一书,对书中重要概念部分和代码块进行了摘录,结合实际业务开发,对部分内容加上了自己的理解

面向对象编程

函数链式调用

函数链式调用是一种编程模式,其中多个函数调用可以链接在一起,每个函数的返回值都是另一个函数的输入。这种模式通常用于编写易读且具有清晰结构的代码。

在许多编程语言中,函数链式调用通常通过返回对象本身(通常是当前对象的引用)来实现。这样做可以使调用者在同一行上依次调用多个函数,并且代码结构更加紧凑和易读。

var checkObject = {
  checkName: function() {
    return this;
  },
  checkEmail: function() {
    return this;
  },
  checkPassword: function() {
    return this;
  }
}

checkObject.checkName().checkEmail().checkPassword();

习惯于类式调用方式,可以改成以下样子

var CheckObject = function() {};
CheckObject.prototype.checkName = function() {
  return this;
}
CheckObject.prototype.checkEmail = function() {
  return this;
}
CheckObject.prototype.checkPassword = function() {
  return this;
}

var a = new CheckObject();
a.checkName().checkEmail().checkPassword();

安全模式

先来看看以下例子

// --run--
var Book = function(title, time, type) {
  this.title = title;
  this.time = time;
  this.type = type;
}

var book = Book("JavaScript", "2014", "js");

console.log(book); // undefined

new 关键字的作用可以看作是对当前对象的 this 不停地赋值,然而例子中没有用 new,所以就会直接执行这个函数,而这个函数在全局作用域中执行了,所以在全局作用域中 this 指向的当前对象自然就是全局变量,在你的页面里全局变量就是 window 了,所以添加的属性自然就会被添加到 window 上面了,而我们这个 book 变量最终的作用是要得到 Book 这个类(函数)的执行结果,由于函数中没有 return 语句,这个 Book 类自然不会告诉 book 变量的执行结果了,所以就是 undefined(未定义)。

// --run--
var Book = function(title, time, type) {
  if (this instanceof Book) {
    this.title = title;
    this.time = time;
    this.type = type;
  } else {
    return new Book(title, time, type);
  }
}

var book = Book('JavaScript', '2014', 'js');
console.log(book); // {"title":"JavaScript","time":"2014","type":"js"}

继承

类式继承

类式继承需要将第一个类的实例赋值给第二个类的原型

// --run--
function SuperClass() {
  this.books = ["JavaScript", "html", "css"];
}
function SubClass() {}
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books);
instance1.books.push("设计模式");
console.log(instance2.books); // ["JavaScript", "html", "css", "设计模式"]

这种类式继承还有2个缺点。其一,由于子类通过其原型 prototype 对父类实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类;其二,由于子类实现的继承是靠其原型 prototype 对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。

构造函数式继承

// --run--
function SupClass() {
  this.books = ["JavaScript", "html", "css"];
}
SupClass.prototype.say = function() {
  console.log("Hello World");
}
function SubClass() {
  SupClass.call(this);
}
var instance1 = new SubClass();
var instance2 = new SubClass();
instance1.books.push("设计模式");
try {
  console.log(instance1.say());
} catch(error) {
  console.log(error.toString());
}
console.log(instance1.books);
console.log(instance2.books);

SuperClass.call(this, id);这条语句是构造函数式继承的精华,由于 call 这个方法可以更改函数的作用环境,因此在子类中,对 superClass 调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给 this 绑定属性的,因此子类自然也就继承了父类的共有属性。由于这种类型的继承没有涉及原型 prototype,所以父类的原型方法自然不会被子类继承,而如果要想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。

组合式继承

// --run--
function SupClass() {
  this.books = [];
}
SupClass.prototype.say = function() {
  console.log("Hello World");
}
function SubClass() {
  SupClass.call(this);
}
SubClass.prototype = new SupClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
instance1.books.push("设计模式");
console.log(instance1.say());
console.log(instance1.books);
console.log(instance2.books);

console.log(instance1 instanceof SupClass);

在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式,这样就融合了类式继承和构造函数继承的优点。

但在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类构造函数,因此父类构造函数调用了两遍。

原型式继承

原型式继承是一种通过利用已有对象作为新创建对象的原型来实现继承的方法。在 JavaScript 中,所有的对象都有一个指向原型对象的引用,可以通过这个原型对象实现继承。原型式继承通常用于创建一个新对象,该对象具有与原始对象相同的属性和方法,同时也可以添加新的属性和方法。最直接的方式可以通过 Object.create() 方法来实现。

// --run--
// 父对象
var parent = {
    name: "Parent",
    greet: function() {
        console.log("Hello, I'm " + this.name);
    }
};

// 使用 Object.create() 创建一个新对象,将 parent 作为其原型
var child = Object.create(parent);

// 对新对象进行扩展
child.name = "Child"; // 重写属性
child.sayHi = function() { // 添加新方法
    console.log("Hi, I'm " + this.name);
};

// 测试继承是否成功
child.greet(); // 输出:Hello, I'm Child
child.sayHi(); // 输出:Hi, I'm Child

没有 Object.create() 之前需要创建一个函数实现其功能,比如 createObject 函数接受一个对象作为参数,然后通过一个中间构造函数 F 来实现原型链的复制。最后,通过 new F() 创建一个新对象,该对象的原型链即为传入的对象。

// --run--
function createObject(obj) {
    // 通过一个中间空对象进行原型链的复制
    function F() {}
    F.prototype = obj;
    return new F();
}

// 示例:创建一个对象 person,它有一个属性 name
var person = {
    name: "John"
};

// 通过原型式继承创建一个新对象 student
var student1 = createObject(person);

student1.name = 'Tom'; // 这里并不是修改原型上的name属性,而是在实例上添加name属性

var student2 = createObject(person);
student2.name = 'Anne';

console.log(student1.name);
console.log(student2.name);

console.log(student1.__proto__.name);
console.log(student2.__proto__.name);

需要注意的是,原型式继承存在一些潜在的问题。如果原型对象上有引用类型的属性,那么它们会被多个实例共享,可能导致意外的修改。为了解决这个问题,可以考虑深拷贝原型对象,但这也可能会引入其他复杂性。

寄生式继承

寄生式继承就是对原型继承的第二次封装,并且在这第二次封装过程中对继承的对象进行了拓展,这样新创建的对象不仅仅有父类中的属性和方法而且还添加新的属性和方法。

// --run--
function createAnother(original) {
    // 通过调用 Object.create() 创建一个新对象,并继承自 original
    var clone = Object.create(original);

    // 向新对象添加额外的方法或属性
    clone.sayHi = function() {
        console.log("Hi there!");
    };

    // 返回新对象
    return clone;
}

// 示例:创建一个对象 person,它有一个属性 name
var person = {
    name: "John"
};

// 使用寄生式继承创建一个新对象 anotherPerson
var anotherPerson = createAnother(person);

// 测试新对象的属性和方法
console.log(anotherPerson.name);  // 输出: John
anotherPerson.sayHi();            // 输出: Hi there!

寄生式继承的优点在于可以在不必修改原始对象的情况下,为对象添加额外的行为或属性。但是,和原型式继承一样,寄生式继承也存在着对原始对象的引用共享问题,需要小心处理。通常情况下,寄生式继承被视为一种补充而不是主要的继承模式,在某些特定情况下可以发挥作用。

寄生组合式继承

JavaScript的寄生组合式继承结合了构造函数继承和寄生式继承的优点,是一种常用的继承模式,它能够避免构造函数多次调用和原型链上不必要的属性共享问题。

// --run--
function inheritPrototype(subType, superType) {
    // 创建父类原型的副本
    var prototype = Object.create(superType.prototype);

    // 修正副本的构造函数
    prototype.constructor = subType;

    // 将修正后的副本赋值给子类的原型
    subType.prototype = prototype;
}

// 父类
function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

// 父类的原型方法
SuperType.prototype.sayName = function() {
    console.log(this.name);
};

// 子类
function SubType(name, age) {
    // 继承父类的属性
    SuperType.call(this, name);
    this.age = age;
}

// 寄生式继承父类的原型方法
inheritPrototype(SubType, SuperType);

// 子类的原型方法
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

// 测试
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // 输出: ["red", "blue", "green", "black"]
instance1.sayName(); // 输出: Nicholas
instance1.sayAge(); // 输出: 29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // 输出: ["red", "blue", "green"]
instance2.sayName(); // 输出: Greg
instance2.sayAge(); // 输出: 27

多继承

在 JavaScript 中,原生并不直接支持多继承,即一个对象同时继承多个父对象的属性和方法。但是,可以通过一些技巧和模式来实现类似于多继承的效果。以下是几种常见的实现方式:

混入(Mixin)模式

在混入模式中,通过将一个或多个对象的属性和方法复制到另一个对象中来实现多继承的效果。这种方式常常在一些第三方库中见到。

// 定义多个对象
var parent1 = {
    method1: function() {
        console.log('Method 1 from Parent 1');
    }
};

var parent2 = {
    method2: function() {
        console.log('Method 2 from Parent 2');
    }
};

// 混入对象
function mixin(target, ...sources) {
    sources.forEach(source => {
        for (let key in source) {
            if (source.hasOwnProperty(key)) {
                target[key] = source[key];
            }
        }
    });
}

// 创建子对象
var child = {};
mixin(child, parent1, parent2);

// 测试多继承效果
child.method1(); // 输出:Method 1 from Parent 1
child.method2(); // 输出:Method 2 from Parent 2

使用 ES6 的类和 extends 关键字

在 ES6 中,可以使用类和 extends 关键字来实现类似于传统面向对象语言中的继承。

class Parent1 {
    method1() {
        console.log('Method 1 from Parent 1');
    }
}

class Parent2 {
    method2() {
        console.log('Method 2 from Parent 2');
    }
}

class Child extends Parent1 {
    constructor() {
        super();
    }
}

Object.assign(Child.prototype, Parent2.prototype); // 将 Parent2 的方法复制到 Child 中

// 创建子对象
var child = new Child();

// 测试多继承效果
child.method1(); // 输出:Method 1 from Parent 1
child.method2(); // 输出:Method 2 from Parent 2

多态

在 JavaScript 中,实现多态的方式不仅仅限于方法的重写,还可以根据传递不同的参数来实现不同的行为。这种情况下,同一个方法可以根据传入的参数类型或数量产生不同的行为。以下是一个简单的示例:

class Shape {
    draw() {
        console.log('Drawing a shape');
    }
}

class Circle extends Shape {
    draw() {
        console.log('Drawing a circle');
    }
}

class Rectangle extends Shape {
    draw() {
        console.log('Drawing a rectangle');
    }
}

function drawShape(shape) {
    shape.draw();
}

// 创建不同的图形对象
const shape = new Shape();
const circle = new Circle();
const rectangle = new Rectangle();

// 测试多态效果
drawShape(shape); // 输出:Drawing a shape
drawShape(circle); // 输出:Drawing a circle
drawShape(rectangle); // 输出:Drawing a rectangle

创建型设计模式

简单工厂模式

在 JavaScript 中,简单工厂方法通常是一个函数,根据传入的参数或条件来创建并返回不同类型的对象实例。

// --run--
// 定义一个简单工厂方法
function createCar(type) {
  if (type === "suv") {
    return new SUV();
  } else if (type === "sedan") {
    return new Sedan();
  } else {
    throw new Error("Invalid car type.");
  }
}

// 定义不同类型的车辆类
function Sedan() {
    this.type = 'sedan';
    this.doors = 4;
}

function SUV() {
    this.type = 'suv';
    this.doors = 5;
}

// 使用简单工厂方法创建不同类型的车辆
let sedan = createCar('sedan');
console.log(sedan); // 输出: Sedan { type: 'sedan', doors: 4 }

let suv = createCar('suv');
console.log(suv);   // 输出: SUV { type: 'suv', doors: 5 }

简单工厂方法的优点在于它将对象的创建逻辑封装在一个函数中,使得代码更加模块化和可维护。同时,它也有一些缺点,比如难以扩展,因为一旦需要添加新的类型,就需要修改工厂方法的逻辑

工厂方法模式

对于创建多类对象,简单工厂模式就不太适用了,这是简单工厂模式的应用局限,当然这正是工厂方法模式的价值之所在,通过工厂方法模式我们可以轻松创建多个类的实例对象,这样工厂方法对象在创建对象的方式也避免了使用者与对象类之间的耦合,用户不必关心创建该对象的具体类,只需调用工厂方法即可。

// --run--
var Factory = function(type) {
  if (this instanceof Factory) {
    return new this[type]();
  } else {
    return new Factory(type);
  }
}

Factory.prototype.SUV = function() {
  this.type = 'suv';
  this.doors = 5;
};
Factory.prototype.Sedan = function() {
  this.type = 'sedan';
  this.doors = 4;
};

let sedan = Factory('Sedan');
console.log(sedan); // 输出: Sedan { type: 'sedan', doors: 4 }

let suv = Factory('SUV');
console.log(suv);   // 输出: SUV { type: 'suv', doors: 5 } 

抽象工厂模式

// --run--
// 抽象工厂类
function AbstractFactory() {
  if (this.constructor === AbstractFactory) {
    throw new Error("Cannot instantiate abstract class AbstractFactory");
  }
}

AbstractFactory.prototype.createInstance = function() {
  throw new Error("Abstract method createInstance() must be overridden.");
}

// 具体工厂类
function ConcreteFactory(type) {}
ConcreteFactory.prototype = Object.create(AbstractFactory.prototype);
ConcreteFactory.prototype.constructor = ConcreteFactory;
ConcreteFactory.prototype.createInstance = function(type) {
  return new this[type]();
};

ConcreteFactory.prototype.SUV = function() {
  this.type = 'suv';
  this.doors = 5;
};
ConcreteFactory.prototype.Sedan = function() {
  this.type = 'sedan';
  this.doors = 4;
};

const factory = new ConcreteFactory();
const suv = factory.createInstance('SUV');
console.log(suv);
const sedan = factory.createInstance('Sedan');
console.log(sedan);

try {
  new AbstractFactory();
} catch(error) {
  console.log(error.toString());
}

抽象工厂模式是设计模式中最抽象的一种,也是创建模式中唯一一种抽象化创建模式。该模式创建出的结果不是一个真实的对象实例,而是一个类簇,它制定了类的结构,这也就区别于简单工厂模式创建单一对象,工厂方法模式创建多类对象。当然由于 JavaScript 中不支持抽象化创建与虚拟方法,所以导致这种模式不能像其他面向对象语言中应用得那么广泛。

抽象工厂模式和一般工厂模式区别

抽象工厂模式和一般工厂模式都是用于对象的创建,但它们之间有几个重要的区别:

  • 目的和复杂度:

    • 一般工厂模式:一般工厂模式旨在通过一个工厂方法,根据给定的参数或条件创建并返回不同类型的对象实例。它通常处理相对简单的对象创建需求。
    • 抽象工厂模式:抽象工厂模式更加复杂,它提供了一种用于创建一系列相关或依赖对象的接口,而无需指定它们具体的类。抽象工厂模式通常用于创建一组相关的产品对象,这些产品对象的实现可以彼此独立,但是它们之间的创建逻辑是相关的。
  • 使用场景:

    • 一般工厂模式:一般工厂模式适用于需要根据一些条件动态创建不同类型对象的情况。它适用于简单的对象创建需求,比如根据用户输入创建不同类型的按钮、对话框等。
    • 抽象工厂模式:抽象工厂模式适用于需要一次性创建一组相关或依赖对象的情况。它适用于复杂的对象创建需求,比如创建一组相互关联的 GUI 组件,或者创建一组相关的产品对象。
  • 层次结构:

    • 一般工厂模式:一般工厂模式通常只涉及单个工厂方法和一组相关的产品类。
    • 抽象工厂模式:抽象工厂模式涉及到多个工厂方法和多个产品族,每个工厂方法用于创建一个产品族的所有对象。

建造者模式

前面的几种工厂模式,他们都有一个共同特点,就是创建的结果都是一个完整的个体,我们对创建过程不得而知,我们只了解得到的创建结果对象。而在建造者模式中我们关心的是对象创建过程,因此我们通常将创建对象的类模块化,这样使被创建的类的每一个模块都可以得到灵活的运用与高质量的复用。当然我们最终的需求是要得到一个完整的个体,因此在拆分创建的整个过程,我们将得到一个统一的结果。

// --run--
var Human = function(skill, hobby) {
  this.skill = skill;
  this.hobby = hobby;
};

var Work = function(work, workDesc) {
  this.work = work;
  this.workDesc = workDesc;
};

Work.prototype.changeWork = function(work) {
  this.work = work;
};

var Person = function() {
  var human = new Human("保密", "保密");
  Work.call(human, "工程师", "沉迷于代码无法自拔");
  return human;
};

var person = new Person();
console.log(person); // {"skill":"保密","hobby":"保密","work":"工程师","workDesc":"沉迷于代码无法自拔"}

原型模式

原型模式就是将可复用的、可共享的、耗时大的从基类中提出来然后放在其原型中,然后子类通过组合继承或者寄生组合式继承而将方法和属性继承下来,对于子类中那些需要重写的方法进行重写,这样子类创建的对象既具有子类的属性和方法也共享了基类的原型方法。

// --run--
function LoopImages(imgs, container) {
  this.imgs = imgs;
  this.container = container;
}

// 创建轮播图
LoopImages.prototype.createImage = function() {};
// 切换图片
LoopImages.prototype.changeImage = function() {};

// 上下滑动切换类(组合继承)
function SlideLoopImg(imgs, container) {
  LoopImages.call(this, imgs, container);
}
SlideLoopImg.prototype = new LoopImages();
SlideLoopImg.prototype.changeImage = function() {
  console.log('SlideLoopImg changeImage function');
}

// 渐隐切换类(寄生组合)
function FadeLoopImage(imgs, container) {
  LoopImages.call(this, imgs, container);
}

FadeLoopImage.prototype = Object.create(LoopImages.prototype);
FadeLoopImage.prototype.constructor = FadeLoopImage;
FadeLoopImage.prototype.changeImage = function() {
  console.log('FadeLoopImage changeImage function');
}

// 使用
const slideLoopImg = new SlideLoopImg([], "SlideLoopImg");
slideLoopImg.changeImage();
const fadeLoopImage = new FadeLoopImage([], "FadeLoopImage");
fadeLoopImage.changeImage();

原型模式可以让多个对象分享同一个原型对象的属性与方法,这也是一种继承方式,不过这种继承的实现是不需要创建的,而是将原型对象分享给那些继承的对象。当然有时需要让每个继承对象独立拥有一份原型对象,此时我们就需要对原型对象进行复制。

由此我们可以看出,原型对象更适合在创建复杂的对象时,对于那些需求一直在变化而导致对象结构不停地改变时,将那些比较稳定的属性与方法共用而提取的继承的实现。

单例模式

单例模式(Singleton):又被称为单体模式,是只允许实例化一次的对象类。有时我们也用一个对象来规划一个命名空间,井井有条地管理对象上的属性与方法。

// --run--
var utils = {
  ajax: {
    get: function() {},
    post: funtion() {}
  },
  string: {
    slice: function() {},
    join: function() {}
  }
}

无法修改的静态变量

// --run--
var Conf = (function() {
  // 私有变量外界无法访问
  var conf = {
    MAX_NUM: 100,
    MIN_NUM: 0,
    COUNT: 200
  };

  // 返回取值器方法
  return {
    get: function(name) {
      return conf[name] || null;
    }
  }
})();

console.log(Conf.get('MAX_NUM'));

创建实例

// --run--
var createBot = (function() {
  var instance = null;
  var Bot = function(name) {
    this.name = name;
  };
  Bot.prototype.introduce = function() {
    console.log('I am a bot, name is ' + this.name);
  };
  return function(name) {
    if (!instance) {
      instance = new Bot(name);
    }

    return instance;
  }
})();

createBot('Kennel').introduce();
createBot('Anne').introduce(); // 还是Kennel的实例

结构型设计模式

外观模式

外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简化的接口,用于与复杂系统或一组接口进行交互。外观模式隐藏了系统的复杂性,为客户端提供了一个更简单和统一的接口,使得客户端更容易使用系统。

在 JavaScript 中,外观模式经常被用于封装复杂的 API 或操作,提供一个简单的接口给客户端使用。

下面是一个简单的 JavaScript 外观模式示例:

// --run--
// 复杂的模块A
var ModuleA = {
    method1: function() {
        console.log('Method 1 of Module A');
    },
    method2: function() {
        console.log('Method 2 of Module A');
    }
};

// 复杂的模块B
var ModuleB = {
    method1: function() {
        console.log('Method 1 of Module B');
    },
    method2: function() {
        console.log('Method 2 of Module B');
    }
};

// 外观模块
var Facade = {
    // 简化客户端调用接口
    operation1: function() {
        ModuleA.method1();
        ModuleB.method1();
    },
    operation2: function() {
        ModuleA.method2();
        ModuleB.method2();
    }
};

// 客户端代码
Facade.operation1(); // 执行操作1
Facade.operation2(); // 执行操作2

统一不同浏览器环境绑定元素事件

// --run--
function addEvent(dom, type, fn) {
  if (dom.addEventListener) {
    dom.addEventListener(type, fn, false);
  } else if (dom.attachEvent) {
    dom.attachEvent('on' + type, fn);
  } else {
    dom['on' + type] = fn;
  }
}

当一个复杂的系统提供一系列复杂的接口方法时,为系统的管理方便会造成接口方法的使用极其复杂。这在团队的多人开发中,撰写成本是很大的。当然通过外观模式,对接口的二次封装隐藏其复杂性,并简化其使用是一种很不错的实践。当然这种实践增加了一些资源开销以及程序的复杂度,当然这种开销相对于使用成本来说有时也是可忽略的。

适配器模式

传统设计模式中,适配器模式往往是适配两个类接口不兼容的问题,然而在 JavaScript 中,适配器的应用范围更广,比如适配两个代码库,适配前后端数据,等等。

参数适配器

// --run--
function doSomething(params) {
  var default = {};
  for(let key in default) {
    default[key] = params[key] || default[key];
  }
  // 或者
  // extend(default, params);

  // ...
}

数据适配

// --run--
var ajaxAdapter = function(data) {
  return data.flat();
}
$.ajax({
  url: '',
  method: 'GET',
  success: function(response) {
    doSomething(ajaxAdapter(response));
  }
})

Axios针对不同环境适配请求

在 Axios 中,适配器模式用于处理不同平台或环境下的 HTTP 请求的发送和响应处理。Axios 通过适配器模式实现了对不同环境的适配,例如在浏览器中使用 XMLHttpRequest 对象发送请求,在 Node.js 环境中使用 http 模块发送请求。

Axios 的适配器模式可以让开发者在不同环境中保持相同的 API 调用方式,而无需关心底层的请求处理细节。

// --run--
function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolve, reject) {
    var transport = isHttps ? https : http;
    var req = transport.request();
    // ...
  })
}

function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var request = new XMLHttpRequest();
    // ...
  })
}

// 有了适配后,在不同环境下保持相同的 API 调用方式

适配器模式与外观模式的区别

适配器模式(Adapter Pattern):

  • 定义:适配器模式旨在解决接口不兼容的问题。它允许将一个类的接口转换成客户端所期望的另一个接口,使得原本由于接口不匹配而无法在一起工作的类可以一起工作。
  • 目的:适配器模式的目的是让原本不兼容的接口能够协同工作,它通常用于将现有接口转换为另一个接口,以满足客户端的需求,而无需修改现有代码。
  • 实现:适配器模式通常涉及一个适配器类,该类包装了要适配的对象,并实现了客户端所期望的接口。

外观模式(Facade Pattern):

  • 定义:外观模式旨在为复杂系统或一组接口提供一个简化的接口,以隐藏系统的复杂性,并为客户端提供更简单和统一的接口。
  • 目的:外观模式的目的是简化客户端与系统之间的交互,降低客户端与系统之间的耦合度,提高系统的易用性和可维护性。
  • 实现:外观模式通常涉及一个外观类,该类封装了系统中的一组复杂对象或接口,并提供一个简单的接口给客户端使用。

区别:

  • 应用场景:适配器模式主要用于解决接口不兼容的问题,而外观模式主要用于简化复杂系统的接口,提供一个统一的接口给客户端使用。
  • 封装程度:适配器模式主要关注将一个接口转换成另一个接口,而外观模式主要关注隐藏系统的复杂性,提供一个更简单的接口给客户端使用。
  • 涉及对象:适配器模式涉及两个接口之间的适配,而外观模式涉及多个对象或接口的封装。

代理模式

在 JavaScript 中,代理模式是一种结构型设计模式,它允许你创建一个代理对象来控制对另一个对象的访问。代理对象充当着另一个对象的接口,可以在访问另一个对象之前或之后执行一些额外的操作。

在 JavaScript 中,ES6 引入了 Proxy 对象,它提供了一种机制,可以在目标对象之前进行拦截和定制操作。通过使用 Proxy,我们可以创建一个代理对象来代替另一个对象,并在目标对象上执行操作之前或之后添加自定义逻辑。

// --run--
var handler = {
  get: function(obj, prop) {
    return prop in obj ? obj[prop] : 0;
  }
};

const p = new Proxy({
  a: 1,
}, handler);

console.log(p.a, p.b);

装饰器模式

装饰者模式(Decorator):在不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法)使原有对象可以满足用户的更复杂需求。

// --run--
var decorator = function(input, fn) {
  var $input = document.getElmentById(input);
  if (typeof $input.onclick === "function") {
    var oldClick = $input.onclick;
    $input.onclick = function() {
      // 原事件源回调函数
      oldClick();
      // 执行事件源新增回调函数
      fn();
    }
  } else {
    $input.onclick = fn;
  }
}

修饰器

ES6 中的修饰器(Decorator)是一种特殊的语法,可以用来修改类和类的属性。修饰器提供了一种在声明时修改类和类成员的能力,通常用于添加元数据、注入代码或实现 AOP(面向切面编程)等功能。

// --run--
// 类修饰器
function classDecorator(target) {
  // 对类进行修改或注入
  target.isDecorated = true;
}

@classDecorator
class MyClass {
  // 类的定义
}

// 方法修饰器
function methodDecorator(target, key, descriptor) {
  // 对方法进行修改或注入
}

class MyClass {
  @methodDecorator
  myMethod() {
    // 方法的定义
  }
}

// 属性修饰器
function propertyDecorator(target, key) {
  // 对属性进行修改或注入
}

class MyClass {
  @propertyDecorator
  myProperty = 'value';
}

高阶组件

在 React 中,高阶组件 (Higher Order Components, HOCs) 可以看作是一种装饰器模式的应用。高阶组件接受一个组件作为参数,并返回一个新的增强了功能的组件。

// --run--
// 高阶组件:添加日志功能的装饰器
function withLogger(WrappedComponent) {
    return class extends React.Component {
        componentDidMount() {
            console.log(`Component ${WrappedComponent.name} is mounted`);
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

// 原始组件
class MyComponent extends React.Component {
    render() {
        return <div>Hello, world!</div>;
    }
}

// 使用高阶组件增强原始组件
const EnhancedComponent = withLogger(MyComponent);

// 渲染增强后的组件
ReactDOM.render(<EnhancedComponent />, document.getElementById('root'));

装饰器模式与适配器模式的区别

装饰器模式(Decorator Pattern)和适配器模式(Adapter Pattern)是两种不同的设计模式,它们解决了不同的问题,具有不同的应用场景和实现方式。

  1. 装饰器模式(Decorator Pattern)

    • 装饰器模式用于动态地给对象添加额外的功能,而不需要改变其接口或结构。
    • 装饰者对对象的拓展是一种良性拓展,不用了解其具体实现,只是在外部进行了一次封装拓展,这又是对原有功能完整性的一种保护。
  2. 适配器模式(Adapter Pattern)

    • 适配器模式用于解决两个接口不兼容的问题,它允许将一个接口转换成另一个客户端期望的接口。
    • 适配器进行拓展很多时候是对对象内部结构的重组,因此了解其自身结构是必需的。

总的来说,装饰器模式主要用于动态地添加额外的功能,而适配器模式主要用于解决接口不兼容的问题。虽然它们都涉及包装一个对象以改变其行为,但它们的目的和应用场景是不同的。

桥接模式

桥接模式(Bridge Pattern)是一种结构型设计模式,它用于将抽象部分与实现部分分离,从而使它们可以独立地变化。桥接模式通过将抽象和实现分离,使得它们可以独立地进行变化和扩展,同时也使得系统更加灵活和易于维护。

在桥接模式中,有两个独立变化的维度:抽象和实现。抽象部分定义了对象的接口,而实现部分提供了对象的具体实现。桥接模式通过一个桥接类将抽象部分和实现部分连接起来。

以下是一个简单的 JavaScript 桥接模式的示例:

// 实现部分接口
class DrawingAPI {
    drawCircle(x, y, radius) {
        console.log(`Drawing circle at (${x}, ${y}) with radius ${radius}`);
    }
}

// 抽象部分
class Shape {
    constructor(drawingAPI) {
        this.drawingAPI = drawingAPI;
    }

    draw() {}
}

// 具体的抽象实现
class Circle extends Shape {
    constructor(x, y, radius, drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    draw() {
        this.drawingAPI.drawCircle(this.x, this.y, this.radius);
    }
}

// 使用桥接模式创建并绘制圆
const circle = new Circle(1, 2, 3, new DrawingAPI());
circle.draw(); // 输出: Drawing circle at (1, 2) with radius 3

在这个示例中,Shape 类代表抽象部分,它定义了对象的接口。DrawingAPI 类代表实现部分,它提供了对象的具体实现。Circle 类是具体的抽象实现,它将抽象部分和实现部分连接起来,并实现了抽象部分的接口。

桥接模式使得抽象部分和实现部分可以独立地进行扩展和变化。例如,如果我们需要添加新的形状或者修改绘图 API,我们可以分别扩展 Shape 类和 DrawingAPI 类,而不会影响到彼此。这使得系统更加灵活和易于维护。

桥接模式与建造者模式区别

建造者模式(Builder Pattern)和桥接模式(Bridge Pattern)是两种不同的设计模式,它们解决了不同的问题,并且在应用场景和实现方式上也有所不同。

  1. 建造者模式(Builder Pattern)

    • 建造者模式用于创建一个复杂对象,将对象的构建过程和表示分离开来,使得相同的构建过程可以创建不同的表示。
    • 建造者模式的典型应用是在创建一个包含多个部分的复杂对象时,可以将对象的构建过程拆分成多个步骤,每个步骤由一个建造者负责,然后由指挥者来组装这些部分。
  2. 桥接模式(Bridge Pattern)

    • 桥接模式用于将抽象部分和实现部分分离开来,使得它们可以独立地变化和扩展。
    • 桥接模式通常包含一个抽象部分和一个实现部分,抽象部分定义对象的接口,而实现部分提供对象的具体实现。桥接模式通过一个桥接类将抽象部分和实现部分连接起来。

总的来说,建造者模式主要用于创建一个复杂对象,并将对象的构建过程和表示分离开来,而桥接模式主要用于将抽象部分和实现部分分离开来,使得它们可以独立地变化和扩展。它们解决了不同的问题,并且在应用场景和实现方式上也有所不同。

组合模式

组合模式是一种结构型设计模式,它允许你将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端对单个对象和组合对象的处理方式保持一致。

在组合模式中,有两种类型的对象:

  • 叶子对象(Leaf): 这些是树结构中的最小单位,它们没有子对象,代表树的最底层节点。
  • 复合对象(Composite): 这些对象包含叶子对象或其他复合对象,代表树中的分支节点和根节点。

组合模式的核心思想是让客户端对于单个对象和组合对象的处理方式保持一致。这意味着客户端可以像处理单个对象一样来处理组合对象,而不需要关心它是单个对象还是组合对象。

// --run--
var News = function() {
  this.childrens = [];
  this.element = '';
}

News.prototype = {
  init: function() {
    throw new Error("请重写你的方法");
  },
  add: function() {
    throw new Error("请重写你的方法");
  },
  getElement: function() {
    throw new Error("请重写你的方法");
  }
};

// 容器类
var Container = function(id, parent, className) {
  News.call(this);
  this.id = id;
  this.parent = parent;
  this.init(className);
}
Container.prototype = Object.create(News.prototype);
Container.prototype.init = function(className) {
    this.element = document.createElement("div");
    this.element.id = this.id;
    this.element.className = className;
};
Container.prototype.add = function(child) {
    this.childrens.push(child);
    this.element.appendChild(this.getElement());
    return this;
};
Container.prototype.getElement = function() {
    return this.element;
};
Container.prototype.show = function() {
    this.parent.appendChild(this.element);
};

// 纯文本
var Text = function(id, text, className) {
  News.call(this);
  this.id = id;
  this.init(text, className);
};

Text.prototype = Object.create(News.prototype);
Text.prototype.init = function(text, className) {
  this.element = document.createElement("span");
  this.element.innerHTML = text;
  this.element.id = this.id;
  this.element.className = className;
};
Text.prototype.add = function() {};
Container.prototype.getElement = function() {
    return this.element;
};

// 图片
var Img = function(id, url, className) {
  News.call(this);
  this.id = id;
  this.init(url, className);
};

Img.prototype.init = function(url, className) {
  this.element = document.createElement("img");
  this.element.src = url;
  this.element.id = id;
  this.element.className = className;
};
Img.prototype.add = function() {};
Img.prototype.getElement = function() {
    return this.element;
};

// 使用
new Container('News', document.body, 'container')
  .add(new Text('Text', '我是文本', 'text-item'))
  .add(new Img('Img', 'https://xxxx.png', 'img-item'))

享元模式

享元模式是一种结构型设计模式,它旨在通过共享对象来最大限度地减少内存使用和提高性能。在享元模式中,对象被分为两种部分:内部状态(Intrinsic State)和外部状态(Extrinsic State)。

  • 内部状态 是对象可以共享的信息,它存储在享元对象内部,并且通常不会随着对象的上下文而改变。
  • 外部状态 是对象的上下文信息,它取决于对象被使用的场合,可以随着对象的上下文而改变。
    享元模式的核心思想是将内部状态和外部状态分离开来,使得相同的内部状态的对象可以被多个上下文共享,从而节省内存空间。

以下是享元模式的一些关键点:

  • 对象共享: 享元模式通过共享对象来减少内存占用。当多个对象需要相同的内部状态时,它们可以共享同一个享元对象。
  • 状态分离: 内部状态和外部状态被分离,内部状态存储在享元对象内部,外部状态由客户端管理。这使得享元对象可以被多个上下文共享。
  • 适用场景: 享元模式适用于存在大量相似对象,且内部状态相对固定而外部状态变化较大的情况。

行为型设计模式

观察者模式

观察者模式(Observer):又被称作发布-订阅者模式或消息机制,定义了一种依赖关系,解决了主体对象与观察者之间功能的耦合。

// --run--
var mediator = {
  _events: [],
  eachEvent(events, callback, iterator) {
    // 不支持对象,只支持多个event用空格隔开
    const separator = /\s+/;
    (events || "").split(separator).forEach((key) => {
      iterator(key, callback);
    });
  },
 
  on(name, callback, context) {
    if (!callback) return this;
 
    const set = this._events || (this._events = []);
 
    this.eachEvent(name, callback, (name, callback) => {
      const handler = { e: name };
      handler.cb = callback;
      handler.ctx = context;
      handler.ctx2 = context || this;
      handler.id = set.length;
 
      set.push(handler);
    });
  },
 
  triggerHanders(events, args) {
    let stoped = false;
    const len = events.length;
    let i = -1;
    while (++i < len) {
      const handler = events[i];
 
      if (handler.cb.apply(handler.ctx2, args) === false) {
        stoped = true;
        break;
      }
    }
 
    return !stoped;
  },
 
  // 根据条件过滤出事件handlers.
  findHandlers(arr, name, callback, context) {
    return arr.filter((handler) => {
      return (
        handler &&
        (!name || handler.e === name) &&
        (!callback || handler.cb === callback || handler.cb._cb === callback) &&
        (!context || handler.ctx === context)
      );
    });
  },
 
  trigger(type, ...args) {
    if (!this._events || !type) return this;
 
    const events = this.findHandlers(this._events, type);
    const allEvents = this.findHandlers(this._events, "all");
 
    return (
      this.triggerHanders(events, args) &&
      this.triggerHanders(allEvents, [type, ...args])
    );
  }
};

mediator.on('onCompleted', () => {console.log('completed');});

setTimeout(() => {
  mediator.trigger('onCompleted');
}, 3000);

状态模式

状态模式(State):当一个对象的内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对象。

状态模式既是解决程序中臃肿的分支判断语句问题,将每个分支转化为一种状态独立出来,方便每种状态的管理又不至于每次执行时遍历所有分支。在程序中到底产出哪种行为结果,决定于选择哪种状态,而选择何种状态又是在程序运行时决定的。当然状态模式最终的目的即是简化分支判断流程。

// --run--
var MarryState = function() {
  var currentState = {};
  var states = {
    jump: function() {
      console.log('jump');
    },
    move: function() {
      console.log('move');
    },
    shoot: function() {
      console.log('shoot');
    },
    squat: function() {
      console.log('squat');
    }
  };

  var actions = {
    // 改变状态
    changeState: function() {
      var args = [].slice.call(arguments);
      currentState = {}; // 重置
      for(var i = 0; i < args.length; i++) {
        currentState[args[i]] = true;
      }

      return this;
    },
    // 执行动作
    goes: function() {
      for(var key in currentState) {
        currentState[key] && states[key]();
      }
      return this;
    }
  };

  return {
    change: actions.changeState,
    goes: actions.goes
  }
}

var marry = new MarryState();
marry.change("jump", "move").goes();

策略模式

策略模式(Strategy):将定义的一组算法封装起来,使其相互之间可以替换。封装的算法具有一定独立性,不会随客户端变化而变化。

策略模式最主要的特色是创建一系列策略算法,每组算法处理的业务都是相同的,只是处理的过程或者处理的结果不一样,所以它们又是可以相互替换的,这样就解决了算法与使用者之间的耦合。在测试层面上讲,由于每组算法相互之间的独立性,该模式更方便于对每组算法进行单元测试,保证算法的质量。

对于策略模式的优点可以归纳为3点,第一,策略模式封装了一组代码簇,并且封装的代码相互之间独立,便于对算法的重复引用,提高了算法的复用率。第二,策略模式与继承相比,在类的继承中继承的方法是被封装在类中,因此当需求很多算法时,就不得不创建出多种类,这样会导致算法与算法的使用者耦合在一起,不利于算法的独立演化,并且在类的外部改变类的算法难度也是极大的。第三,同状态模式一样,策略模式也是一种优化分支判断语句的模式,采用策略模式对算法封装使得算法更利于维护。

当然策略模式也有其自身的缺点。由于选择哪种算法的决定权在用户,所以对用户来说就必须了解每种算法的实现。这就增加了用户对策略对象的使用成本。其次,由于每种算法间相互独立,这样对于一些复杂的算法处理相同逻辑的部分无法实现共享,这就会造成一些资源的浪费。

// --run--

// 使用 gemini-pro 模型
class Gemini {
  constructor() {}

  explain(prompt) {
    return 'Gemini model';
  }
}

// 千问大模型
class QWenBot {
  constructor() {}

  explain(prompt) {
    return 'QWenBot model';
  }
}

class Bot {
  constructor(strategy) {
    this.strategy = strategy;
  }

  answer(prompt) {
    return this.strategy.explain(prompt);
  }
}

const replay = new Bot(new QWenBot()).answer('who are you');
console.log(replay);

状态模式与策略模式的区别

状态模式(State Pattern)和策略模式(Strategy Pattern)是两种不同的行为设计模式,它们各自解决了不同类型的问题,并且有不同的应用场景和实现方式。

状态模式(State Pattern)

  • 目的: 状态模式主要用于解决对象在不同状态下行为不同的问题,使得对象在内部状态发生变化时能够改变它的行为,看起来就像是对象在运行时修改了自己的类一样。
  • 结构: 包括上下文(Context)、状态(State)和具体状态(Concrete State)等角色。上下文持有一个状态对象,根据状态对象的不同,执行不同的行为。
  • 示例: 例如,在一个订单管理系统中,订单可能有不同的状态(待付款、已付款、已发货等),针对不同的状态,订单的行为(如取消订单、确认订单等)也会有所不同。

策略模式(Strategy Pattern)

  • 目的: 策略模式用于定义一系列算法,并将每个算法封装成一个对象,使得它们可以相互替换。策略模式使得算法可以独立于使用它的客户而变化。
  • 结构: 包括策略接口(Strategy)、具体策略(Concrete Strategy)、上下文(Context)等角色。上下文对象持有一个策略对象,并在运行时根据需求选择不同的策略对象执行算法。
  • 示例: 例如,在一个电商系统中,针对不同的支付方式(支付宝、微信支付、信用卡支付等),可以定义不同的支付策略,并在订单结算时选择合适的支付策略进行支付。

区别

  • 目的不同: 状态模式主要用于解决对象在不同状态下行为不同的问题,而策略模式用于定义一系列算法并使得它们可以相互替换。
  • 关注点不同: 状态模式关注对象内部状态的变化,而策略模式关注不同的算法之间的替换和切换。
  • 结构不同: 状态模式中的行为会随着状态的改变而改变,而策略模式中的行为由客户端选择的策略对象决定。

虽然状态模式和策略模式在某些情况下可能会有一定的相似之处,但它们的关注点和解决的问题是不同的,因此在设计时需要根据具体的场景和需求选择合适的模式。

责任链模式

职责链模式(Chain of Responsibility):解决请求的发送者与请求的接受者之间的耦合,通过职责链上的多个对象对分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理。

职责链模式定义了请求的传递方向,通过多个对象对请求的传递,实现一个复杂的逻辑操作。因此职责链模式将负责的需求颗粒化逐一实现每个对象分内的需求,并将请求顺序地传递。对于职责链上的每一个对象来说,它都可能是请求的发起者也可能是请求的接收者。通过这样的方式不仅仅简化原对象的复杂度,而且解决原请求的发起者与原请求的接收者之间的耦合。当然也方便对每个阶段对象进行单元测试。同时对于中途插入的请求,此模式依然使用,并可顺利对请求执行并产出结果。

// --run--
// 抽象处理者
class Handler {
  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handleRequest(days) {
    if (this.nextHandler) {
      return this.nextHandler.handleRequest(days);
    }
    return 'No one can handle your request';
  }
}

// 具体处理者
class Manager extends Handler {
  handleRequest(days) {
    if (days <= 2) {
      return `Manager approved your leave for ${days} days`;
    } else {
      return super.handleRequest(days);
    }
  }
}

class Director extends Handler {
  handleRequest(days) {
    if (days <= 5) {
      return `Director approved your leave for ${days} days`;
    } else {
      return super.handleRequest(days);
    }
  }
}

class President extends Handler {
  handleRequest(days) {
    return `President approved your leave for ${days} days`;
  }
}

// 客户端
const manager = new Manager();
const director = new Director();
const president = new President();

manager.setNext(director).setNext(president);

console.log(manager.handleRequest(1)); // Manager approved your leave for 1 days
console.log(manager.handleRequest(4)); // Director approved your leave for 4 days
console.log(manager.handleRequest(10)); // President approved your leave for 10 days

链式校验

在实际业务中,常常需要确保提交单据时经过一系列规则校验才能顺利提交成功。这种情况下,我们可以将校验规则构建成责任链的节点,每个节点负责检查一个特定的校验条件。如果某个节点验证通过,则将请求传递给责任链中的下一个节点进行进一步的校验。这种设计模式能够帮助我们更灵活地管理和扩展校验逻辑,使得代码结构更加清晰和易于维护。

// --run--
// 校验器基类
class Validator {
  constructor() {
    this.nextValidator = null; // 下一个校验器
  }

  setNext(validator) {
    this.nextValidator = validator;
    return validator;
  }

  validate(data) {
    // 校验逻辑的实现
    throw new Error("validate()方法一定要被实现");
  }
}

// 具体校验器类
class FirstValidator extends Validator {
  validate(data) {
    // 第一种校验逻辑的实现
    const isValid = /* 校验逻辑 */;
    if (isValid) {
      return true;
    } else if (this.nextValidator) {
      return this.nextValidator.validate(data); // 如果有下一个校验器,则交给下一个校验器处理
    } else {
      return false; // 所有校验器都处理完毕,返回最终结果
    }
  }
}

class SecondValidator extends Validator {
  validate(data) {
    // 第二种校验逻辑的实现
    const isValid = /* 校验逻辑 */;
    if (isValid) {
      return true;
    } else if (this.nextValidator) {
      return this.nextValidator.validate(data);
    } else {
      return false;
    }
  }
}

class ThirdValidator extends Validator {
  validate(data) {
    // 第三种校验逻辑的实现
    const isValid = /* 校验逻辑 */;
    if (isValid) {
      return true;
    } else if (this.nextValidator) {
      return this.nextValidator.validate(data);
    } else {
      return false;
    }
  }
}
 
// 创建校验器链
const firstValidator = new FirstValidator();
const secondValidator = new SecondValidator();
const thirdValidator = new ThirdValidator();

firstValidator.setNext(secondValidator).setNext(thirdValidator);

// 执行校验
const data = /* 待校验的数据 */;
const isValid = firstValidator.validate(data);
if (isValid) {
  console.log("数据通过校验");
} else {
  console.log("数据未通过校验");
}

Promise 链式调用

Promise链式调用与责任链模式有一些相似之处,但它们并不完全相同。

Promise链式调用通常用于处理异步操作,特别是在 JavaScript 中。通过Promise,可以按顺序执行一系列异步任务,并在每个任务完成后执行下一个任务。在这个过程中,每个Promise的处理器都可以决定是否将控制权传递给下一个Promise。

责任链模式则更多地用于处理同步操作,将请求的处理者连接成一条链,并按顺序传递请求,直到有一个处理者处理该请求为止。责任链模式的重点在于将请求传递给下一个处理者,直到找到合适的处理者为止。

// --run--
// 第一个校验
function firstValidation(data) {
  return new Promise((resolve, reject) => {
    // 第一个校验逻辑的实现
    const isValid = /* 校验逻辑 */;
    if (isValid) {
      resolve();
    } else {
      reject("第一个校验未通过");
    }
  });
}
 
// 第二个校验
function secondValidation(data) {
  return new Promise((resolve, reject) => {
    // 第二个校验逻辑的实现
    const isValid = /* 校验逻辑 */;
    if (isValid) {
      resolve();
    } else {
      reject("第二个校验未通过");
    }
  });
}
 
// 执行校验
const data = /* 待校验的数据 */;
 
Promise.resolve()
  .then(() => firstValidation(data))
  .then(() => secondValidation(data))
  .then(() => {
    console.log("数据通过校验");
  })
  .catch((error) => {
    console.log("数据未通过校验:", error);
  });

迭代器模式

通过选代器我们可以顺序地访问一个聚合对象中的每一个元素。在开发中,迭代器极大简化了代码中的循环语句,使代码结构清晰紧凑,然而这些简化了的循环语句实质上隐形地移到了迭代器中。当然用迭代器去处理一个对象时,我们只需提供处理的方法,而不必去关心对象的内部结构,这也解决了对象的使用者与对象内部结构之间的耦合。当然迭代器的存在也为我们提供了操作对象的一个统一接口。

// --run--
// 迭代器
class Iterator {
    constructor(collection) {
        this.collection = collection;
        this.index = 0;
    }

    hasNext() {
        return this.index < this.collection.length;
    }

    next() {
        return this.hasNext() ? this.collection[this.index++] : null;
    }
}

// 聚合器
class Aggregate {
    constructor() {
        this.collection = [];
    }

    addItem(item) {
        this.collection.push(item);
    }

    createIterator() {
        return new Iterator(this.collection);
    }
}

// 使用示例
const aggregate = new Aggregate();
aggregate.addItem('Item 1');
aggregate.addItem('Item 2');
aggregate.addItem('Item 3');

const iterator = aggregate.createIterator();
while (iterator.hasNext()) {
    console.log(iterator.next());
}

解释器模式

解释器即是对客户提出的需求,经过解析而形成的一个抽象解释程序。而是否可以应用解释器模式的一条重要准侧是能否根据需求解析出一套完成的语法规则,不论该语法规则简单或是复杂都是必须的。因为解释器要按照这套规则才能实现相应的功能。

以下是统计页面中点击事件触发元素在页面中所处的路径

var interpreter = (function() {
  function getSublingName(node) {
    if(node.previousSibling) {
      var name = "",
        count = 1,
        nodeName = node.nodeName, 
        sibling = node.previousSibling;
      while(sibling){
        if(sibling.nodeType == 1 && sibling.nodeType === node.nodeType && sibling.nodeName) {
          if(nodeName == sibling.nodeName) {
            name += ++count;
          } else {
            count = 1;
            name += '|' + sibling.nodeName.toUpperCase();
          }
        }
        sibling = sibling.previousSibling;
      }
      return name;
    } else {
      return '';
    }
  }
  return function(node, wrap) {
    var path = [],
      wrap = wrap || document;
    if (node === wrap) {
      if (wrap.nodeType === 1) {
        path.push(wrap.nodeName.toUpperCase());
      }
      return path;
    }
    if (node.parentNode !== wrap) {
      path = arguments.callee(node.parentNode, wrap);
    } else {
      if (wrap.nodeType === 1) {
        path.push(wrap.nodeName.toUpperCase());
      }
    }
    var sublingsNames = getSublingName(node);
    if (node.nodeType === 1) {
      path.push(node.nodeName.toUpperCase() + sublingsNames);
    }
    return path;
  }
})();

技巧型设计模式

链模式

链模式(Operate of Responsibility):通过在对象方法中将当前对象返回,实现对同一个对象多个方法的链式调用。从而简化对该对象的多个方法的多次调用时,对该对象的多次引用。

jQuery 是一个高端而不失奢华的框架。奇特的设计思想使其通过链式调用而显得更加简洁。不过这种链模式是基于原型继承的,并且在每一个原型方法的实现上都返回当前对象 this,使当前对象一直处于原型链作用域的顶端。这样即可实现链式调用。

// --run--
var A = function() {};
A.prototype = {
  length: 2,
  size: function() {
    return this.length;
  }
}

// 只能以下执行访问
// var a = new A();
// console.log(a.size); 

// 如果这么访问就会报错
// A().size()
// A.size()

// 回想一下JQuery是怎么做到的呢?$()函数在执行后会返回一个带有很多方法的对象
var A = function() {
  return B;
}
B = A.prototype = {
  length: 2,
  size: function() {
    return this.length;
  }
}
// 这样就可以通过A().size()访问了

// 在JQuery中,为了减少变量的创建,索性将B看作A的一个属性设置
var A = function() {
  return A.fn;
}
A.fn = A.prototype = {
  length: 2,
  size: function() {
    return this.length;
  }
}

// 到这里又有新的问题,我们知道JQuery的目的是获取元素,返回的是一个元素簇,而显示返回的是一个A.fn对象,可以在A.fn中定义一个获取元素的方法
var A = function(selector) {
  return A.fn.init(selector);
}
A.fn = A.prototype = {
  init: function(selector) {
    return document.getElementById(selector);
  },
  length: 2,
  size: function() {
    return this.length;
  }
}

// 虽然现在已经返回了元素簇对象,但是其没有拥有原型上的方法了,我们可以将返回的元素簇对象放置在当前对象this上
var A = function(selector) {
  return A.fn.init(selector);
}
A.fn = A.prototype = {
  init: function(selector) {
    this[0] = document.getElementById(selector);
    this.length = 1;
    return this;
  },
  length: 2,
  size: function() {
    return this.length;
  }
}

// 最后还有一个问题,A("test")和A("demo")会共享A.fn对象,length会根据执行顺序相互覆盖,想要创建具有独立空间的对象,new关键字可以帮助我们搞定
var A = function(selector) {
  return new A.fn.init(selector);
}
A.fn = A.prototype = {
  init: function(selector) {
    this[0] = document.getElementById(selector);
    this.length = 1;
    return this;
  },
  length: 2,
  size: function() {
    return this.length;
  }
}
A.fn.init.prototype = A.fn;

// 测试一下
A("PC_INQUIRY_BANNER") // init {0: div#PC_INQUIRY_BANNER.nivoSlider, length: 1}
$("#PC_INQUIRY_BANNER") // n.fn.init {0: div#PC_INQUIRY_BANNER.nivoSlider, length: 1, context: document, selector: '#PC_INQUIRY_BANNER'}

惰性模式

惰性模式是一种拖延模式,由于对象的创建或者数据的计算会花费高昂的代价(如页面刚加载时无法辨别是该浏览器支持某个功能,此时创建对象是不够安全的),因此页面之处会延迟对这一类对象的创建。惰性模式又分为两种:第一种是文件加载后立即执行对象方法来重定义对象。第二种是当第一次使用方法对象时重定义对象。对于第一种方式,由于文件加载时执行,因此会占用一些资源。对于第二种方式由于在第一次使用时重定义对象,以致第一次执行时间增加。有时候两种方式对资源的开销都是可接受的,因此到底使用哪种方式,要看具体需求而定。

// --run--
// 第一种,加载时损失性能,但是第一次调用时不损失性能
var createXHR = function(){
  if (typeof XMLHttpRequest !== "undefined") {
    return function() {
      return new XMLHttpRequest();
    }
  } else if (typeof ActiveXObject !== "undefined") {
    return function() {
      return new ActiveXObject("Microsoft.XMLHTTP");
    }
  } else {
    return function() {
      throw new Error("no XHR object avaliable.");
    }
  }
}()

// 第二种,加载时不损失性能,在第一次调用时损失性能
var createXHR = function() {
  if (typeof XMLHttpRequest !== "undefined") {
    createXHR = function() {
      return new XMLHttpRequest();
    }
  } else if (typeof ActiveXObject !== "undefined") {
    createXHR = function() {
      return new ActiveXObject("Microsoft.XMLHTTP");
    }
  } else {
    createXHR = function() {
      throw new Error("no XHR object avaliable.");
    }
  }
}

等待者

等待者模式意在处理耗时比较长的操作,比如 canvas 中遍历并操作一张大图片中的每一个像素点、定时器操作、异步请求等。等待者模式为我们提供了一个抽象的非阻塞的解决方案,通过创建 Promise 对象,对耗时逻辑的未来状态变化返回一个响应,通过在等待者对象内部捕获这些响应信息,为耗时较长的操作提供了回调方案,使我们可以捕获耗时操作完成时或中断时的状态并执行相应的回调方案。

// --run--
var Waiter = function() {
  var dfd = [], // 等待队列
    doneArr = [], // 成功回调函数队列
    failArr = []; // 失败回调函数队列
  var that = this;

  function MyPromise() {
    this.resolved = false;
    this.rejected = false;
  }

  MyPromise.prototype = {
    resolve: function() {
      this.resolved = true;
      if (!dfd.length) {
        return;
      }
      // 因为循环体内改变了dfd,所以不能使用for(var i = 0; i < dfd.length; i++)
      for(var i = dfd.length - 1; i >= 0; i--) {
        if (dfd[i] && !dfd[i].resolved || dfd[i].rejected) {
          return;
        }
        dfd.splice(i, 1);
      }
      _exec(doneArr);
    },
    reject: function() {
      this.rejected = true;
      if (!dfd.length) {
        return;
      }
      dfd.splice(0);
      _exec(failArr);
    }
  };

  function _exec(arr) {
    for(var i = 0; i < arr.length; i++) {
      try {
        arr[i] && arr[i]();
      } catch(error) {

      }
    }
  }

  that.Deferred = function() {
    return new MyPromise();
  }

  that.when = function() {
    dfd = Array.prototype.slice.call(arguments);
    for(var i = 0; i < dfd.length; i++) {
      if (!dfd[i] || dfd[i].resolved || dfd[i].rejected || !dfd[i] instanceof MyPromise) {
        dfd.splice(i, 1);
      }
    }
    return that;
  }

  that.done = function() {
    doneArr = doneArr.concat(Array.prototype.slice.call(arguments));
    return that;
  }

  that.fail = function() {
    failArr = failArr.concat(Array.prototype.slice.call(arguments));
    return that;
  }
}

const waiter = new Waiter();
const first = (function() {
    const deferred = waiter.Deferred();
    setTimeout(() => {console.log(1);deferred.resolve()}, 2000)

    return deferred;
})();
const second = (function() {
    const deferred = waiter.Deferred();
    setTimeout(() => {console.log(2);deferred.resolve()}, 6000)

    return deferred;
})();

waiter.when(first, second).done(function() {console.log('success')});

架构设计型模式

同步模块模式

同步模块模式(Synchronous Module Definition,简称为SMD)是一种用于在浏览器端组织和加载JavaScript模块的模式。与AMD(Asynchronous Module Definition)和CommonJS模块系统相比,SMD 是一种同步加载模块的方式。

在SMD中,模块之间的依赖关系是在模块声明时定义的,模块的加载是同步的,因此模块的定义和依赖关系会在运行时立即解析和加载,这也意味着在加载模块时,后续的代码会等待模块加载完成后再执行。

首先,定义一个模块管理器对象 F,然后为其创建一个模块定义方法 define。

// --run--
var F = F || {};

F.define = function(str, fn) {
  var parts = str.split(".");
  var old = parent = this;
  if (parts[0] === "F") {
    parts = parts.slice(1);
  }

  if (parts[0] === "define" || parts[0] === "module") {
    return;
  }

  for(var i = 0; i < parts.length; i++) {
    if (typeof parent[parts[i]] === "undefined") {
      parent[parts[i]] = {};
    }
    old = parent;
    parent = parent[parts[i]];
  }

  if (fn) {
    old[parts[--i]] = fn();
  }
};

F.define('string', function() {
  return {
    trim: function(str) {
      return str.replace(/^\s+|\S+$/g, '');
    }
  }
});

F.string.trim(' 123 456 ');

有了模块创建方法,现在还需要一个模块调用方法

// --run--
F.module = function() {
  var args = [].slice.call(arguments);
  var fn = args.pop();
  var parts = args[0] && args[0] instanceof Array ? args[0] : args;
  var modules = [];

  for(var i = 0; i < parts.length; i++) {
    var parent = this;
    if (typeof parts[i] === "string") {
      var mIds = parts[i].replace(/^F\./, '').split('.');
      
      for(var j = 0; j < mIds.length; j++) {
        parent = parent[mIds[j]] || false;
      }

      modules.push(parent);
    } else {
      // F.module([$], function() {}) 依赖不是字符串,直接入队
      modules.push(parts[i]);
    }
  }

  // 执行回调
  fn.apply(null, modules);
}

F.module(['dom', 'string.trim'], function(dom, trim) {
  // ...
})

异步模块模式

AMD(Asynchronous Module Definition)是一种用于在浏览器端组织和加载JavaScript模块的规范。AMD规范标准旨在解决JavaScript中模块加载的异步性问题,使得开发者可以更加方便地管理模块之间的依赖关系,而不需要手动管理加载顺序。

// --run--
var modules = {};

var getModule = function(id) {
  var module = modules[id];
  if (!module) {
    throw new Error( '`' + id + '` is undefined' );
  }

  return module;
};

var setModule = function(id, factory, args) {
  var module = {
    exports: factory
  }, returned;

  if (typeof factory === "function") {
    args.length || (args = [require, module.exports, module]); // 依赖项参数是可选的。如果省略,则应默认为 ["require", "exports", "module"]
    returned = factory.apply(null, args);
    returned !== undefined && (module.exports = returned);
  }

  modules[id] = module.exports; 
}

var define = function(id, deps, factory) {
  if (arguments.length === 2) {
    factory = deps;
    deps = null;
  }

  require(deps || [], function() {
    setModule(id, factory, arguments);
  })
};

var require = function(deps, callback) {
  if (typeof deps === "string") {
    return getModule(deps);
  } else {
    var args = [];
    for(var i = 0; i < deps.length; i++) {
      args.push(getModule(deps[i]));
    }
    return callback.apply(null, args);
  }
};

// 定义模块B
define('moduleB', [], function() {
    var name = 'Module B';
    function sayHi() {
        console.log('Hi from ' + name);
    }
    return {
        sayHi: sayHi
    };
});

// 定义模块A
define('moduleA', ['moduleB'], function(moduleB) {
    var name = 'Module A';
    function sayHello() {
        console.log('Hello from ' + name);
        moduleB.sayHi(); // 使用模块B中的函数
    }
    return {
        sayHello: sayHello
    };
});

// 加载模块A,并执行
require(['moduleA'], function(moduleA) {
    moduleA.sayHello();
});

MVC

MVC(Model-View-Controller)是一种软件架构模式,用于将应用程序分为三个主要组件:模型(Model)、视图(View)和控制器(Controller)。每个组件都有其特定的责任和作用,有助于组织和管理代码,降低耦合度,提高可维护性和可测试性。

// --run--
(function() {
  var MVC = MVC || {};
  MVC.model = (function() {})();
  MVC.view = (function() {})();
  MVC.controller = (function() {})();
})();

数据模型层内部的服务器端获取数据(data),为了能被视图层调用,我们要让数据模型对象返回操作这两类数据的接口方法。

// --run--
MVC.model = (function() {
  var data = {};
  return {
    getData: function(key) {
      return data[key];
    },
    setData: function(key, value) {
      data[key] = value;
      return this;
    }
  }
})();

根据模型数据层对象的设计思想我们可以创建出视图对象,当然视图对象保存着对应组件内部的视图,为了创建这些视图我们还要在视图对象内部引用模型数据对象以操作模型数据对象内部的数据,最后为了让控制器可操作视图层内的视图,我们则需要返回一些操作接
口方法。

// --run--
MVC.view = (function() {
  var M = MVC.model;
  var V = {
    createView: function() {
      var data = M.getData();
      // 使用数据创建视图
    }
  };
  return function(v) {
    V[v]();
  }
})();

控制器要做的事情其实很简单,第一创建视图页面,第二添加交互与动画特效。而在 MVC 的控制器中,由于创建视图的主要逻辑在视图层对象中,因此也就弱化了控制器中创建对象的功能,我们只需一行搞定(直接调用视图层对象接口方法渲染),而控制器对象也将自已主要的精力放在交互与特效上。

// --run--
MVC.controller = (function() {
  var M = MVC.model;
  var V = MVC.view;
  var C = {
    init: function() {
      V["createView"]();
      $(ele).on('click', function() {})
    }
  }
})();

MVP

MVP 即模型(Model)一视图(View)一管理器 (Presenter):View 层不直接引用Model层内的数据,而是通过 Presenter 层实现对Model 层内的数据访问。即所有层次的交互都发生在Presenter 层中。MVC模式开发中,视图层常常因渲染页面而直接引用数据层内的数据,对于发生的这一切,控制器常常不得而知。因此数据层内的数据修改,常常在控制器不知情的情况下影响到视图层的呈现

MVP 是在 MVC 模式中演变过来的,因此数据层不需要太多变化。但是视图层则不一样了,既然要与数据层解耦,并且可以独立创建视图模板,我们就要大刀阔斧地修改视图层了。首先我们创建一个 MVP 单体对象。

// --run--
(function() {
  var MVP = MVP || {};
  MVP.model = (function() {})();
  MVP.view = (function() {})();
  MVP.controller = (function() {})();
})();

相对于 MVC 中的数据层对象结构,在 MVP 模式中数据层对象的结构变化不大。不过,对于视图层就要改头换代了。

// --run--
MVP.view = (function() {
  var tpl = '<ul>{{each data as value}}<li>{{value}}</li>{{/each}}</ul/>';
  return {
    getTpl: function() {
      return tpl; // 返回视图模板
    }
  };
})();

有了模板引擎,我们在管理器(管理层 P)中实现就容易多了。为了使管理器更适合我们的 MVP 模式,我们对管理器稍加改动,添加管理器执行方法 init,这样方便在任何时候执行我们的管理器。但总的来说还是和控制器(MVC模式中的控制层)类似。

// --run--
MVC.presenter = (function() {
  var M = MVP.model;
  var V = MVP.view;
  var C = {
    createHTML: function(M, V) {
      var data = M.getData();
      var tpl = V.getTpl();
      // 编译模板
      var html = template(tpl, data);
      $(container).appendChild(html);
    }
  };

  return {
    init: function() {
      for(var i in C) {
        C[i] && C[i](M, V);
      }
    }
  }
})();

MVVM

MVVM 模式,模型(Model)-视图(View)-视图模型(ViewModel):为视图层(View)量身定做一套视图模型(ViewModel),并在视图模(ViewModel)中创建属性和方法,为视图层(View)绑定数据(Model)并实现交互。

在 MVP 中,实现何种需求(创建哪种页面)的主动权在管理器中,因此必须通过创建管理器实现需求。然而某些情况下,一些开发者对于复杂的 JavaScript 了解得不是很深入,操作管理器成本很大,他们能否直接通过 html 创建视图实现页面的需求呢?这种异想天开的想法可以实现吗?

每次实现页面某个组件需求时,总是要操作管理器或者控制器,然而有些时候,页面 UI 功能类似,为此创建的管理器或者控制器代码或多或少有些重复。MVP 模式给了我一些启发,既然我们可以将视图独立出来,那么我们可不可以通过创建视图反过来控制管理器实现组件需求呢?

创建视图即是创建页面内的视图,因此本质上就是在页面中书写 HTML 代码,因此如果将视图作用提升,通过在页面中直接书写 HTML 代码创建视图组件,让控制器或者管理器去监听这些视图组件,并处理这些组件完成预期功能,这种实现方式可以让那些只懂得 HTML 代码的开发者,轻松完成一些功能需求。这就是 MVVM 的核心思想。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MVVM Example</title>
</head>
<body>
    <div id="app">
        <input type="text" id="input" data-bind="value: inputValue">
        <button id="btn" data-bind="click: handleClick">Click me</button>
        <p data-bind="text: message"></p>
    </div>

    <script>
        // 定义 ViewModel
        function ViewModel() {
            this.inputValue = '';
            this.message = '';

            this.handleClick = function() {
                this.message = 'Hello, ' + this.inputValue + '!';
            }.bind(this);
        }

        // 数据绑定
        function bindData() {
            var elements = document.querySelectorAll('[data-bind]');
            elements.forEach(function(element) {
                var property = element.getAttribute('data-bind').split(':')[1].trim();
                var event = element.getAttribute('data-bind').split(':')[0].trim();
                if (event === 'value') {
                    element.value = viewModel[property];
                    element.addEventListener('input', function() {
                        viewModel[property] = element.value;
                    });
                } else if (event === 'click') {
                    element.addEventListener('click', function() {
                        viewModel[property]();
                    });
                } else if (event === 'text') {
                    element.innerText = viewModel[property];
                }
            });
        }

        var viewModel = new ViewModel();
        bindData();
    </script>
</body>
</html>

记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。