preface

The normal coder writes the code to run and implement the functions, while the Big Coder writes the code to consider performance, expansion, maintenance and compatibility. The design pattern is like a ladder between the two. The ordinary coder deeply understands the design pattern and applies and practices extensively, and can also become the Big coder step by step. When we receive the demand, do not immediately start, but to the bullet fly for a while ~, stop to think about how the design will be more suitable 🤔

Design patterns 👂 🏻 listen very tall, is in fact predecessors summary writing code routines, and as many as 20 sets, this paper mainly introduces JS eight kinds of commonly used language, some patterns may be used but we 😂 in name, so there may be a while reading “oh, this is xx design pattern 😯”. In order to deepen the readers’ understanding and memory of the routine, the article combines a large number of examples and classic application scenarios, visitors please taste carefully.

Just to mention, there’s a code principle that’s important

[Open and closed] principle

What is open and closed? To put it simply: When you change or extend an application, add code, but don’t change the logic.

Keep in mind as you read this article that many design patterns are designed to make code follow the “open and closed” principle. If you write code that follows this principle, the number of bugs will be greatly reduced.

👇🏻 below start routine time..

1. Proxy mode

1.1 Object Proxy

The proxy mode prevents an object from being directly accessed and requires a third party (proxy) to bridge access.

ES6 proxies are typically used to implement proxies.

Look at the business of a social App

  1. Users do not log in, but only check the strangers’ profile pictures, nicknames and other basic information
  2. The user is an ordinary user, can check the stranger’s basic information, but also check the education background
  3. Vip users can view strangers’ life photos in addition to the above information
// A stranger to represent
const stranger = {
  nickname: 'jack ma'.portrait: 'avatar'.educationBackground: 'bachelor'.lifePhotos: []}/ / permission group
const baseInfo = ['nickname'.'portrait']
const loginInfo = ['educationBackground']
const vipInfo = ['lifePhotos']

// Login user
const user = {
  isLogin: true.isVIP: false,}// Implement the proxy
const objProxy = new Proxy(stranger, {
  get(obj, key) {
    if(! user.isLogin && loginInfo.indexOf(key) ! = = -1) {
      console.log('Please login first')
      return
    }else if(! user.isVIP && vipInfo.indexOf(key) ! = = -1) {console.log('Please become a VIP first')
      return
    }
    return obj[key]
  },
  set(obj, key, val) {
    return val
  }
})

console.log(objProxy.nickname) // jack ma
console.log(objProxy.educationBackground) / / undergraduate
console.log(objProxy.lifePhotos) // undefined
Copy the code

1.2 Event Agent

It can be expensive to add click events to multiple child elements

  <div id="father">
    <a href="#">Link 1</a>
    <a href="#">Link 2</a>
    <a href="#">Link to no. 3,</a>
    <a href="#">Link 4,</a>
    <a href="#">Links to 5,</a>
    <a href="#">Links to 6</a>
  </div>
Copy the code

We can delegate events to the parent element

// Get the parent element
const father = document.getElementById('father')

// Install a listener for the parent element
father.addEventListener('click'.function(e) {
    // Identify if it is a target child
    if(e.target.tagName === 'A') {
        // This is the body of the listener function
        e.preventDefault()
        alert(I was `${e.target.innerText}`)}})Copy the code

When the child element is clicked, the click method of the parent element is triggered by the event bubble, and the real clicked child element is obtained by E.target, which is also a proxy mode.

1.3 Cache Proxy

When there are more complex operations, we can use the cache proxy to cache the values that have already been calculated, avoiding double operations and thus reducing overhead.

For example, if we have implemented a function that calculates the sum of all the input arguments, we can cache the calculated values through the proxy:

// Sum all the arguments passed in
const addAll = function() {
    let result = 0
    const len = arguments.length
    for(let i = 0; i < len; i++) {
        result += arguments[i]
    }
    return result
}

// Create a proxy for the summation method
const proxyAddAll = (function(){
    // Sum the result of the cache pool
    const resultCache = {}
    return function() {
        // Convert the input parameter to a unique input parameter string
        const args = Array.prototype.join.call(arguments.', ')
        
        // Check whether the input parameter has the corresponding calculation result
        if(args in resultCache) {
            // If so, return the result already in the cache pool
            return resultCache[args]
        }
        returnresultCache[args] = addAll(... arguments) } })()Copy the code

In the example above, the resultCache object is the cache pool, the key object is the string converted from all input arguments, and the value object is the sum result. This way, caching proxies can be implemented through proxyAddAll.

2. Singleton mode

The singleton pattern only allows a class or object to have a single instance, and it uses global variables to store that instance.

How to do it: Determine if there is an instance of the object, if there is, do not create it, if not, create an instance and use a closure to store it

Application scenario: Applies to service scenarios where only one instance exists, such as a pop-up window or shopping cart

/ / LanHanShi

let ShopCar = (function () {
  let instance;
  function init() {
    /* Here we define the singleton code */
    return {
      buy(good) {
        this.goods.push(good);
      },
      goods: [],}; }return {
    getInstance: function () {
      if(! instance) { instance = init(); }returninstance; }}; }) ();let car1 = ShopCar.getInstance();
let car2 = ShopCar.getInstance();
car1.buy("Orange");
car2.buy(The word "apple");
console.log(car1.goods); //[' orange ', 'apple']
console.log(car1 === car2); // true
Copy the code
/ / the hungry

var ShopCar = (function () {
  var instance = init();
  function init() {
    /* Here we define the singleton code */
    return {
      buy(good) {
        this.goods.push(good);
      },
      goods: [],}; }return {
    getInstance: function () {
      returninstance; }}; }) ();let car1 = ShopCar.getInstance();
let car2 = ShopCar.getInstance();
car1.buy("Orange");
car2.buy(The word "apple"); //[' orange ', 'apple']
console.log(car1.goods);
console.log(car1 === car2); // true
Copy the code

The realization effect has two kinds, lazy type and hungry han type, each have bad and bad, use according to demand

  • Lazy in the class load, do not create instances, so the class load speed is fast, but the runtime object acquisition speed is slow;
  • Hungry type when loading a class completes the initialization, class loading is so slower, but access speed of objects

3. Strategic mode

The strategy mode is not difficult to solve, and the weight is not high in the interview, so it can be understood and used.

The strategy pattern simply takes advantage of object mapping and avoids writing too many if and else.

For example, here is the code:

// Price tag and original price are accepted as input
function askPrice(tag, originPrice) {
  // Handle the preheating price
  if (tag === "pre") {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  }

  // Deal with big price
  if (tag === "onSale") {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  }

  // Handle the return price
  if (tag === "back") {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    returnoriginPrice; }}Copy the code

After using the strategy mode transformation:

// Price handles objects
let priceProcessor = {
  prePrice(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  salePrice(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  backPrice(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    returnoriginPrice; }};// Ask about the price
function askPrice(tag, originPrice) {
  return priceProcessor[tag](originPrice);
}
Copy the code

Advantages of using the policy pattern:

  • Different logics are separated from each other and do not affect each other. For example, when you modifyprePriceAfter logic, just let the test students regression the function of the type of price
  • Convenient to add new prices directlypriceProcessor.newPriceCan be

To sum up, the definition of a strategy pattern can be summarized: define a series of algorithms, encapsulate them one by one, and make them interchangeable.

To elaborate in the author’s own words:

Separate the if and else logic into functions, map these functions to an object, and finally provide an interface function for external calls.

4. State mode

State Pattern: Allows an object to change its behavior when its internal State changes. The object appears to have modified its class.

class Fox {
  constructor() {
    this.animationType = "default";
    this.foxLevel = 1;
  }
  // Animation processor
  animationProcessor = {
    that: this.default() {
      console.log(` fox levelThe ${this.that.foxLevel}Default animation ');
    },
    feed() {
      console.log(` fox levelThe ${this.that.foxLevel}Feeding animation ');
    },
    touch() {
      console.log(` fox levelThe ${this.that.foxLevel}Touch animation ');
    },
    upLevel() {
      console.log(` fox levelThe ${this.that.foxLevel}Upgrade animation ');
    },
    // Feed and upgrade combination animation
    feedAndUpLevel() {
      console.log(` fox levelThe ${this.that.foxLevel}Feed and upgrade animation '); }};// Trigger animation
  triggerAnimation(animationType) {
    try {
      this.animationProcessor[animationType]();
    } catch (err) {
      console.error("Error executing animation", err.message);
    }
    this.animationType = animationType; }}const myFox = new Fox();
myFox.triggerAnimation("feed"); // Fox level 1 feeding animation
Copy the code

5. Factory mode

When will it be used?

Use the factory pattern when there are too many classes to manage and the objects you want to create are related to each other (having the same parent class, implementing the same interface, and so on)

Role?

The factory pattern provides a centralized, unified approach that avoids the duplication of code and poor flexibility associated with the creation of objects in a decentralized manner.

5.1 Simple Factory Mode

For example, we use the simple factory model: build a factory that can make multiple brands of cars

// Car constructor
function SuzukiCar(color) {
  this.color = color;
  this.brand = "Suzuki";
}

// Car constructor
function HondaCar(color) {
  this.color = color;
  this.brand = "Honda";
}

// Car constructor
function BMWCar(color) {
  this.color = color;
  this.brand = "BMW";
}

// Car brand enumeration
const BRANDS = {
  suzuki: 1.honda: 2.bmw: 3};/** * auto factory */
function CarFactory() {
  this.create = function (brand, color) {
    switch (brand) {
      case BRANDS.suzuki:
        return new SuzukiCar(color);
      case BRANDS.honda:
        return new HondaCar(color);
      case BRANDS.bmw:
        return new BMWCar(color);
      default:
        break; }}; }Copy the code

5.2 Abstract factory pattern

To understand abstract factories, we need to clarify four concepts:

  • Abstract factories: Abstract classes used to declare the commonality of the end goal of a product.
  • Concrete factory: Inherits from the Abstract factory and implements the methods defined by the abstract factory to create the final product.
  • Abstract products: Abstract classes used to declare the commonalities of fine-grained products.
  • Concrete products: Inherit from abstract products and implement methods of abstract product definitions for creating fine-grained products. The implementation of an interface in a specific factory depends on the specific product class.

At first glance, the concept is confusing, but the specific application is clear:

// Abstract factory
class MobileFactory {
  constructor(name) {
    this.brandName = name;
  }

  installOS() {
    throw new Error("Currently abstract factory, sub-brand needs to realize this function"); }}// Specific factory
class HuaWei extends MobileFactory {
  installOS(OS) {
    OS.install(this); }}// Abstract product
class OS {
  constructor(name) {
    this.name = name;
  }
  install(phone) {
    throw new Error("Currently an abstract product that needs to be inherited and implemented"); }}// Specific products
class IosOS extends OS {
  constructor() {
    super("IOS");
  }
  install(phone) {
    console.log(`${phone.brandName} install The ${this.name} OS success`); }}class AndroidOS extends OS {
  constructor() {
    super("Android");
  }
  install(phone) {
    console.log(`${phone.brandName} install The ${this.name} OS success`); }}// Huawei phone P40
const huaweiP40 = new HuaWei("HuaWei P40");
const androidOSInstance = new AndroidOS();
huaweiP40.installOS(androidOSInstance); // HuaWei P40 install Android OS success
Copy the code

As you can see, abstract factories and abstract products are used to set specifications, while concrete factories and products do the real work. Factories and products are similar, but the granularity is different.

Abstract factory sets specifications through abstract factory class and abstract product class, making complex business clear. Finally, concrete factory integrates concrete products to achieve a complete product.

6. Decorator mode

The principle of decorator pattern: Add incremental code without changing the original code logic.

Suppose there is old code:

window.onload = () = > {
  console.log(document.getElementByTagName("*").length); / / 8
};
Copy the code

For existing requirements, print ‘page loaded complete’ after the page is loaded.

Use the decorator pattern to implement:

const fn = window.onload;
window.onload = () = > {
  typeof fn === "function" && fn();
  console.log("Page loaded");
};
/ / 8
// The page is loaded
Copy the code

Using decorator mode, you don’t touch old code, greatly reducing the chance of bugs.

7. Adapter mode

The adapter pattern is primarily used for code compatibility.

Consider an ancient project where data requests use ajax libraries.

// Send a get request
Ajax('get', URL address, post entry parameter,function(data){
    // Successful callback logic
}, function(error){
    // Failed callback logic
})
Copy the code

Now you need to change the data request library to the modern request library FETCH.

The adaptor pattern can be used if it is too expensive to modify and test everything where Ajax is called.

// Ajax adaptor functions that are used to participate in the old interface remain the same
async function AjaxAdapter(type, url, data, success, failed) {
    const type = type.toUpperCase()
    let result
    try {
         // All actual requests are initiated by the new interface
         if(type === 'GET') {
            result = await HttpUtils.get(url) || {}
        } else if(type === 'POST') {
            result = await HttpUtils.post(url, data) || {}
        }
        // Assume that the status code corresponding to a successful request is 1
        result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
    } catch(error) {
        // Catch network errors
        if(failed){ failed(error.statusCode); }}}// Use an adapter to accommodate older Ajax methods
async function Ajax(type, url, data, success, failed) {
    await AjaxAdapter(type, url, data, success, failed)
}
Copy the code

Using the adapter pattern:

  1. You need to implement an adapter that allows the parameters of the original Ajax function to seamlessly connect to the new request library
  2. Override the Ajax function and call the adapter

In this way, Ajax functions can be reused without having to change.

8. Observer mode

The observer mode is the uHF test point of the interview.

Observer mode, also known as publish and subscribe mode. A publisher provides the following functions: Adding subscriptions and publishing notifications; a subscriber provides the following functions: receiving notifications and running commands.

Application Scenarios:

  • Vue. Js bidirectional binding
  • Event Bus

A simple implementation of the observation pattern:

class Publisher {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  publish() {
    this.observers.forEach((item) = >{ item.receive(); }); }}class Observer {
  receive() {
    console.log("Execute orders upon receipt of notice."); }}const obs = new Observer();
const publish = new Publisher();

publish.addObserver(obs);
publish.publish(); // Execute the command after receiving the notification
Copy the code

To learn more about observation patterns, go to:

  • Vue bidirectional binding implementation principle
  • Handwritten EventBus

After the language

Finally, I wish you in interpersonal communication, encounter a little less routine, write code routine use a little more, I hope we can write the code like art, and you are not a code migrant workers 👩🏻🌾, but an artist!

Other articles

My front-end knowledge base

Classic and commonly used JS code snippets

Personal scaffolding