[class and module] class and type

Classes and types

JavaScript defines a small number of data types: null, undefined, Boolean, number, string, function, and object. The typeof operator yields the type of the value. However, we often prefer to treat classes as types, so that objects can be distinguished according to the class to which they belong. The built-in objects in the core of JavaScript language (usually the host object of client JavaScript) can be distinguished from each other according to their class attributes.

instanceof operator

Instanceof operator, the left operand is the object of the class to be detected, and the right operand is the constructor that defines the class. If o inherits from c.prototype, the expression o instanceof c is true. The inheritance here can not be direct inheritance. If o the inherited object inherits from another object and the latter object inherits from c.prototype, the operation result of this expression is also true.

The constructor is the public identity of the class, but the prototype is the unique identity. Although the right operand of the instanceof operator is a constructor, the calculation process actually detects the inheritance relationship of the object, not the constructor that creates the object.

If you want to detect whether there is a specific prototype object in the prototype chain of an object, is there a method that does not use a constructor as an intermediary? The answer is yes, you can use the isPrototypeOf() method. For example, you can detect whether the object r is a member of the scope class through the following code:

range.methods.isPrototypeOf(r);     // range.method is the prototype object

The disadvantage of the instanceof operator and isPrototypeOf() method is that we cannot obtain the class name from the object, but can only detect whether the object belongs to the specified class name. There is also a serious deficiency in client-side JavaScript, which is poor compatibility in Web applications with multi window and multi frame pages. Each window and shelf page has a separate execution context, and each context contains a unique global variable and a set of constructors. The two arrays created in two different frame pages inherit from two identical but independent prototype objects. The array in one frame page is not an instance of the Array() constructor of the other frame page, and the instanceof operation result is false.

constructor property

Another way to identify whether an object belongs to a class is to use the constructor property. Because the constructor is the public identifier of the class, the most direct method is to use the constructor attribute, such as:

function typeAndValue(x) {
    if (x == null) return "";                          // NUll and undefined have no constructors
    switch(x.constructor) {
        case Number: return "Number: " + x;            // Process original type
        case String: return "String: '" + x + "'";      
        case Date: return "Date: " + x;                // Processing built-in functions
        case RegExp: return "Regexp: " + x; 
        case Complex: return "Complex: " + x;          // Processing custom types
    }
}

It should be noted that the expressions after the keyword case in the code are functions. If you use the typeof operator or get the class attribute of the object, they should be changed to strings.

Using the constructor attribute to detect that an object belongs to a class has the same shortcomings as instanceof. In the scenario of multiple execution contexts, it does not work properly (such as in multiple frame pages of the browser window). In this case, each frame page has its own set of constructors. The Array constructor of one frame page and the Array constructor of another frame page are not the same constructor.

Similarly, not all objects in JavaScript contain the constructor attribute. There will be a constructor attribute on each newly created function prototype by default, but we often miss the constructor attribute on the prototype.

The name of the constructor

There is a major problem in using instanceof operator and constructor attribute to detect the class to which the object belongs. When there are multiple copies of constructor in multiple execution contexts, the detection results of these two methods will make errors. Functions as like as two peas in multiple execution contexts are identical, but they are separate objects, so they are not equal to each other.

One possible solution is to use the name of the constructor instead of the constructor itself as the class identifier. The Array constructor in one window is not equal to the Array constructor in another window, but their names are the same. In some JavaScript implementations, a non-standard attribute name is provided for the function object to represent the name of the function. For JavaScript implementations that do not have the name attribute, you can convert the function to a string and extract the function name from it.

The type() function defined in the following example returns the type of the object as a string. It uses the typeof operator to handle raw values and functions. For an object, it returns either the value of the class property or the name of the constructor.

Example: you can judge the type of value type()function
/**
 * Type returned o as string:
 *  -If o is null, return "null"; If o is nan, "nan" is returned
 *  -If the value returned by typeof is not "object", this value is returned
 *  (Note that there are some JavaScript implementations that recognize regular expressions as functions)
 *  - If o's class is not "Object", this value is returned
 *  - If o contains a constructor and the constructor has a name, this name is returned
 *  - Otherwise, "Object" will be returned
 **/
function type(o) {
    var t, c, n; // type, class, name
    // Special case for handling null values
    if (o === null) return "null";
    // Another special case: NaN is not equal to itself
    if (o !== o) return "nan";

    // If the value of typeof is not "object", this value is used
    // This identifies the type and function of the original value
    if ((t = typeof o) !== "object") return t;

    // Returns the class name of the Object, unless the value is "Object"
    // Most built-in objects can be identified in this way
    if ((c = classof(o)) !== "Object") return c;

    // Returns the name of the object constructor if it exists
    if (o.constructor && typeof o.constructor === "function" &&
       (n = o.constructor.getName())) return n;

    // Other types cannot be distinguished, and "Object" is returned 
    return "Object";
}

// Returns the class of the object
function classof(o) {
    return Object.prototype.toString.call(o).slice(8, -1);
};

// Returns the name of the function (possibly an empty string). If it is not a function, it returns null
Function.prototype.getName = function () {
    if ("name" in this) return this.name;
    return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};

This method of using constructor names to identify object classes has the same problem as using constructor properties: not all objects have constructor properties. In addition, not all functions have names. If you define a constructor using a function definition expression without a name, the getName() method returns an empty string:

// This constructor has no name
var Complex = function(x,y) { this.r = x; this.i = y; }
// This constructor has a name
var Range = function Range(f,t) { this.from = f; this.to = t; }

Duck argument

The various techniques described above for detecting classes of objects are somewhat problematic, at least in client-side JavaScript. The solution is to avoid these problems: don't focus on "what the class of the object is", but on "what the object can do". This way of thinking is very common in Python and Ruby and is called "duck debate" (this expression was put forward by author James Whitcomb Riley).

A bird that walks, swims and quacks like a duck is a duck.

For JavaScript programmers, this sentence can be understood as "if an object can walk, swim and quack like a duck, it is considered to be a duck, even if it does not inherit from the prototype object of the duck class".

The implementation method of duck argument makes people feel too "laissez faire": it is only assumed that the input object implements the necessary method, and no further inspection is performed at all. If the input object does not follow "what if", the code will report an error when trying to call methods that do not exist. Another implementation method is to check the input object. Instead of checking their classes, check the methods they implement with appropriate names. In this way, illegal input can be intercepted as early as possible, and error reports with more prompt information can be given.

In the following example, the quacks() function is defined according to the concept of duck argument (the function name "implements" would be more appropriate, but implements is a reserved word). quacks() is used to check whether an object (the first argument) implements the method represented by the remaining parameters. For each parameter except the first parameter, if it is a string, directly check whether there is a method named after it; If it is an object, check whether the method in the first object also has a method with the same name in this object; If the parameter is a function, it is assumed to be a constructor, and the function will check whether the method implemented by the first object also has a method with the same name in the prototype object of the constructor.

Example: functions implemented by duck argument
//Returns true if o implements the method represented by a parameter other than the first parameter 
function quacks(o /*, ... */) {
	for (var i = 1; i < arguments.length; i++) {  		
	// All parameters after traversal o
		var arg = arguments[i];
		switch (typeof arg) {	    // If the parameter is:
			case 'string':	        // string: check directly by name
				if (typeof o[arg] !== "function") return false;
				continue;
			case 'function':	    // Function: check the method on the prototype object of the function
  // If the argument is a function, its prototype is used
  			arg = arg.prototype;        // Go to the next case
			case 'object':	        // object: check matching method
				for (var m in arg) {        // Traverses the object, not each property
					if (typeof arg[m] !== "function") continue; // Skip properties that are not methods 
					if (typeof o[m] !== "function") return false;
				}
			}
		}

		// If the program can be executed here, it shows that o has implemented all the methods
		return true;
}

There are a few things you should pay special attention to about this quacks() function. First of all, we only detect whether the object contains one or more properties whose value is a function by a specific name. We can't know the details of these existing attributes. For example, what is the function for? How many parameters do they need? What is the parameter type? However, this is the essence of duck argument. If you use duck argument instead of forced type detection to define the API, the created API should be more flexible, so as to ensure that the API you provide to users is more secure and reliable. Another problem with the quacks() function is that it cannot be applied to built-in classes. For example, quacks(o,Array) cannot be used to detect whether o implements all methods with the same name in Array. The reason is that the methods of built-in classes are not enumerable, and the for/in loop in quacks() cannot traverse them (note that there is a remedy in ECMAScript 5, which is to use ojbeet.getOwnPropertyNames()).

Tags: Javascript Front-end

Posted on Thu, 02 Dec 2021 17:53:43 -0500 by doogles