[Issue 767] You don’t understand JS: mixing (obfuscating) objects of “class”

image

Preface

It’s Friday again. Is this week a bit dizzy when I read this series of articles? If you have a book, you will feel a lot, you may not be able to finish it, but you may not feel it if you read one article every day. Too much, what do you think? Today, the front-end morning reading class columnist @HetfieldJoe will continue to share the serial "You Don't Know JS".


The text starts here~


You don’t understand JS: this and object prototypes Chapter 4: Mixing (obfuscating) "class" objects


Following our exploration of objects in the previous chapter, we naturally shifted our attention to "object-oriented (OO) programming" and "class". Let's take "class-oriented" as a design pattern first, and then we will examine the mechanism of "class": "instantiation", "inheritance" and "relative polymorphism".


As we will see, these concepts do not naturally map to the object mechanism of JS, and the efforts (mixins, etc.) made by many JavaScript developers to overcome these challenges.


Note: This chapter spent a considerable amount of time (the first half!) focusing on explaining the theory of "object-oriented programming". When discussing "Mixins" in the second half, we will eventually connect these theories with real and actual JavaScript code. But there are a lot of concepts and hypothetical codes to wade through here first, so don't get lost-stick with it!


Class theory

"Class/Inheritance" describes a specific form of code organization and structure-a way of modeling the real world in our software.


OO or class-oriented programming emphasizes the inherent connection between data and the behavior of operating it (of course, it varies according to the type and nature of the data!), so a reasonable design is to pack data and behavior together (also known as Package). This is sometimes called "data structure" in formal computer science.


For example, a series of characters representing a word or phrase is usually called a "string". These characters are data. But you almost never care about the data, you always want to do things with the data, so the actions that can be implemented to the data (calculate its length, add data to the end, retrieve, etc.) are all designed as methods of the String class.


Any given string is an instance of this class, this class is a neat set of packaging: character data and the functions we can perform on it.


The class also implies a classification method for a specific data structure. The way we do this is to consider a given structure as a more generalized type of basic definition.


Let us explore this classification process through a most frequently cited example. A car can be described as a concrete realization of a "class" of more general things-vehicles.


We model this relationship in the software by defining Vehicle and Car classes.


The definition of Vehicle may include things like power (engine, etc.), manned capacity, etc. These are all behaviors. What we define in Vehicle is what all (or most) different types of vehicles (aircraft, trains, motor vehicles) have in common.


It may not make sense to redefine the basic nature of "manned capacity" again and again for each different type of vehicle in our software. Instead, we define this capability once in Vehicle, and then when we define Car, we simply point out that it "inherits" (or "extends") from the basic Vehicle definition. The definition of Car is a specialization of the general definition of Vehicle.


Although Vehicle and Car intensively define behavior in the form of methods, the data in an instance belongs to a specific car like a unique license plate number.


In this way, classes, inheritance, and instantiation are born.


Another key concept about classes is "polymorphism", which describes the idea that a generalized behavior from a parent class can be overridden by a subclass, making it more specific. In fact, relative polymorphism allows us to refer to the underlying behavior in the coverage behavior.


Class theory strongly recommends that the parent class and the subclass share the same method name for the same behavior, so that the subclass (differentially) overwrites the parent class. As we are about to see, doing so in your JavaScript code can lead to all kinds of difficult and fragile code.


"Class" design pattern

You may never consider classes as a "design pattern", because the most common discussion is about popular "object-oriented design patterns", such as "Iterator", "Observer", "Factory", "Singleton" and so on. When expressed in this way, it can almost be assumed that OO classes are the underlying mechanism for us to implement all (high-level) design patterns, as if OO is a given foundation for all code.


Depending on the level of formal education you have received in programming, you may have heard of "procedural programming": a type that does not require any high-level abstraction, but is composed of procedures (that is, functions) calling other functions. How to describe the code. You may have been told that classes are an appropriate way to transform procedural style "noodle code" into well-structured, well-organized code.


Of course, if you have "functional programming" experience, you probably know that a class is just one of several common design patterns. But for others, this may be the first time you ask yourself whether classes are really the fundamental foundation of the code, or are they selective abstractions at the top level of the code.


Some languages ​​(such as Java) don't give you a choice, so there is no choice at all-everything is a class. Other languages ​​such as C/C++ or PHP give you both procedural and class-oriented grammars. Developers are left with more choices on which style is appropriate or mixed.


JavaScript "classes"

Which side does JavaScript belong to on this issue? JS has some class-like syntax elements (such as new and instanceof) for a while, and in recent ES6, there are some additional ones, such as the class keyword (see Appendix A).


But does this mean that JavaScript actually owns classes? Straightforward and simple: no.


Since classes are a design pattern, you can, with considerable effort (we will see in the rest of this chapter), approximate the functions of many classic classes. JS is striving to satisfy the extremely wide-ranging desire to design with classes by providing syntax that looks like classes.


Although we seem to have a grammar that looks like a class, it seems that the JavaScript mechanism is resisting your use of the class design pattern, because at the bottom, the mechanisms you are working on work very differently. Syntactic sugar and the (extremely widely used) JS "Class" library took a lot of effort to hide these truths from you, but sooner or later you will face the reality: the classes you encounter in other languages ​​are the same as you The "class" simulated in JS is different.


All in all, class is an optional mode in software design, you can choose to use it or not in JavaScript. Because many developers have a special liking for class-oriented software design, we will explore in the rest of this chapter, what is the cost to maintain the illusion of classes using the things provided by JS, and the pain we have experienced.


Class mechanism

In many class-oriented languages, the "standard library" provides a data structure called "stack" (push, pop, etc.), represented by a Stack class. This class has a set of variables to store data, and a set of publicly accessible behaviors ("methods") that enable your code to interact with (hidden) data (add or remove data, etc.).


But in such a language, you are not directly operating on Stack (unless you make a static class member reference, but this is beyond the scope of our discussion). The Stack class is just an abstract explanation of what any "stack" will do, but it is not a "stack" itself. In order to get a real data structure that can be manipulated, you must instantiate the Stack class.


building

The traditional analogy between "class" and "instance" is derived from the construction of buildings.


An architect will plan out all the properties of a building: how wide, how high, how many windows are there, and even what materials to use for walls and ceilings. At this time, she doesn't care where the building will be built, and she doesn't care how many copies of this building will be built.


At the same time, she doesn't care about the contents of the building—furniture, wallpaper, ceiling fans, etc.—she only cares about the structure of the building.


The architectural blueprints she produced are merely "plans" of the building. They do not actually constitute a building into which we can actually enter and sit down. We need a builder for this task. The builders took the plans and built the building exactly according to them. In a real sense, he is copying the intended nature of the plan into the physical building.


Once completed, this building is a physical instance of the blueprint plan, a copy that is expected to be a perfect substance. Then the builder can move to the next door and redo it again, creating another copy.


The relationship between the building and the blueprint is indirect. You can view the blueprints to understand how the building is constructed, but for directly examining each part of the building, the blueprint alone is not enough. If you want to open a door, you have to walk into the building itself-the blueprint is just a line drawn on paper to indicate the location of the door.


A class is a blueprint. In order to actually get an object and interact with it, we must build (that is, instantiate) something from the class. The final result of this "construction" is an object, typically called an "instance", we can directly call its methods as needed to access its public data attributes.


This object is a copy of all the characteristics described in the class.


You don’t expect to walk into a building and find that a blueprint for planning this building is framed and hung on the wall, although the blueprint may be in the office’s public records. Similarly, you generally don't use object instances to directly access and manipulate classes, but this is at least possible for determining which class the object instance comes from.


Rather than considering any indirect relationship between an object instance and the class from which it originated, it is more useful to consider the direct relationship between a class and an object instance. A class is instantiated into the form of an object through a copy operation.

image


As you can see, the arrow goes from left to right and top to bottom, which represents the conceptual and physical copy operation.


Constructor

An instance of a class is constructed by a special method of the class. The name of this method is usually the same as the class name and is called a "constructor". The clear job of this method is to initialize all the information (state) needed for the instance.


For example, consider the following hypothetical code (the syntax is self-created):

image

In order to create a CoolGuy instance, we need to call the constructor of the class:

image

Note that the CoolGuy class has a constructor CoolGuy(), which is actually called when we say new CoolGuy(..). We get an object (an instance of the class) from this constructor, and we can call the showOff() method to print the special talent of this particular CoolGuy.


Obviously, skipping rope makes Joe look cool.


The constructor of a class belongs to that class, almost always with the same name as the class. At the same time, the constructor always needs to be called with new in most cases, so that the language engine knows that you want to build an instance of a new class.


Class Inheritance

In a class-oriented language, not only can you define a class that can initialize itself, you can also define another class to inherit from the first class.


This second class is usually called the "child class", and the first class is called the "parent class". These terms obviously come from the comparison of parent-child relationship, although this comparison is a bit distorted, as you will see soon.


When a parent has a child who is related to him, the genetic nature of the parent will be copied to the child. Obviously, in most biological reproduction systems, both parents equally contribute genes to mix. But for the purpose of this comparison, we assume there is only one relative.


Once the child appears, he or she is separated from the relatives. This child is heavily influenced by the inheritance of his relatives, but is unique. If the child has red hair, it does not mean that his relatives’ hair was once red or will automatically turn red.


In a similar way, once a subclass is defined, it is separated and distinguished from the parent class. The subclass contains an initial copy of the behavior from the parent class, but it can override these inherited behaviors and even define new behaviors.


It is important to remember that we are talking about parent classes and subclasses, not physical things. This is what makes this parent-child comparison confusing, because we should actually say that the parent is the DNA of the family, and the child is the DNA of the child. We have to create (that is, initialize) people from two sets of DNA, and use the obtained physical people to talk to them.


Let's put the biological parent-child aside and look at inheritance from a slightly different perspective: different types of vehicles. This is the most classic (and controversial) analogy used to understand inheritance.


Let us revisit the discussion of Vehicle and Car earlier in this chapter. Consider the following hypothetical code expressing inherited classes:

image.png


Note: For brevity and clarity, the constructors of these classes have been omitted.


We define the Vehicle class, assuming it has an engine, a method to turn on the lighter, and a method to drive. But you will never create a generalized "vehicle", so here it is just a conceptual abstraction.


Then we defined two specific vehicles: Car and SpeedBoat. They all inherit the generalization properties of Vehicle, but then they all appropriately specialize these properties. A car has 4 wheels, and a speedboat has two engines, which means it needs to pay special attention to starting two engines when it starts a fire.


Polymorphism

Car defines its own drive() method, which covers the method of the same name inherited from Vehicle. However, Car's drive() method calls inherited:drive(), which means that Car can refer to its inherited and overwrite the previous original drive(). The pilot() method of SpeedBoat also references the drive() copy it inherits.


This technique is called "polymorphism" or "virtual polymorphism". To be more specific about our current situation, we call it "relative polymorphism."


The topic of polymorphism is much broader than what we can talk about here, but our current "relative" means a special level: any method can refer to other methods that are higher in the inheritance hierarchy (same name or Different names). We say "relative" because we don't absolutely define which level of inheritance (that is, class) we want to visit, but essentially say "up one level" to refer to it relatively.


In many languages, the super keyword is used where inherited: is used in this example, which is based on the idea that a "super class" is the parent/ancestor of the current class.


Another aspect of polymorphism is that a method name can have multiple definitions at different levels of the inheritance chain, and these definitions can be automatically selected appropriately when resolving which method is called.


In our example above, we saw this behavior happen twice: drive() is defined in Vehicle and Car, and ignition() is defined in Vehicle and SpeedBoat.


Note: Another traditional class-oriented language gives you the ability through super is to directly access the parent class constructor from the subclass constructor. This is largely true, because for the real class, the constructor belongs to this class. In JS, however, this is the opposite-in fact, it is more appropriate to think that the "class" belongs to the constructor (Foo.prototype...type reference). Because in JS, the parent-child relationship only exists between the two .prototype objects of their respective constructors, the constructors themselves are not directly related, and there is no easy way to refer to one from the other (see Appendix A, see ES6 Use super to "solve" this problem in the class).


An interesting meaning of polymorphism can be seen specifically from ignition(). Inside pilot(), a relatively polymorphic reference points to the (inherited) Vehicle version of drive(). And this drive() only refers to the ignition() method by name (not a relative reference).


Which version of ignition() will the language engine use? Is it Vehicle or SpeedBoat? It will use the SpeedBoat version of ignition(). If you can initialize the Vehicle class itself and call its drive(), then the language engine will use the Vehicle's ignition() definition.


In other words, the definition of the ignition() method is polymorphic (changes) according to which class (inheritance level) the instance you refer to is.


This seems too deep into academic details. But in order to compare the similar behavior of JavaScript's [[Prototype]] mechanism, it is important to understand these details.


If the classes are inherited, there is a method for these classes themselves (not the objects created by them) to refer to the objects they inherit from, and this relative reference is usually called super.


Remember this picture just now:

image.png


Note how the arrows represent copy operations for instantiation (a1, a2, b1, and b2) and inheritance (Bar).


Conceptually, it seems that the subclass Bar can use relative polymorphic references (that is, super) to access the behavior of its parent class Foo. However, in reality, the subclass is just given a copy of the behavior it inherits from the parent class. If a subclass "overrides" a method it inherits, both the original method and the overridden method actually exist, so they are all accessible.


Don't let polymorphism confuse you and make you think that subclasses are linked to parent classes. The child class gets a copy of what it needs to inherit from the parent class. Class inheritance means copying.


Multiple Inheritance

Can you recall the parent-child and DNA we mentioned earlier? We said that this comparison is a bit strange, because most offspring in biology come from both parents. If the class can inherit from the other two classes, then this parent-child comparison would be more appropriate.


Some class-oriented languages ​​allow you to specify more than one "parent class" for "inheritance". Multiple inheritance means that the definition of each parent class is copied to the child class.


On the surface, this is a powerful addition to class-oriented, giving us the ability to combine more functions. However, this will undoubtedly create some complex problems. If the two parent classes provide a method named drive(), which version of the drive() reference in the subclass will be resolved? Do you always have to manually specify which parent class's drive() is what you want, thus losing some of the elegance of polymorphic inheritance?


There is another so-called "diamond problem": the subclass "D" inherits from two parent classes ("B" and "C"), and both of them inherit from the common parent class "A". If "A" provides the method drive(), and both "B" and "C" cover (polymorphically) this method, then when "D" refers to drive(), which version should it use (B: drive() or C:drive())?

image.png


Things will be much more complicated than what we can see in a glimpse. We write them down here so that we can compare it with how the JavaScript mechanism works.


JavaScript is simpler: it does not provide a native mechanism for "multiple inheritance". Many people think this is a good thing, because the elimination of complexity is much more than the "reduce" function. But this does not stop developers from using various methods to simulate it, let's take a look next.


Mixins

When you "inherit" or "instantiate", JavaScript's object mechanism does not automatically perform the copy behavior. Quite simply, there are no "classes" in JavaScript that can be instantiated, only objects. And objects will not be copied to another object, but will be linked together (see Chapter 5 for details).


Because the behavior of classes observed in other languages ​​implies copying, let us see how JS developers can simulate the copying behavior of this missing class in JavaScript: mixins. We will see two kinds of "mixins": explicit and implicit.


Explicit Mixins (Explicit Mixins)

Let us review the previous Vehicle and Car examples again. Because JavaScript does not automatically copy behavior from Vehicle to Car, we can build a tool to copy manually. Such tools are often referred to as extend(..) by many packages/frameworks, but for illustrative purposes, we call it mixin(..) here.

image.png


Note: Important details: we are no longer talking about classes, because there are no classes in JavaScript. Vehicle and Car are just the source and target objects of our copy.


Car now has a copy of the attributes and functions obtained from Vehicle. Technically, the function is not actually copied, but the reference to the function is copied. So, Car now has a property called ignition, which is a copy of the ignition() function reference; and it also has a property called engines, which holds the value 1 copied from Vehicle.


Car already has the drive attribute (function), so this attribute reference has not been overwritten (see the if statement of mixin(..) above).


Revisit "Polymorphism"

Let's examine this statement: Vehicle.drive.call( this ). I call it "explicit pseudo-polymorphism". Recall that this line of our previous hypothetical code is the inherited:drive() we call "relative polymorphism".


JavaScript is not capable of achieving relative polymorphism (before ES6, see Appendix A). So, because both Car and Vehicle have a function called drive(), in order to call them differently, we must use absolute (not relative) references. We clearly indicate the Vehicle object by name, and then call the drive() function on it.


But if we say Vehicle.drive(), then the this binding of this function call will be the Vehicle object, not the Car object (see Chapter 2), which is not what we want. Therefore, we use .call(this) (see Chapter 2) to ensure that drive() is executed in the context of the Car object.


Note: If the function name identifier of Car.drive() does not overlap with Vehicle.drive() (that is, "masking"; see Chapter 5), we will not have the opportunity to demonstrate "method polymorphism" ". Because in that case, a reference to Vehicle.drive() will be copied by the mixin(..) call, and we can access it directly using this.drive(). The overlap of the chosen identifiers is why we have to use more complex explicit pseudo-polymorphisms.


In a relatively polymorphic class-oriented language, the connection between Car and Vehicle is established once, at the top of the class definition. This is the only place to maintain this relationship.


But due to the particularity of JavaScript, explicit hypothetical polymorphism (because of obscuration!) creates a fragile manual/explicit link in every function that you need to reference such (hypothetical) polymorphism. This may significantly increase maintenance costs. Moreover, although explicit hypothetical polymorphism can simulate the behavior of "multiple inheritance", it will only increase complexity and code fragility.


The result of this approach is usually more complex, harder to read, and harder to maintain code. The use of explicit hypothetical polymorphism should be avoided as much as possible, because it costs more than benefits at most levels.


Mixing Copies

Recall the mixin(..) tool above:

image.png


Now, let's examine how mixin(..) works. It iterates all the attributes of sourceObj (Vehicle in our example), and if there is no attribute with a matching name in targetObj (Car in our example), it copies it. Because we are copying when the original object exists, we must be careful not to overwrite the target attribute.


If we make a copy before specifying the specific content of Car, then we can omit the targetObj check, but this is a bit clumsy and inefficient, so it is usually not preferred:

image.png


Either way, we explicitly copy the non-overlapping content in Vehicle to Car. The name "mixin" comes from another way of explaining this task: Car mixes the contents of Vehicle, just like you mix chocolate chips into your favorite cookie dough.


The result of this copy operation is that Car will run independently of Vehicle. If you add attributes to Car, it will not affect Vehicle, and vice versa.


Note: There are a few small details that have been overlooked here. There are still some subtle ways that two objects can "influence" each other after the copy is completed, for example, they share a reference to a common object (such as an array).


Since two objects also share references to their common functions, this means that even if you manually copy (that is, mix in) functions from one object to another, you cannot actually simulate the subclasses that occur in a class-oriented language Real copy to the instance.


JavaScript functions cannot be copied in a true sense (in a standard, reliable way), so what you end up with is a copied reference to the same shared function object (functions are objects; see Chapter 3). For example, if you add properties to a shared function object (such as ignition()) to modify it, both Vehicle and Car will be affected by the shared reference.


A clear mixin in JavaScript is a good mechanism. But they seem to be exaggerated. Compared to defining an attribute twice, copying attributes from one object to another does not yield much practical benefit. This is especially true for the subtle changes we just mentioned to add function object references.


If you explicitly mix two or more objects into your target object, you can simulate the behavior of "multiple inheritance" to some extent, but when copying methods or properties from more than one source object, there is no direct The method can resolve the name conflict. Some developers/packages use "late binding" and other weird alternatives to solve the problem, but fundamentally speaking, these "tricks" usually outweigh the gain (and inefficient!).


Be careful, only use it when a clear mixin actually improves the readability of the code, and if you find that it makes the code more difficult to trace, or creates unnecessary or cumbersome dependencies between objects, Avoid using this mode.


If the correct use of mixin makes your problem more difficult than before, then you should probably stop using mixin. In fact, if you have to use complex packages/tools to deal with these details, it may indicate that you are on a more difficult and perhaps unnecessary path. In Chapter 6, we will try to extract a simpler way to achieve our desired results while avoiding these twists and turns.


Parasitic Inheritance

A variant of the explicit mixin model, which is explicit in a certain sense and implicit in a certain sense, is called "Parasitic Inheritance", which is mainly promoted by Douglas Crockford.


This is how it works:

image.png


As you can see, we first made a copy of the definition from the "parent class" (object) Vehicle, then we mixed our "subclass" (object) definition into it (retaining the parent class reference as needed), and finally The combined object car is passed as a subclass instance.


Note: When we call new Car(), a new object is created and referenced by Car's this (see Chapter 2). But since we didn't use this object, but returned our own car object, the object created by this initialization was discarded. Therefore, Car() can be called without the new keyword to achieve the same function as the above code, and it can also save the creation and recycling of objects.


Implicit Mixin (Implicit Mixins)

The implicit mixin is closely related to the explicit hypothetical polymorphism explained earlier. So they need to pay attention to the same things.


Consider this code:

image


Something.cool.call( this) can be used either in the "constructor" call (the most common case), or in the method call (as shown here), we essentially "borrow" Something.cool( ) Function and call it in Another environment instead of Something environment (through this binding, see Chapter 2). As a result, the assignment made in Something.cool() is implemented on the Another object instead of the Something object.


So, this means that we "mixed" Something's behavior with Another.


Although this technique seems to effectively use the function of this rebinding, that is, call Something.cool.call( this) bluntly, but this kind of call cannot be used as a relative (and more flexible) reference, so you should improve alert. In general, try to avoid using this structure to keep the code clean and easy to maintain.


review

Class is a design pattern. Many languages ​​provide syntax to enable natural class-oriented software design. JS also has a similar syntax, but its behavior is very different from what you are familiar with in other languages.


Class means copy.


When a traditional class is instantiated, the behavior of the class is copied to the instance. When the class is inherited, the behavior of the parent class is copied to the child class.


Polymorphism (having different functions with the same name at different levels of the inheritance chain) may seem to mean a relative reference link from the child class back to the parent class, but it is still just the result of copying the line.


JavaScript does not automatically (like classes) create copies between objects.


The mixin pattern is often used to simulate the copy behavior of a class to some extent, but this usually leads to an ugly and fragile syntax like explicit hypothetical polymorphism (OtherObj.methodName.call(this, ...)). Often leads to more difficult to understand and more difficult to maintain code.


Explicit mixin and class copy are not exactly the same, because objects (and functions!) are just shared references being copied, not objects/functions themselves. Not paying attention to such subtleties is usually the source of various traps.


Generally speaking, simulating classes in JS usually lays more pits than solving the current real problems.


Guess you like

Origin blog.51cto.com/15080028/2595042