preface
This paper is divided into two parts, the first part is the basic knowledge of JS, mainly introduces some relevant peripheral knowledge in the design pattern, such as JS object oriented, this pointing and closure and higher-order function application. The second part is the positive part, mainly introduces 14 kinds of JS design mode.
The first part is JS related basis
1. Object oriented in JS
1.1 Dynamic typing languages and duck types
Programming languages can be roughly divided into two types in terms of data types: statically typed and dynamically typed.
Statically typed languages: statically typed languages have made it clear at compile time variable data types, advantage is that the compiler can be run before test the data type, avoid during program execution data types of errors, because clearly the data type, the compiler can also some optimization work according to these information, so as to improve the execution efficiency.
Dynamically typed languages: Variables in dynamically typed languages do not have a type until the program runs and the variable is assigned a value. The advantage is that the code is simple, and although the data type is not differentiated, more energy can be put into the logical layer of the business.
JS is a typical dynamic typing language, and its tolerance of variable types brings great flexibility to the actual coding. Because there is no type detection, we can try to call any method on any object, regardless of whether it was designed to own the method in the first place. It all builds on the concept of duck typing, which goes like this: “If it walks like a duck and quacks like a duck, it’s a duck.”
Once upon a time, there was a king who thought the most beautiful sound in the world was the quacking of ducks. So the king called together his ministers to form a choir of 1,000 ducks. The ministers searched all over the country to find 999 ducks, but one was still missing. At last, the ministers found a very special chicken. Its quack sounded exactly like a duck, so the chicken became the last member of the chorus.
The moral of the story is that all the king listens to is the quack of a duck, and it doesn’t matter whether the sound belongs to a chicken or a duck. The duck type instructs us to focus only on the behavior of the object and not on the object itself.
let duck={
call(){
console.log("gagaga")}}let chicken={
call(){
console.log("gagaga")
}
}
duck.call();// output "gagaga"
chicken.call();// output "gagaga"
Copy the code
1.2 polymorphic
The concept of polymorphism: the same operation on different objects can produce different execution results. In other words, send the same request to different objects, and these objects will give different responses based on the request.
On the set of a movie, when the director calls out the word “action”, the main character begins to recite his lines, the lighting engineer does the lighting, the walk-on is responsible for pretending to be shot and falling to the ground, and the prop engineer is responsible for igniting the explosives. Each object knows what to do when given the same instruction, and each object does something different. This is the polymorphism that objects exhibit.
The fundamental purpose of polymorphism is to eliminate procedural conditional branching statements by converting them into object polymorphism.
Here’s a practical example:
function callOut(annimal){
if(annimal instanceof Forg){
console.log("Quack, quack.");
}else if(annimal instanceof Cat){
console.log("Meow"); }}let Forg=function(){};
let Cat=function(){};
callOut(new Forg());// output "quack quack"
callOut(new Cat());// output "meow"
Copy the code
The code above does demonstrate polymorphism. When we say bark, they will react differently to the command, but if we add another dog, the dog will bark, and we have to change the callOut function by adding a branch statement to the function. Modifying the code is always accompanied by risks. The more places are modified, the greater the probability of program errors will be. If there are more and more animals, the callOut function body will become huge, which is not conducive to our later maintenance. We can separate out the things that don’t change, encapsulate the things that don’t change, in this case the animal calls, but the things that don’t change are how different animals call, and we can separate what from who does it and how it does it, to make it look like it’s growable. The code optimization results are as follows:
let Forg=function(){};
let Cat=function(){};
Forg.prototype.call=function(){
console.log("Quack, quack.");
}
Cat.prototype.call=function(){
console.log("Meow");
}
function callOut(annimal){
annimal.call();
}
callOut(new Forg());// output "quack quack"
callOut(new Cat());// output "meow"
Copy the code
This not only makes the code more extensible, but also eliminates conditional branching.
1.3 packaging
Encapsulation is to hide information, encapsulation can be divided into encapsulation data, encapsulation implementation, encapsulation type, encapsulation change.
Encapsulating data: Many languages provide private, public, protected and other keywords to provide different access permissions, but there is no support for these keywords in JS. We can rely on the scope of variables to achieve data encapsulation, ES6 can use let and {} block level scope to achieve data encapsulation, ES6 can use let and {} block level scope to achieve data encapsulation. We can also use the scope of a function to implement encapsulation:
let obj=(function(){
let _name="Zhang Chaoyang";
return {
getName(){
return_name; }}}) ()console.log(obj.getName());// Output "Zhang Chaoyang"
console.log(_name);/ / output "undefined"
Copy the code
Encapsulation implementation: the implementation details, design details for encapsulation and external exposure API interface to communicate, so that users can not care about its internal implementation, as long as the external interface does not change, it will not affect the other functions of the program.
Encapsulating type: generally encapsulating type is for statically typed language, through abstract class and interface to carry on type encapsulation, but JS is a kind of fuzzy type dynamic language, so in encapsulating type, JS has no ability, also do not need to do.
Package change: Package change is to separate the stable part of the system and easy to change the part, and then package in turn. When changing parts of the system need to be changed, they are relatively easy to replace because they have been packaged before, thus ensuring the stability and scalability of the program.
1.4 Prototype pattern and JS object system based on prototype inheritance
The prototype pattern is a pattern for creating objects. If we want to create an object, one way is to specify its type and then create the object from the class. The other is the prototype model, where we don’t care about the specific type of object, but find an object and clone it to create an identical object. The key to the prototype pattern is whether the language itself provides a Clone approach. ES5 provides the object. create method, which can be used to clone objects. The code is as follows:
Function Person(){this.name=" "; } let p1=new Person(); let p2=Object.create(p1); console.log(p2.name); // Output "Zhang Chaoyang"Copy the code
JS object system based on prototype inheritance: Any object has a prototype object, which is pointed to by the object’s built-in attribute _proto_ to the prototype object of its constructor. Objects don’t have a Prototype property; only functions have a Prototype property. Any Object has a constructor property that points to the constructor that created it, such as the {} Object, whose constructor is function Object(){}.
Constructor: function is declared as a function, but if called directly, it is a normal function. When called with the keyword new, the function is called as a constructor, and it is also the constructor of the object out of new. The following code demonstrates the above concepts:
function Person() {}var p = new Person();
// Method has prototype, normal object has no prototype
console.log(Person.prototype); // Object{}
console.log(p.prototype); // undifined
Any object has a constructor. Objects created by the constructor can also get a reference to the constructor
// Just print what the constructors of the following objects are.
console.log(p.constructor); //function Person(){}
console.log(Person.constructor); //function Function(){}
console.log({}.constructor); // function Object(){}
console.log(Object.constructor); // function Function() {}
console.log([].constructor); //function Array(){}
Copy the code
JS prototype chain inheritance, first look at the following code:
let A=function(){}; A.prototype={"name":" zhang "}; let B=function(){}; B.prototype=new A(); let b=new B(); console.log(b.name); // Output "Zhang Chaoyang"Copy the code
Let’s take a look at what the engine is doing when this code is executed.
- First, try traversing all properties in object B, but the name property is not found.
- To find the
name
Property is delegated to the prototype of object B’s constructor, which is calledb._propt_
Recording and pointingB.prototype
And theB.prototype
Is set to a passnew A()
The created object. - Still not found in object
name
Property, and the request continues to delegate to the prototype of the object constructorA.prototype
. - in
A.prototype
Found in thename
Property and returns its value.
Prototype =new A() adds one more layer to the prototype chain than b.protoType =new A(), which points directly to A literal object. However, there is no essential difference between the two. They both point the prototype of the Object constructor to another Object. Inheritance always occurs between objects.
2, This, call, apply, bind introduction
2.1 this
This in JS always refers to an object that is dynamically bound at runtime based on the execution environment of the function, not the environment in which the function was declared. The direction of this can be roughly divided into four types:
- Method invocation as an object
let person={
name:"Zhang Chaoyang".getName(){
console.log(this===person);/ / output true
console.log(this.name);// Output "Zhang Chaoyang"
}
}
person.getName();
Copy the code
- Called as a normal function
When a function is not called as a property of an object, as is often the case with ordinary functions, this always refers to the global object. In JS, this global object is the Window object.
window.name="Zhang Chaoyang";
function getName(){
console.log(this.name);
}
getName();// Output "Zhang Chaoyang"
Copy the code
Note that when we add an event function to the div node, inside the function this refers to the div object. When we define a normal function inside the function (except for the arrow function, which doesn’t hold this, it calls the parent’s this pointer), The this inside of this ordinary function refers to the window object.
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="test">Click on the event</div>
<script type="text/javascript">
window.id="Zhang Chaoyang";
document.getElementById("test").onclick=function(){
console.log(this.id);/ / output "test"
function callback(){
console.log(this.id);// Output "Zhang Chaoyang"
}
callback();
let handle=() = >{
console.log(this.id);/ / output "test"
}
handle();
}
</script>
</body>
</html>
Copy the code
- Constructor call
Most functions in JS can be used as constructors. Constructors look just like normal functions; the difference is how they are called. When a function is called with the new operator, the function always returns an object. Normally, this in the constructor refers to the object returned.
function Person(){
this.name="Zhang Chaoyang";
}
let p=new Person();
console.log(p.name);// Output "Zhang Chaoyang"
Copy the code
If the constructor explicitly returns an object of type Object, the result of the operation will eventually return that object, meaning that this refers to the returned object.
Function Person(){this.name=" "; Return {name:" Person "}} let p=new Person(); console.log(p.name); // output "luo Yonghao"Copy the code
If the constructor does not explicitly return any data, or if it returns data of a non-object type, this does not cause the above problems.
Function Person(){this.name=" "; } let p=new Person(); console.log(p.name); // Output "Zhang Chaoyang"Copy the code
- Function is
call
orapply
A call
Call and Apply can dynamically change the this reference inside functions
let p1={
name:"Zhang Chaoyang".getName(){
console.log(this.name); }}let p2={
name:"Luo Yonghao"
}
p1.getName();// Output "Zhang Chaoyang"
p1.getName.call(p2);// output "luo Yonghao"
Copy the code
The this pointer in the function is dependent on the execution environment, not the declaration environment, for example:
let p1={ age:30, getAge(){ console.log(this.age); } } p1.getAge(); // let p2= p1.getage; p2(); / / output "undefined"Copy the code
When p1.getage () is called, getAge is called as an attribute of p1, and this refers to p1. When p2 is called, this refers to the global window object. So the result of program execution is undefined.
2.2 Call, apply, bind
Similarity: Change the orientation of this inside the function. Similarities: Call and apply accept parameters differently. Bind is not executed immediately, while apply and call are executed immediately.
Call and apply accept parameters in different ways.
let arr = [1.5.9.3.5.7];
Math.max.apply(Math, arr);// Result: 9
Math.max.call(Math.1.5.9.3.5.7);// Result: 9
Copy the code
If you change the direction of this, bind will not execute immediately, but apply and call will execute immediately.
let p1={
"name":"Zhang Chaoyang"
}
let getName=function (){
console.log(this.name)
}.bind(p1)
getName();// Output "Zhang Chaoyang"
Copy the code
When using Call or Apply, if the first argument we pass in is null, this in the function body points to the default host object, or window in the browser.
In addition to changing the direction of this, call and apply can also borrow methods of other objects, such as constructors. In this way, some effects similar to inheritance can be achieved:
function A(name){
this.name=name;
}
function B(){
A.apply(this.arguments);
}
B.prototype.getName=function(){
return this.name;
}
let b=new B("Zhang Chaoyang");
console.log(b.getName());// Output :" Zhang Chaoyang"
Copy the code
You can also add some new elements to function arguments using the array. prototype method:
(function(){
Array.prototype.push.call(arguments.3);
console.log(arguments);// output class array [1,2,3]}) (1.2)
Copy the code
Closures and higher-order functions
3.1 the closure
The cause of closures: The formation of closures is closely related to the scope and life cycle of variables.
The scope of a variable refers to the effective scope of a variable, usually the scope of a variable declared in a function. In general, var or let keywords are used to declare a variable in a function. If the variable name is not preceded by a keyword, the variable becomes a global variable. A variable declared inside a function with the keyword var or let is a local variable. This variable can be accessed only inside the function, not outside the function. Example code is as follows:
function test(){
let a=1;
console.log(a);/ / output 1
}
test();
console.log(a);// The console reported an error, a is not defined
Copy the code
Internal variable access features: When accessing a variable in a function, if the variable is not declared, it continues to search layer by layer outside the scope until it reaches the global object. Variables are searched from the inside out. Example code is as follows:
let a=1;
function test(){
let b=2;
function test2(){
let c=1;
console.log(a);/ / output 1
console.log(b);2 / / output
}
test2();
console.log(c);// Error c is not defined
}
test();
Copy the code
Life cycle of a variable: Cycle statement for global variables, it is permanent, unless we take the initiative to destroy the global variables (i.e., assigning null), for the functions declared within a local variable, when out of function, they will usually be destroyed by the end of the function call, of course, a special kind of being, is the local variables can be outside access, There is a reason not to destroy the local variable, and the life cycle of the local variable seems to continue. We call this environment a closure. Example code is as follows:
function test(){
let a=1;
return function(){ a++; alert(a); }}let fn=test();
fn();2 / / output
fn();3 / / output
fn();/ / output 4
Copy the code
When fn() is called, fn is an anonymous function that has access to the local variable A in the test environment, so a is not destroyed at the end of the test execution, but is stored in a closure.
The role of closures
1. Encapsulating variable closures can encapsulate variables that do not need to be exposed globally as “private variables”, such as variable A in the previous example, which can be modified and accessed by returning functions.
2. Extend the life of local variables
The Image object can be used for data reporting. The code is as follows:
function report(src){
let img=new Image();
img.src=src;
}
report("http://www.xxx.com/xxx");
Copy the code
The above method for data reporting will cause problems in browsers of earlier versions, and about 30% of the data will be reported missing. The reason is that the report function does not successfully initiate HTTP requests every time. Img is a local variable in the report function, and the img local variable will be destroyed immediately after the call of report function. At this point, the HTTP request may not be sent, so the data report will be lost. The img variable can be enclosed in a closure to extend the lifetime of the change, thus solving the problem of data loss. Modify the code as follows:
let report=(function(){
let imgs=[];
return function(src){
let img=new Image();
imgs.push(img);
img.src=src;
console.log(imgs);
}
})()
report("http://www.xxx.com/xxx");
Copy the code
The relationship between closures and memory
Closures are a very powerful feature, but there are a lot of misconceptions about them. Closures cause memory leaks, so reduce the use of closures. That’s not true. Local variables in closures that are not set to NULL have the same effect as global variables and are stored in memory. This is not a memory leak. If we do not use these variables in the future, we can set them to NULL manually to free up memory. If a circular reference is created between two objects, then neither object can be reclaimed. This is essentially a leak caused by a circular reference, not a leak caused by a closure. To solve the memory leak caused by circular references, we simply set the variable in the circular reference to NULL.
3.2 Higher-order functions
Features of higher-order functions: 1. Functions can be passed as parameters; 2. 2. The function can be output as a return value.
1. Functions are passed as arguments
The purpose of passing functions as parameters is to separate the mutable parts of the business code from the mutable parts by encapsulating the business logic into functions and passing it as parameters. One of the most important scenario applications is the callback function.
For example, we have a requirement to create 10 div nodes in a page and set them to hidden. Then we write code like this:
function appendDiv(){
for(let i=0; i<10; i++){let oDiv=document.createElement('div');
oDiv.innerHTML=i;
document.body.appendChild(oDiv);
oDiv.style.display="none";
}
}
appendDiv();
Copy the code
The oDiv. Style. The display = “none”; It is not appropriate to write the logic inside the appendDiv function because it is personalization code, and once it is written into the function, it becomes a function that is difficult to reuse. Not everyone needs to hide the node after it is created, but it is possible to do something else. Odiv.style. display=”none”; Pull it out and pass it into the appendDiv function as a callback. The code is modified as follows:
function appendDiv(callback){
for(let i=0; i<10; i++){let oDiv=document.createElement('div');
oDiv.innerHTML=i;
document.body.appendChild(oDiv);
if(typeof callback === 'function'){
callback(oDiv);
}
}
}
appendDiv(function(obj){
obj.style.display="none";
});
Copy the code
As you can see, the request to hide the node is actually initiated by the client, but the client does not know when the node will be created, so it delegates the logic to the appendDiv method in a callback function.
2. The function is output as a return value
There are probably more applications where functions are output as return values than functions passed as arguments, and functional programming is more clever. Having a function continue to return an executable function means that the operation is continuous. The following is an example:
function test(){
let a=1;
return function(){ a++; alert(a); }}let fn=test();
fn();2 / / output
fn();3 / / output
fn();/ / output 4
Copy the code
Higher-order functions implement AOP
The main role of AOP is to extract some functions irrelevant to the core business module, such as log statistics, security control, exception handling, etc. We can do this with the help of function.prototype, as shown in the following example:
Function.prototype.before=function(beforeFn){
let self=this;// This is the antifunction
return function(){
beforeFn.apply(this.arguments);// This refers to the window object and executes the function passed in
return self.apply(this.arguments);// This refers to the window object and executes the original function}}Function.prototype.after=function(afterFn){
let self=this;
return function(){
self.apply(this.arguments);
afterFn.apply(this.arguments); }}let func=function(){
console.log(2);
}
func=func.before(function(){
console.log(1);
}).after(function(){
console.log(3);
})
func();// the console outputs 1,2,3 in sequence
Copy the code
Other applications of higher order functions
1. Curried rying
Because currization is not easy to understand literally, here’s an example. Let’s say I want to figure out how much I spent for three days. The code is as follows:
let total=0;
function cost(money){
total=total+money;
}
cost(100);
cost(200);
cost(300);
console.log(total);// The output cost for 3 days is 600;
Copy the code
Through the above code, we can know, I will calculate it again after the end of the day until today, a total of how much you spend, but we don’t really care about these, I just want to get the final total spent how many money, also is just at the end of the day just calculate how much in total, not every day to do calculation. This is called currying. We can transform the above code into a currie. The code is as follows:
let calTotal=(function(){
let arr=[];
return function(){
if(arguments.length===0) {let total=0;
for(let i=0; i<arr.length; i++){ total=total+arr[i]; }return total;
}else{ arr.push.apply(arr,[...arguments]); }}}) (); calTotal(100);
calTotal(200);
calTotal(300);
console.log(calTotal());// The output cost for 3 days is 600;
Copy the code
2. Uncurrying
The effect of inverse Corrification is to enlarge the applicability of functions, so that functions that are originally owned by specific objects can be used by any object. Func (arg1, arg2) is given as obj. Func (obj, arg1, arg2) and converted into a function form with the signature as func(obj, arg1, arg2). This is the formal description of anti-Currization.
3. Function throttling
In some scenarios, functions can be called very frequently, causing major performance problems. Such as window. The onresize event functions in some complex operations, and a large number of frequent trigger this function will certainly cause some caton, when changes in the browser window, performed 100 assumption 1 second internal logic operation, obviously we don’t need so often perform the internal logic, which actually need to be performed only once or twice, This requires us to ignore the execution of some internal logic according to the time period. For example, we only execute once within 500ms, which requires setTimeout to complete this task. The following is an example:
function throttle(fn,interval){
let timer=null;
let firstTime=true;
return function(){
let that=this;
let args=arguments;
if(firstTime){
fn.apply(this,args);
return firstTime=false;
}
if(timer){
return false;
}
timer=setTimeout(function(){
clearTimeout(timer);
timer=null;
fn.apply(that,args);
},interval||500)}}window.onresize=throttle(() = >{
console.log(1);
})
Copy the code
4. Time-sharing function
Due to some user operations, when a large number of rendering or DOM operations are needed in the page, it is bound to cause page lag. For example, when thousands of DOM nodes need to be created in the page at one time, time-sharing and batch operations are needed to ensure that the page does not lag. Let’s take a look at some examples of code that can cause a page to freeze or stagnate:
let arr=[];
for(let i=0; i<100000; i++){ arr.push(i); }function renderList(list){
for(let i=0; i<list.length; i++){let div=document.createElement('div');
div.innerHTML=i;
document.body.appendChild(div);
}
}
renderList(arr);// Render is slow
Copy the code
Optimize the code as shown in the following example:
First, we create a timeChunk function that takes three arguments. The first argument is the data needed to create the DOM, the second argument is the logical function that encapsulates the creation of the node, and the third argument represents the number of nodes created in each batch.
let arr=[];
for(let i=0; i<100000; i++){ arr.push(i); }function timeChunk(list,fn,count){
let timer=null;
function start(){
for(let i=0; i<Math.min(count||1,list.length); i++){letitem=list.shift(); fn(item); }}return function(){
timer=setInterval(function(){
if(list.length===0) {return clearInterval(timer);
}else{ start(); }},200)}}let render=timeChunk(arr,function(item){
let div=document.createElement('div');
div.innerHTML=item;
document.body.appendChild(div);
},10)
render();
Copy the code
5. Lazy loading functions
In real development, some sniffing is inevitable due to differences between browsers. Of course, there are very few differences between the major browsers, so this is just an example of how to do a generic object event binding with IE8 compatibility.
<! DOCTYPE html><html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<button id="test">Click on the I</butt>
<script type="text/javascript">
let addEvent=function(elem,type,handler){
if(window.addEventListener){
return elem.addEventListener(type,handler,false);
}
if(window.attachEvent){
return ele.attachEvent('on'+type,handler); }}let obj=document.getElementById('test');
addEvent(obj,'click'.() = >{
alert(1);
})
</script>
</body>
</html>
Copy the code
The disadvantage of this method is that the if statement is executed every time it is called. Although the if statement is not expensive, there is a way to avoid the repeated judgment in the conditional statement. The optimized code is as follows:
<! DOCTYPE html><html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<button id="test">Click on the I</butt>
<script type="text/javascript">
let addEvent=(function(){
if(window.addEventListener){
return function(elem,type,handler){
elem.addEventListener(type,handler,false); }}if(window.attachEvent){
return function(elem,type,handler){
ele.attachEvent('on'+type,handler); }}}) ();let obj=document.getElementById('test');
addEvent(obj,'click'.() = >{
alert(1);
})
</script>
</body>
</html>
Copy the code
This is done immediately when the code is loaded, and if it is executed again, it does not need to be done again. However, the disadvantage is that if we do not use addEvent at all, we will also perform a sniffing task, which is obviously unnecessary. So we can optimize the code as follows:
<! DOCTYPE html><html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<button id="test">Click on the I</butt>
<script type="text/javascript">
let addEvent=function(elem,type,handler){
if(window.addEventListener){
addEvent=function(elem,type,handler){
elem.addEventListener(type,handler,false); }}else if(window.attachEvent){
addEvent=function(elem,type,handler){
elem.attachEvent('on'+type,handler);
}
}
addEvent(elem,type,handler);
}
let obj=document.getElementById('test');
addEvent(obj,'click'.() = >{
alert(1);
})
</script>
</body>
</html>
Copy the code
This approach is lazy-loaded functions, where the current function is re-executed internally after the first execution, so that the next time the branch logic is overwritten, the execution efficiency is improved.
The second part is design mode
This section does not cover all of the 23 design patterns proposed by GoF, but instead selects 14 design patterns that are more common in JS development.
4. Singleton mode
The singleton pattern is defined to ensure that a class has only one instance and provides a global access point to access it.
4.1 Implement the singleton mode
Implementing a standard singleton pattern is nothing more than a variable that indicates whether an object has been created for a class and, if so, returns the previously created object the next time an instance of the class is fetched. The code is as follows:
let Singleton=function(name){
this.name=name;
this.instance=null;
}
Singleton.prototype.getName=function(){
alert(this.name);
}
Singleton.getInstance=function(name){
if(!this.instance){
this.instance=new Singleton(name);
}
return this.instance
}
let a=Singleton.getInstance("a");
let b=Singleton.getInstance("b");
alert(a===b);/ / output true
Copy the code
Or:
let Singleton=function(name){
this.name=name;
}
Singleton.prototype.getName=function(){
alert(this.name);
}
Singleton.getInstance=(function(){
let instance=null;
return function(name){
if(! instance){ instance=new Singleton(name);
}
return instance;
}
})()
let a=Singleton.getInstance("a");
let b=Singleton.getInstance("b");
alert(a===b);/ / output true
Copy the code
The above code implements a simple Singleton pattern, but one problem is that it increases the opacity of the class. Users of the Singleton class must know that this is a Singleton class. Instead of fetching objects using the new operator, singleton.getInstance is used to fetch objects. We can optimize it further.
4.2 Transparent singleton mode
When a user creates a singleton object from a class, it can be created just like any other normal type, with the following code:
let CreateDiv=(function(){
let instance;
let CreateDiv=function(html){
if(instance){
return instance;
}
this.html=html;
this.init();
return instance=this;
}
CreateDiv.prototype.init=function(){
let div=document.createElement("div");
div.innerHTML=this.html;
document.body.appendChild(div);
}
returnCreateDiv; }) ()let a=new CreateDiv('a');
let b=new CreateDiv('b');
alert(a===b);/ / output true
Copy the code
The code above completes a transparent singleton pattern, but the code looks a bit complex and isn’t very pleasant to read. The CreateDiv constructor actually does two things: it creates an object and initializes the init method, and it ensures that there is only one object. We can actually separate these two things out of the constructor and optimize them further. Because the current structure is not conducive to retrofit and maintenance, if we no longer need a single instance, we need to completely change the constructor.
4.3 Implement singleton pattern with proxy
To solve this problem, we use the proxy pattern. First, we take the CreateDiv constructor’s code to manage the singleton and make it a normal class, as follows:
let CreateDiv=function(html){
this.html=html;
this.init();
}
CreateDiv.prototype.init=function(){
var div=document.createElement('div');
div.innerHTML=this.html;
document.body.appendChild(div);
}
Copy the code
Next introduce the proxy class proxySingletonCreateDiv:
let ProxySingletonCreateDiv=(function(){
let instance;
return function(html){
if(! instance){ instance=new CreateDiv(html);
}
return instance;
}
})()
let a=new ProxySingletonCreateDiv('a');
let b=new ProxySingletonCreateDiv('b');
alert(a===b);/ / output true
Copy the code
Instead, we delegate the logic for managing singletons to the proxySingletonCreateDiv class, making CreateDiv a generic class and making the code more reusable and maintainable.
4.4 Singleton pattern in JavaScript
The singleton pattern in front of the implementation, more is for the traditional object-oriented language implementation, JS is actually a classless language, mechanically copy the singleton pattern of traditional object-oriented language is meaningless, is not suitable for JS.
The core of the singleton pattern is to ensure that there is only one instance and to provide global access. JS global variables, although not a singleton pattern, but it conforms to the two characteristics of the singleton pattern, so we often to use global variables as a singleton, but much more global variable definition can create namespace pollution, usually can use a namespace and using closures encapsulated private variables in two ways to reduce the variable naming pollution.
1. Namespace mode
let namespace={
a:function(){
alert(1)},b:function(){
alert(2)
}
}
namespace.a();
Copy the code
2. Encapsulate private variables with closures
let user=(function(){
let _name='Zhang Chaoyang';
let _age='32';
return {
getUserInfo(){
return `${_name}-${_age}`}}}) ()console.log(user.getUserInfo());// output zhang Chaoyang -32
Copy the code
4.5 Lazy Singleton
Lazy singletons are instances of objects that are created only when needed. The singleton pattern used in the previous example is to create the instance as soon as the page is loaded, but we may not need the instance yet. Can we create the singleton when we call create instance, in which case we will use the lazy singleton?
For example, there is a login button in the page. After clicking the login button, a login popup box will appear. Obviously, this popup window is unique in the page and it is impossible to have two login Windows at the same time.
Solution: Create the div popover when the page is loaded. The popover is hidden from view until the user clicks the login button. The code looks like this:
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button type="button" id="login">The login button</button>
<script type="text/javascript">
let loginLayer = (function() {
let div = document.createElement('div');
div.innerHTML = "I'm a login popover.";
div.style.display = "none";
document.body.appendChild(div);
returndiv; }) ()document.getElementById("login").onclick = function() {
loginLayer.style.display = "block";
}
</script>
</body>
</html>
Copy the code
One problem with this approach is that when we only want to view non-login related pages, we don’t need to log in at all, because the login popup is always created in the first place, wasting resources. We can optimize the code so that the login popover is created only when the user clicks the login button, as follows:
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button type="button" id="login">The login button</button>
<script type="text/javascript">
let createLoginLayer = function() {
let div = document.createElement('div');
div.innerHTML = "I'm a login popover.";
div.style.display = "none";
document.body.appendChild(div);
return div;
}
document.getElementById("login").onclick = function() {
let loginLayer=createLoginLayer();
loginLayer.style.display = "block";
}
</script>
</body>
</html>
Copy the code
The above code achieves its purpose of inertia, but it loses the singleton effect. Every time we click on the login button, we create a login popover, which is obviously not what we want, so we can continue to optimize, with the following code:
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button type="button" id="login">The login button</button>
<script type="text/javascript">
let createLoginLayer = (function() {
let div;
return function(){
if(! div){ div =document.createElement('div');
div.innerHTML = "I'm a login popover.";
div.style.display = "none";
document.body.appendChild(div);
}
return div;
}
})();
document.getElementById("login").onclick = function() {
let loginLayer=createLoginLayer();
loginLayer.style.display = "block";
}
</script>
</body>
</html>
Copy the code
4.6 Generic Lazy singleton
This section is the most core and important content of THE JS singleton pattern. All the examples explained before are paving the way for this section.
In the previous section, we completed a lazy singleton, but there are still some problems. The code above violates the single responsibility principle, which is to create the object and manage the singleton logic in the createLoginLayer function, making the code reusable and maintainable. If we can create the object and manage the singleton logic separately, To solve the above problem, the code is as follows:
Let’s first deal with the singleton logic management part. This part of the logic is the same idea wherever we put it, that is, we use a variable to indicate whether the object was created, if so, we will return the object itself next time, abstracting the code as follows:
let obj;
if(! obj){ obj="xxx"
}
Copy the code
We have taken the logic of how to manage singletons out of the original code and wrapped it inside the getSingle function, passing the object creation method fn as an argument, as follows:
function getSingle(fn){
let result;
return function(){
return result || (result=fn.apply(this.arguments));
}
Copy the code
After optimization, the completion code is as follows:
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button type="button" id="login">The login button</button>
<script type="text/javascript">
function getSingle(fn){
let result;
return function(){
return result || (result=fn.apply(this.arguments)); }}function createLoginLayer() {
div = document.createElement('div');
div.innerHTML = "I'm a login popover.";
div.style.display = "none";
document.body.appendChild(div);
return div;
};
let createSingleLayer=getSingle(createLoginLayer)
document.getElementById("login").onclick = function() {
let loginLayer=createSingleLayer();
loginLayer.style.display = "block";
}
</script>
</body>
</html>
Copy the code
In the above code, we create an instance object and the duty of the duties of managing the singleton respectively in two different ways, these two methods are independent of each other, each other, when they were together, completes a create singleton object function, and the two functions can also be used alone, significantly increase the code reusability.
summary
Singleton pattern is the first pattern we learned, we first learned the traditional singleton pattern implementation, but also learned that because of language differences, there are more suitable methods to create singleton in JS. This chapter also addresses the agency model and the single responsibility principle.
In the getSingle function, the concept of closures and higher-order functions is actually mentioned as well. The singleton pattern is a simple but very useful pattern, especially the lazy singleton technique of creating objects only when appropriate and only one. Even more wonderful, the responsibility for creating objects and managing singletons is split between two different methods, which together have the power of the singleton pattern.
5. Strategy mode
Definition of a policy pattern: Define a series of algorithms, encapsulate them one by one, and make them interchangeable.
Interchangeover: This statement is very much relative to statically typed languages. Because of type checking in statically typed languages, each policy class needs to implement the same interface. They are interchangeable only when their true type is hidden behind the interface. In JS, a “type-fuzzy” language, there is no such problem, and any object can be used instead. Thus, interchangeable uses in JS appear to have the same purpose and intent. (Concepts may not be easy to understand at first, but after looking at specific examples, go back and read them again.)
5.1 Use the policy mode to calculate the bonus
Assuming that the person performing at S gets 4 times his salary, the person performing at A gets 3 times his salary, and the person performing at B gets 2 times his salary, then we will write A code to calculate the employee’s annual bonus. The code is as follows:
function calculateBonus(level,salary){
if(level==='S') {return salary*4
}
if(level==='A') {return salary*3
}
if(level==='B') {return salary*2}}console.log(calculateBonus('B'.3000));/ / output 6000
console.log(calculateBonus('S'.6000));/ / output 24000
Copy the code
The code above defect is very obvious, is the function within the if branch is overmuch, function lack of flexibility, once the rules or modify performance increase performance, it is necessary to penetrate to achieve function, in violation of the code of the open closed principle, and the code reusability is very poor, also once used in another place, can only copy paste.
We can use composition functions to refactor the above code:
function levelS(salary){
return salary*4;
}
function levelA(salary){
return salary*3;
}
function levelB(salary){
return salary*2;
}
function calculateBonus(level,salary){
if(level==='S') {return levelS(salary);
}
if(level==='A') {return levelA(salary);
}
if(level==='B') {returnlevelB(salary); }}console.log(calculateBonus('B'.3000));/ / output 6000
console.log(calculateBonus('S'.6000));/ / output 24000
Copy the code
We encapsulate all kinds of algorithms to a small function, and has good naming these small functions, can clearly know what kind of algorithm, it corresponds to the they can be reused in other parts of the program, although the code has improved, but the calculateBonus function may be bigger and bigger, and lack of elasticity in the system change.
The next step is to use the strategy pattern to refactor the code. The whole idea is to separate the use of the algorithm from the implementation of the algorithm. A strategy pattern consists of at least two parts. The first part is a set of policy classes, which encapsulate the specific algorithm and are responsible for the specific calculation process. The second part is the environment class, which accepts the client’s request and then delegates it to a policy class.
// Define the policy class
function levelS(){}
levelS.prototype.calculate=function(salary){
return salary*4;
}
function levelA(){}
levelA.prototype.calculate=function(salary){
return salary*3;
}
function levelB(){}
levelB.prototype.calculate=function(salary){
return salary*2;
}
// Define the bonus class
function Bonus(){
this.salary=null;/ / salary
this.strategy=null;// The policy object corresponding to the performance level
}
Bonus.prototype.setSalary=function(salary){
this.salary=salary;// Set employee salaries
}
Bonus.prototype.setStrategy=function(strategy){
this.strategy=strategy;// Set the measurement object corresponding to the employee performance level
}
Bonus.prototype.getBonus=function(){// Get the bonus amount
return this.strategy.calculate(this.salary);// Delegate the calculation of bonuses to the corresponding policy object
}
let bonus=new Bonus();
bonus.setSalary(1000);
bonus.setStrategy(new levelS());
console.log(bonus.getBonus());/ / output 4000
bonus.setStrategy(new levelB());
console.log(bonus.getBonus());/ / output 2000
Copy the code
The basic idea of the code above is to define a series of algorithms and encapsulate each of them into a policy class. The algorithms are encapsulated in methods inside the policy class. When a client initiates a request, the request is always delegated to one of these policy objects for calculation.
5.2JavaScript version of the policy mode
The previous section simulated some traditional object-oriented language implementations. In fact, there is a better way to do this in JS.
// Define policy objects (functions are also objects in JS)
let strategies={
S(salary){
return salary*4;
},
A(salary){
return salary*3;
},
B(salary){
return salary*2; }}// Define the environment object
function calculateBonus(level,salary){
return strategies[level](salary)
}
console.log(calculateBonus('S'.1000));/ / output 4000
console.log(calculateBonus('B'.1000));/ / output 2000
Copy the code
5.3 Embodiment of polymorphism in policy mode
By using policy pattern code refactoring, we eliminated a large number of conditional branch statements from the original program. All the logic associated with calculating bonuses is no longer in the environment object, but is distributed among the policy objects. The environment object does not have the ability to calculate bonuses but delegates this responsibility to a policy object. The algorithm responsible for each policy object has been encapsulated within the object. When we ask these policy objects to calculate bonuses, they will return different results, which is the object polymorphism and the purpose of “they can be interchangeable”. By replacing the currently saved policy object in the environment object, we can perform different algorithms to get the result we want.