Small Copy of Knowledge Points of JavaScript Mode

Posted by oshecho on Mon, 20 May 2019 02:07:11 +0200

introduce

Recently, I started to set up a learning task for myself every week. The learning results are fed back to the output of an article and the learning records are made well.
My goal for this week (02.25-03.03) JavaScript Mode Chapter 7: Learn it again. Feedback of learning results is this article.
Because the content is too long, I divide this article into two parts:

This article mainly refers to the "JavaScript mode", some of which are from online information, there are notes, if inconvenience, please contact me to delete.

I will include this article in my knowledge base in two days. [Cute-JavaScript] In, and has been synchronized to [github] Above.

I. Singleton Pattern

1. Concept introduction

The idea of Singleton Pattern is to ensure that a particular class has only one instance, that is, no matter how many new objects are created with this class, it will get exactly the same object as the first one created.

It allows us to organize code into a logical unit and access it through a single variable.

The single mode has the following advantages:

  • Used to partition namespaces and reduce the number of global variables.
  • Make code organization more consistent, improve code readability and maintainability.
  • It can only be instantiated once.

But there are no classes in JavaScript, only objects. When we create a new object, it's all new singletons, because there will never be exactly the same objects in JavaScript unless they are the same object.
Therefore, every time we use object literals to create objects, we are actually creating a singleton.

let a1 = { name : 'leo' };
let a2 = { name : 'leo' };
a1 === a2;  // false
a1 == a2;   // false

It is important to note here that a condition of a singleton pattern is that the object can be instantiated. For example, the following is not a singleton pattern, because it cannot be instantiated:

let a1 = {
    b1: 1, b2: 2,
    m1: function(){
        return this.b1;
    },
    m2: function(){
        return this.b2;
    }
}
new a1();  // Uncaught TypeError: a1 is not a constructor

The basic structure of a single pattern is shown below:

let Singleton = function (name){
    this.name = name;
    this.obj = null;
}
Singleton.prototype.getName = function(){
    return this.name;
}
function getObj(name){
    return this.obj || (this.obj = new Singleton(name));
}
let g1 = getObj('leo');
let g2 = getObj('pingan');
g1 === g2;    // true
g1 == g2;     // true
g1.getName(); // 'leo'
g2.getName(); // 'leo'

It can be seen from this that the singleton pattern can only be instantiated once, and later calls are the result of the first instantiation.

2. Application scenarios

The singleton pattern allows only one instantiation, which improves object access speed and saves memory. It is commonly used in the following scenarios:

  • Objects that need to be created and destroyed frequently, or objects that are frequently used: pop-ups, files;
  • Commonly used tool class objects;
  • Commonly used objects with high resource consumption;

3. Realizing the case of bullet-frame

Here we want to use the single mode to create a bullet box, probably need to achieve: element values created once, when used directly call.
So we do this:

let create = (() => {
    let div;
    return () => {
        if(!div){
            div = document.createElement('div');
            div.innderHTML = 'I am leo Bullet Boxes Created';
            div.style.display = 'none';
            div.setAttribute("id", "leo");
            document.body.appendChild(div);
        }
        return div;
    }
})();
// Trigger event
document.getElementById('otherBtn').onclick = () => {
    let first = create();
    first.style.display = 'block';
}

4. Use the new operator

Because there are no classes in JavaScript, JavaScript has a new syntax to create objects with constructors and can use this method to implement monolithic patterns.
When you use the same constructor to create multiple objects with the new operator, you get a new pointer to exactly the same object.

Usually, we use the new operator to create three choices for the singleton pattern, so that the constructor always returns the original object:

  • Use global objects to store the instance (not recommended, easily polluted globally).
  • Using static attributes to store the instance does not guarantee the privatization of the static attributes.
function Leo(name){
    if(typeof Leo.obj === 'object'){
        return Leo.obj;
    }
    this.name = name;
    Leo.obj = this;
    return this;
}
let a1 = new Leo('leo');
let a2 = new Leo('pingan');
a1 === a2 ; // true
a1 ==  a2 ; // true

The only disadvantage is that the obj attribute is public and easy to modify.

  • Use closures to wrap the instance to ensure that the instance is private and will not be modified by the outside world.

By rewriting the above method, we add closures:

function Leo(name){
    let obj;
    this.name = name;
    obj = this;       // 1. Store objects created for the first time
    Leo = function(){ // 2. Modify the original constructor
        return obj;
    }
}
let a1 = new Leo('leo');
let a2 = new Leo('pingan');
a1 === a2 ; // true
a1 ==  a2 ; // true

When we first call the constructor, return this as usual, and then call it later, we rewrite the constructor and access the private variable obj and return.

Factory Pattern

1. Concept introduction

The purpose of the factory pattern is to create objects to achieve the following objectives:

  • Repeatable execution to create similar objects;
  • When the compiler locates the specific type (class), it provides an interface for the caller to create objects.

Objects created by factory methods (or classes) inherit parent objects. The following simple factory method understands:

function Person(name, age, sex){
    let p = {}; // Or let p = new Object(); create an initial object
    p.name = name;
    p.age = age;
    p.sex = sex;
    p.ask = function(){
        return 'my name is' + this.name;
    }
    return p;
}
let leo = new Person('leo', 18, 'boy');
let pingan = new Person('pingan', 18, 'boy');
console.log(leo.name, leo.age, leo.sex);          // 'leo', 18, 'boy'
console.log(pingan.name, pingan.age, pingan.sex); // 'pingan', 18, 'boy'

By calling the Person constructor, we can produce innumerable objects with three attributes and one method, just like a factory.
As you can see, the factory pattern can solve the problem of creating multiple similar objects.

2. advantages and disadvantages

2.1 advantages

  • A caller wants to create an object by knowing its name.
  • Extensibility is high, if you want to add a product, just extend a factory class.
  • Shielding the specific implementation of the product, callers only care about the interface of the product.

2.2 disadvantages

Each time a product is added, a specific class and object implementation factory are needed, which multiplies the number of classes in the system, increases the complexity of the system to a certain extent, and also increases the dependence of the specific class of the system. That's not a good thing.

3. Implementing Complex Factory Model

In complex factory mode, we defer the materialization of its member objects to subclasses, which can override the parent class interface methods to specify their own object types when they are created.
The parent class is similar to a common function, which only deals with problems in the creation process, and these processes will be inherited by the child class, then special functions will be implemented in the child class.

For example, here we need to implement such an example:

  • A common parent function CarMaker is needed;
  • The parent function CarMaker has a factor static method for creating car objects.
  • Define three static attributes with three functions to inherit the parent function CarMaker.

Then we want to use this function like this:

let c1 = CarMaker.factory('Car1');
let c2 = CarMaker.factory('Car2');
let c3 = CarMaker.factory('Car3');
c1.drirve();  // 'My number is 6'
c2.drirve();  // 'My number is 3'
c3.drirve();  // 'My number is 12'

As you can see, the call receives the object of the type specified as a string and returns the object of the request type, which is used without the need for the new operator.

See the code implementation below:

// Create a parent constructor
function CarMaker(){};
CarMaker.prototype.drive = function(){
    return `My number is ${this.id}`;
}
// Adding Static Factory Method
CarMaker.factory = function (type){
    let types = type, newcar;
    // An error occurs if the constructor does not exist
    if(typeof CarMaker[types] !== 'function'){
        throw{ name: 'Error', message: `${types}Non-existent`};
    }
    // If the constructor exists, let the prototype inherit the parent class, but only once
    if(CarMaker[types].prototype.drive !== 'function'){
        CarMaker[types].prototype = new CarMaker();
    }
    // Create a new instance and return
    newcar = new CarMaker[types]();
    return newcar;
}
// call
CarMaker.c1 = function(){
    this.id = 6;
}
CarMaker.c2 = function(){
    this.id = 3;
}
CarMaker.c3 = function(){
    this.id = 12;
}

When the definition is complete, we execute the previous code:

let c1 = CarMaker.factory('Car1');
let c2 = CarMaker.factory('Car2');
let c3 = CarMaker.factory('Car3');
c1.drirve();  // 'My number is 6'
c2.drirve();  // 'My number is 3'
c3.drirve();  // 'My number is 12'

You can print the results properly.

It is not difficult to implement the factory pattern, but mainly to find the constructor of the type object needed for piercing parts.
Here, a simple mapping is used to create the constructor of the object.

4. Built-in Object Factory

The built-in object factory, like the global Object() constructor, is also the behavior of the factory pattern, creating different objects according to the input type.
If an original number is passed in, a Number() constructor is returned to create an object, and a string or Boolean value is passed in.
A regular object is created for any other values that are passed in, including values that are not entered.

Object() can be called whether or not the new operator is used. We test this:

let a = new Object(), b = new Object(1),
    c = Object('1'),  d = Object(true);

a.constructor === Object;  // true 
b.constructor === Number;  // true 
c.constructor === String;  // true 
d.constructor === Boolean; // true 

In fact, Object() is not very useful, as it is listed here because it is our more common factory model.

Iterator Pattern

1. Concept introduction

Iterator pattern provides a way to access each element of an aggregated object sequentially without exposing its interior.

This model belongs to behavioral model and has the following characteristics:

  • Access the content of an aggregated object without exposing its internal representation.
  • Provide a unified interface to traverse data sets of different structures.
  • A traversal colleague may cause problems by changing the set structure where the iterator resides.

In the iterator pattern, there is usually an object that contains a collection of data, and a simple way to access each element is needed.
Here the object needs to provide a next() method, and each call must return the next contiguous element.

Let's assume that we create an object leo that accesses the next contiguous element by calling its next() method:

let obj;
while(obj = leo.next()){
    // do something
    console.log(obj);
}

In addition, in the iterator pattern, aggregated objects provide a more gradual hasNext() method to check whether the end of the data has been reached, so we modify the previous code:

while(leo.hasNext()){
    // do something
    console.log(obj);
}

2. Advantages and disadvantages and application scenarios

2.1 advantages

  • It simplifies aggregate classes and supports traversing an aggregate object in different ways.
  • There can be multiple traversals on the same aggregation.
  • In the iterator pattern, it is convenient to add new aggregator classes and iterator classes without modifying the original code.

2.2 disadvantages

Because the iterator pattern separates the responsibility of storing and traversing data, adding new aggregated classes requires adding new iterator classes corresponding to increasing the number of classes in pairs, which to some extent increases the complexity of the system.

2.3 Application Scenarios

  • Accessing the content of an aggregated object without exposing its internal representation.
  • There are many ways to traverse aggregated objects.
  • It provides a unified interface for traversing different aggregation structures.

3. Simple cases

According to the above introduction, we implement a simple case here, which will assume that our data is just an ordinary array, and then each retrieval will return an array element with an interval (i.e., not a continuous return):

let leo = (function(){
    let index = 0, data = [1, 2, 3, 4, 5],
        len = data.length;
    return {
        next: function(){
            let obj;
            if(!this.hasNext()){
                return null;
            };
            obj = data[index];
            index = index + 2;
            return obj;
        },
        hasNext: function(){
            return index < len;
        }
    }
})()

Then we need to provide it with simpler access and the ability to iterate data multiple times. We need to add the following two methods:

  • rewind() resets the pointer to the initial position;
  • current() returns the current element because the next() operation cannot be used when the pointer moves forward.

The code becomes like this:

let leo = (function(){
    //.. 
    return {
         // .. 
         rewind: function(){
             index = 0;
         },
         current: function(){
             return data[index];
         }
    }
})();

So this case is complete. Next we will test:

// Read record
while(leo.hasNext()){
    console.log(leo.next());
};  // Print 135
// Regression
leo.rewind();
// Get current
console.log(leo.current()); // Back to the original position, print 1

4. Application scenarios

Iterator patterns are commonly used: we can use the iterator pattern in cases where the internal results of a collection often vary, and we don't want to expose its internal structure, but we want the client code to access the elements transparently.

Simple understanding: traverse an aggregate object.

  • jQuery application example:

The $. each() method in jQuery lets us pass in a method that iterates over all items:

$.each([1,2,3,4,5],function(index, value){
    console.log(`${index}: ${value}`)
})
  • Implementing each() method using iterator pattern
let myEach = function(arr, callback){
    for(var i = 0; i< arr.length; i++){
        callback(i, arr[i]);
    }
}

4. summary

Iterator pattern is a relatively simple pattern, and most languages now have built-in iterators. And the iterator pattern is also very common, sometimes inadvertently used.

Decorator Pattern

1. Concept introduction

Decorator Pattern: Without changing the original class and inheritance, add functions to the object dynamically, and implement a new object with the same interface of the original object by wrapping an object.

The decorator model has the following characteristics:

  1. Adding functions does not change the original object structure.
  2. The interface provided by the decorated object is the same as that provided by the original object. It is convenient to use the decorated object according to the interface of the source object.
  3. Decorative objects contain references to the original object. That is to say, the decorative object is the object wrapped by the real original object.

In fact, one of the more convenient features of decorated patterns is the customizability and configurability of their expected behavior. Starting with ordinary objects with only basic functions, some functions of objects are constantly enhanced and decorated in order.

2. Advantages and disadvantages and application scenarios

2.1 advantages

  • Decoration class and decorated class can develop independently without coupling. Decoration mode is an alternative mode inherited. Decoration mode can dynamically expand the functions of an implementation class.

2.2 disadvantages

  • Multilayer decoration is more complicated.

2.3 Application Scenarios

  • Extend the functionality of a class.
  • Dynamic add function, dynamic revoke.

3. Basic cases

Here we implement a basic object sale, which can get the price of different items through the sale object, and return the corresponding price by calling sale.getPrice(). And in different cases, decorating it with additional functions will get different prices under different circumstances.

3.1 Create Objects

Here we assume that customers need to pay national and provincial taxes. According to the decorator model, we need to use national tax and provincial tax decorators to decorate the sale object, and then decorate the decorator using price formatting function. Actually it looks like this:

let sale = new Sale(100);
sale = sale.decorate('country');
sale = sale.decorate('privince');
sale = sale.decorate('money');
sale.getPrice();

After using the decorator model, each decoration is very flexible, mainly according to its decorator order, so if the customer does not need to pay the national tax, the code can be achieved as follows:

let sale = new Sale(100);
sale = sale.decorate('privince');
sale = sale.decorate('money');
sale.getPrice();

3.2 Implementation Object

Next we need to consider how to implement Sale objects.

One way to implement the Decorator pattern is to make each Decorator an object that contains methods that should be overloaded. Each decorator actually inherits objects that have been decorated by the previous decorator. Each decorator invokes the same method on uber (the inherited object) and gets the value. In addition, it continues to perform some operations.

The uber keyword is similar to Java's super, which allows a method to call a parent's method, and the uber attribute points to the parent's prototype.

That is, when we call the sale.getPrice() method, we call the method of the money decorator, and then each decorator method calls the method of the parent object first, so we call it up until the undecorated getPrice() method is implemented by the Sale constructor at the beginning. Understand the following figure:

We can implement the constructor Sale() and the prototype method getPrice():

function Sale (price){
    this.price = price || 100;
}
Sale.prototype.getPrice = function (){
    return this.price;
}

And the decorator object will be implemented by the attributes of the constructor:

Sale.decorators = {};

Next, the decorator country is implemented and its getPrice() is implemented. The method first gets the value from the method of the parent object and then modifies it.

Sale.decorators.country = {
    getPrice: function(){
        let price = this.uber.getPrice(); // Get the value of the parent object
        price += price * 5 / 100;
        return price;
    }
}

In the same way, other decorators are realized:

Sale.decorators.privince = {
    getPrice: function(){
        let price = this.uber.getPrice();
        price += price * 7 / 100;
        return price;
    }
}
Sale.decorators.money = {
    getPrice: function(){
        return "¥" + this.uber.getPrice().toFixed(2);
    }
}

Finally, we need to implement the decorate() method, which splices all our decorators together and does the following:
Create a new object newobj, inherit the current object we own (Sale), whether the original object or the last decorated object, here is the object this, and set the uber attribute of newobj, so that the child object can access the parent object, and then copy all the additional attributes of the decorator into newobj, return to newobj, that is to say, become the updated sale object:

Sale.prototype.decorate = function(decorator){
    let F = function(){}, newobj,
        overrides = this.constructor.decorators[decorator];
    F.prototype = this;
    newobj = new F();
    newobj.user = F.prototype;
    for(let k in overrides){
        if(overrides.hasOwnProperty(k)){
            newobj[k] = overrides[k];
        }
    }
    return newobj;
}

4. Rebuilding Basic Cases

Here we use lists to achieve the same function. This method takes advantage of the dynamic nature of JavaScript language, and does not need to use inheritance, nor does it need to let each decorated method call the method in front of the chain. It can simply pass the results of the previous method as parameters to the next method.

This implementation also has the advantage of supporting anti-decoration or revocation of decoration. We also achieve the following functions:

let sale = new Sale(100);
sale = sale.decorate('country');
sale = sale.decorate('privince');
sale = sale.decorate('money');
sale.getPrice();

The Sale() constructor now has an additional attribute of the Decorator List:

function Sale(price){
    this.price = (price > 0) || 100;
    this.decorators_list = [];
}

Then you need to implement Sale.decorators, where getPrice() will become simpler, and instead of calling getPrice() of the parent object, you pass the result as a parameter:

Sale.decorators = {};
Sale.decorators.country = {
    getPrice: function(price){
        return price + price * 5 / 100;
    }
}
Sale.decorators.privince = {
    getPrice: function(price){
        return price + price * 7 / 100;
    }
}
Sale.decorators.money = {
    getPrice: function(price){
        return "¥" + this.uber.getPrice().toFixed(2);
    }
}

At this point, decorate() and getPrice() of the parent object become complex. Decrate () is used to append the list of decorators. getPrice() needs to complete the task of calling getPrice() method of each decorator at the decorator level, including traversing the currently added decorator, and passing the results obtained from the previous method:

Sale.prototype.decorate = function(decorators){
    this.decorators_list.push(decorators);
}

Sale.propotype.getPrice = function(){
    let price = this.price, name;
    for(let i = 0 ;i< this.decorators_list.length; i++){
        name = this.decorators_list[i];
        price = Sale.decorators[name].getPrice(price);
    }
    return price;
}

5. Comparing the two methods

Obviously, the second list implementation method will be simpler, without design inheritance, and the decoration method will be simpler.
In this case, getPrice() is the only way to decorate. If we want to implement more methods that can be decorated, we can extract a method to iterate over the code in the decorator list for each additional decoration method to receive the method and make it a "decorated" method. In this way, the decorators_list attribute of sale becomes an object, and each attribute of the object is named after the method and value in the decorator object array.

Strategy Pattern

1. Concept introduction

Strategy Pattern: Encapsulates a series of algorithms to support us to choose different algorithms at runtime using the same interface. Its purpose is to separate the use of the algorithm from its implementation.

Policy pattern usually consists of two parts, one is policy class, which is responsible for implementing general algorithms, the other is environment class, which users receive client requests and delegate to policy class.

2. advantages and disadvantages

2.1 advantages

  • Effectively avoid multiple conditional selection statements;
  • It supports the open-close principle and encapsulates the algorithm independently, which makes it easier to switch, understand and expand.
  • It is more convenient for code reuse.

2.2 disadvantages

  • Strategic classes will increase.
  • All policy classes need to be exposed.

3. Basic cases

We can easily map strategies and algorithms directly:

let add = {
    "add3" : (num) => num + 3,
    "add5" : (num) => num + 5,
    "add10": (num) => num + 10,
}
let demo = (type, num) => add[type](num);
console.log(demo('add3', 10));  // 13
console.log(demo('add10', 12)); // 22

Then we extract the algorithm of each strategy:

let fun3  = (num) => num + 3;
let fun5  = (num) => num + 5;
let fun10 = (num) => num + 10;
let add = {
    "add3" : (num) => fun3(num),
    "add5" : (num) => fun5(num),
    "add10": (num) => fun10(num),
}
let demo = (type, num) => add[type](num);
console.log(demo('add3', 10));  // 13
console.log(demo('add10', 12)); // 22

4. Form validation case

We need to use the policy pattern to implement a method of handling form validation, which is invoked regardless of the specific type of form. We need to enable validators to choose the best strategy to handle tasks and delegate specific validation data to appropriate algorithms.

We assume that we need to validate the following form data:

let data = {
    name    : 'pingan',
    age     : 'unknown',
    nickname: 'leo',
}

Here you need to configure validators to use different algorithms for different data in the form data:

validator.config = {
    name    : 'isNonEmpty',
    age     : 'isNumber',
    nickname: 'isAlphaNum',
}

And we need to print the validation error information to the console:

validator.validate(data);
if(validator.hasErrors()){
    console.log(validator.msg.join('\n'));
}

Next, we will implement the validator verification algorithm. They all have the same interface, validator.types, which provides validate() method and instructions help information:

// Non-null value check
validator.types.isNonEmpty = {
    validate: function(value){
        return value !== '';
    }
    instructions: 'This value cannot be empty'
}

// NUMERICAL TYPE CHECKING
validator.types.isNumber = {
    validate: function(value){
        return !isNaN(value);
    }
    instructions: 'This value can only be a number.'
}

// Check if only numbers and letters are included
validator.types.isAlphaNum = {
    validate: function(value){
        return !/[^a-z0-9]/i.test(value);
    }
    instructions: 'This value can only contain numbers and letters, and does not contain special characters.'
}

Finally, we need to implement the core validator object:

let validator = {
    types: {}, // All available checks
    msg:[],    // Error information for current validation
    config:{}, // Verify configuration
    validate: function(data){ // Interface method
        let type, checker, result;
        this.msg = []; // Clear up error messages
        for(let k in data){
            if(data.hasOwnProperty(k)){
                type = this.config[k];
                checker = this.types[type];
                if(!type) continue;  // No type exists and no validation is required
                if(!checker){
                    throw {
                        name: 'Validation failed',
                        msg: `Cannot verify type: ${type}`
                    }
                }
                result = checker.validate(data[k]);
                if(!result){
                    this.msg.push(`Invalid value: ${k},${checker.instructions}`);
                }
            }
        }
        return this.hasErrors();
    }
    hasErrors: function(){
        return this.msg.length != 0;
    }
}

Summing up this case, we can see that the validator object is universal, and the methods that need to enhance the validator object only need to add more type checks. For each new use case, we only need to configure the validator and run the validator() method.

5. summary

In daily development, we still need to choose design patterns according to the actual situation, rather than design patterns for the sake of design patterns. Through the above study, we use the strategy pattern to avoid multiple conditional judgments, and encapsulate the method through the open-close principle. We should accumulate our own development tool library in the process of development, so that we can use it in the future.

Reference material

  1. <JavaScript Patterns>
Author Ping An Wang
E-mail pingan8787@qq.com
Blog www.pingan8787.com
WeChat pingan8787
Daily article recommendation https://github.com/pingan8787...
JS Brochure js.pingan8787.com
Wechat Public Number Front-end self-study

Topics: Javascript Attribute github JQuery