Although JavaScript is inherently casual, as browsers are able to accomplish more and more things, the language is becoming more and more serious. Under complex logic, JavaScript needs to be modularized, and modules need to be encapsulated, leaving only interfaces for external calls. Closure is the key to module encapsulation in JavaScript, and it is also a point that many beginners find difficult to understand. At first, I was confused. Now, I am confident that I have a deeper understanding of this concept. In order to facilitate understanding, the article attempts to encapsulate a relatively simple object.
We are trying to maintain a counter object ticker on the page. This object maintains a value n. As the user operates, we can increment the count (add 1 to the value n), but cannot decrement n or change n directly. Moreover, we need to query this value from time to time.
JSON style modularization of the open door
A way to open the door is:
var ticker = {
n:0,
tick:function(){
this.n++;
},
};
This way of writing is natural and effective. When we need to increase the count, we call the ticker.tick() method. When we need to query the number of times, we access the ticker.n variable. But its shortcomings are also obvious: users of the module are allowed to change n freely, such as calling ticker.n-- or ticker.n=-1. We have not encapsulated ticker. n and tick() appear to be "members" of ticker, but their accessibility is the same as ticker, and they are global (if ticker is a global variable). In terms of encapsulation, this modular approach is only a little bit better than the more ridiculous approach below (although for some simple applications, this little bit is enough).
var ticker = {};
var tickerN = 0;
var tickerTick = function(){
tickerN++;
}
tickerTick();
It is worth noting that in tick(), I access this .n - This is not because n is a member of ticker, but because it is ticker that calls tick(). In fact, it would be better to write ticker.n here, because if tick() is called, it is not ticker, but something else, such as:
var func = ticker.tick;
func();
At this time, call tick( ) is actually window, and when the function is executed, it will try to access window.n and an error will occur.
In fact, this "open door" modular approach is often used to organize JSON-style data rather than programs. For example, we can pass the following JSON object to a function of ticker to determine that ticker starts counting from 100 and advances by 2 each time.
var config = {
nStart:100,
step:2
}
Scope chain and closure
Look at the code below. Note that we have already implemented the customization of ticker by passing in config.
function ticker(config){
var n = config.nStart;
function tick(){
n += config.step;
}
}
console.log(ticker.n); // ->undefined
You may be wondering, why did ticker change from an object to a function? This is because only functions have scope in JavaScript, and variables inside the function cannot be accessed from outside the function body. Accessing ticker.n outside ticker() will result in undefined , but accessing n within tick() will have no problem. From tick() to ticker() to the global, this is the "scope chain" in JavaScript.
But there is still a problem, that is - how to call tick()? The scope of ticker() also covers tick(). There are two solutions:
1) You will need to call a method as the return value, just as we use the method of incrementing n as the return value of ticker();
2) Set the variables of the outer scope, just as we did in ticker() ) in getN.
var getN;
function ticker(config){
var n = config.nStart;
getN = function(){
return n;
};
return function(){
n += config.step;
};
}
var tick = ticker({nStart:100,step:2});
tick();
console.log(getN()); // ->102
Please see, at this time, the variable n It is in a "closure" and cannot be accessed directly outside ticker(), but it can be observed or manipulated through two methods.
In the first piece of code in this section, after the ticker() method is executed, n and tick() are destroyed until the next time the function is called; but in the second piece of code, ticker() is executed Afterwards, n will not be destroyed because tick() and getN() may access it or change it, and the browser will be responsible for maintaining n. My understanding of "closure" is: a mechanism used to ensure that n, a variable that is within the scope of a function, needs to be maintained after the function is executed, and may be accessed through other methods, is not destroyed.
But, I still feel something is wrong? What if I need to maintain two objects ticker1 and ticker2 with the same functionality? There is only one ticker(), so we can't write it again, right?
new operator and constructor
If you call a function through the new operator, a new object will be created and the function will be called using that object. In my understanding, the construction process of t1 and t2 in the following code is the same.
function myClass(){}
var t1 = new myClass();
var t2 = {};
t2.func = myClass;
t2.func();
t2.func = undefined;
t1 and t2 both is a newly constructed object, and myClass() is the constructor. Similarly, ticker() can be rewritten as
function TICKER(config){
var n = config.nStart;
this.getN = function(){
return n;
};
this.tick = function(){
n += config.step;
}
}
var ticker1 = new TICKER({nStart:100,step:2});
ticker1.tick();
console.log(ticker1.getN()); // ->102
var ticker2 = new TICKER({nStart:20,step:3});
ticker2.tick();
ticker2.tick();
console.log(ticker2.getN()); // ->26
Usually , the constructor is in uppercase letters. Note that TICKER() is still a function, not a pure object (the reason why we say "pure" is that functions are actually objects, and TICKER() is a function object). The closure is still valid and we cannot access ticker1.n .
Prototype prototype and inheritance
The above TICKER() still has flaws, that is, ticker1.tick() and ticker2.tick() are independent of each other! Please see, every time you use the new operator to call TICKER(), a new object will be generated and a new function will be bound to this new object. Every time a new object is constructed, the browser will open up a space. Storing tick() itself and variables within tick() is not what we expect. We expect ticker1.tick and ticker2.tick to point to the same function object.
This requires the introduction of prototypes.
In JavaScript, except for Object objects, other objects have a prototype property, which points to another object. This "another object" still has its prototype object and forms a prototype chain, which ultimately points to the Object object. When calling a method on an object, if it is found that the object does not have a specified method, then search for this method on the prototype chain until the Object object.
Functions are also objects, so functions also have prototype objects. When a function is declared (that is, when the function object is defined), a new object is generated, the prototype property of this object points to the Object object, and the constructor property of this object points to the function object.
The prototype of a new object constructed through a constructor points to the prototype object of the constructor. So we can add functions to the prototype object of the constructor, and these functions will not depend on ticker1 or ticker2, but on TICKER.
You might do this:
function TICKER(config){
var n = config.nStart;
}
TICKER.prototype.getN = function{
// attention: invalid implementation
return n;
};
TICKER.prototype.tick = function{
// attention: invalid implementation
n += config.step;
};
Please note that this is an invalid implementation. Because the methods of the prototype object cannot access the contents of the closure, that is, the variable n. After the TICK() method is run, n can no longer be accessed, and the browser will destroy n. In order to access the contents of the closure, the object must have some concise instance-dependent methods to access the contents of the closure, and then define complex public methods on its prototype to implement the logic. In fact, the tick() method in the example is concise enough, let's put it back into TICKER. Next implement a more complex method tickTimes() that will allow the caller to specify the number of times tick() is called.
function TICKER(config){
var n = config.nStart;
this.getN = function(){
return n;
};
this.tick = function(){
n += config.step;
} ;
}
TICKER.prototype.tickTimes = function(n){
while(n>0){
this.tick();
n--;
}
};
var ticker1 = new TICKER({nStart: 100,step:2});
ticker1.tick();
console.log(ticker1.getN()); // ->102
var ticker2 = new TICKER({nStart:20,step:3}) ;
ticker2.tickTimes(2);
console.log(ticker2.getN()); // ->26
This TICKER is very good. It encapsulates n and cannot directly change it from outside the object, and the complex function tickTimes() is defined on the prototype. This function operates on the data in the object by calling small functions of the instance.
So, in order to maintain the encapsulation of the object, my suggestion is to decouple the data operations into the smallest unit function possible, and define it in the constructor as instance-dependent (also called "private" in many places) "), and implement complex logic on the prototype (that is, "public").
Finally, let me say something about inheritance. In fact, when we define a function on the prototype, we are already using inheritance! Inheritance in JavaScript is more...well...simple, or crude, than in C++. In C++, we may define an animal class to represent an animal, and then define a bird class to inherit the animal class to represent a bird, but what I want to discuss is not such inheritance (although such inheritance can also be implemented in JavaScript); I want to The discussion of inheritance in C++ would be to define an animal class and then instantiate a myAnimal object. Yes, this is instantiation in C++, but is treated as inheritance in JavaScript.
JavaScript does not support classes. The browser only cares about the current objects and does not bother to worry about what classes these objects are and what structure they should have. In our example, TICKER() is a function object. We can assign a value to it (TICKER=1) and delete it (TICKER=undefined). However, because there are currently two objects, ticker1 and ticker2, through the new operator When it is called, TICKER() acts as a constructor, and the TICKER.prototype object acts as a class.