JavaScript Jems - Objects Are Anonymous Singletons |
Written by Mike James | |||||
Monday, 05 February 2024 | |||||
Page 3 of 4
Everything Is A SingletonAs JavaScript isn't class-based and as objects are always dynamic, you really have to think of every object as a singleton. Clearly objects have similarities, but you cannot rely on any pair of objects being identical as you can in a class-based language. JavaScript objects are mutable unless you go out of your way to make them immutable. This is often the biggest shortcoming claimed for JavaScript and yet class-based languages have their problems. The key advantage of most class-based languages is that when an object is created from a class is it immutable in the sense that it can't have methods and properties added or removed. Thus, if you know an object is of a particular class, you can be sure of what methods and properties it has - but how do you know what an object's class is? That is, if you have a reference stored in myObject, how can you know the class of the object referenced and hence deduce that it is safe to call myMethod? The usual answer is strong typing. We have already encountered the idea of strong typing in connection with the type hierarchy and inheritance in Jem 2, but there the emphasis was on the type of the object. Here it is the type of the variable that is all important. As already explained, in strong typing every variable has to have a stated class or type that it can reference. To make things more flexible, a variable is allowed to reference not only its declared class, but any class that inherits from that class, i.e. any subclass. The reasoning is that the derived classes have all of the methods and properties of the original class and so it is safe to use those methods and properties. This is hierarchical typing and it has already been described in connection with inheritance. In this case the focus is on checking that instances have the properties they should have at compile time. This is all very reasonable and very attractive, but notice that it only works if the instances that are being assigned to the strongly-typed variables are known at compile time. If they are known then you can discover what properties and methods they have, even in a dynamic language like JavaScript. Strong typing catches errors, but only very trivial errors that are easy enough to find without accepting the restrictions and complications it introduces. The two biggest complications are generics and variance. GenericsStrong typing means that you cannot write generic algorithms. A generic algorithm is one that applies to a wide range of object types. For example, you might implement a sort algorithm that can sort any objects, no matter what type, as long as you can supply a comparison function. There are two general ways of creating a generic method in a strongly-typed language. The first is that you can use the root of the type hierarchy, usually Object, to create variables that can reference any object type. In most languages a variable of type Object can reference any other type in the type hierarchy - as they are all subtypes of Object. In this sense Object acts as a completely general "untyped" reference. Except, of course, as the declared type is Object, you can't access any methods or properties of most of the objects it references unless you use a cast. So it isn't quite as powerful as an untyped reference. The alternative is that you can use the language's generic typing facilities, if it has any. This essentially allows you to specify type as additional parameters. Consider: T add <T>(T a,T b){return a+b}; where <T> is a parameter that specifies the type of the object being used. When you use the generic method you have to specify the value of the type parameter. For example: int c=add<int>(1,2); uses the generic method with T set to int. That is, the function declaration is: int add(int a,int b){return a+b}; Of the two methods, generics is better because it typechecks the method in the form in which you are using it. However, it is more complicated and it also has its limitations and complications in most implementations. Of course, if you drop typing then every function you write can be generic. Things switch around in this case and the problem becomes not making functions work with a wide range of objects, but restricting the range that they work with. In fact, all JavaScript functions and methods are generic. VarianceVariance is an advanced idea, but one that comes about quite naturally if you work with strong typing. We can extend the idea of subtype by simply turning the definition around. If you can use type B in place of type A then you can consider B to be a subtype of A or A>B. For example, a real or floating-point type can be used in place of an integer as you can always write an integer with a zero decimal part, so int>float. Using this idea we can ask if one data structure based on a type is a subtype of one based on another type. For example, is an array of floats a subtype of an array of ints or vice versa. This is where the question becomes subtle. Consider the action of a set on an array element. You can obviously set an integer into a floating point array, but you can't set a float into an integer array without potentially losing the decimal part. So for a set action you can substitute a float array for an int array and hence int[]>float[] and a float array is a subtype of an int array. Now consider a get action on the same pair of arrays. In this case the get has to return an int from the integer array and so you cannot use a floating point array in its place because get could return a non-integer. However, you can use an integer array in place of a float as its values just happen to have a zero decimal part. So, from the point of view of get, we have float[]<int[]. To summarize: int > float and for set: int[] > float[] but for get: int[] < float[] We say that for set the array is covariant as the subtype relationship is the same, but for get the array is contravariant because the relationship is reversed. What does this mean? It means that if you are storing values in an int array then strong typing should allow you to use a float array in its place and if you are retrieving values from a float array you can use an int array in its place. If your reaction that this is quite mad - because you nearly always save and store values to an array - you would be quite right and Java, for example, a strongly-typed language, makes its arrays invariant, i.e. neither covariant or contravariant. In this case you can't use an array of a different type at all, even if it would work for some reason. The way that in this discussion we have been using arrays generalizes to more complex data structures and to functions. In general, values that act as inputs are covariant and those that act as outputs are contravariant. These ideas are particularly important for parameters and return values in functions. Now compare this complex approach to JavaScript's. As variables aren't typed there is no sense in which variance even arises as a concept. You can replace a data structure with any other because data structures aren't typed. To be more clear, all objects are equivalent and all algorithms are generic. When you write a function and use that function it is up to you to work out what you expect the objects involved in the transaction to conform to, and it is up to you to make sure that they do. With JavaScript you can avoid the straitjacket of strong typing and apply controls to your objects that suit the purpose and this is a jem. |
|||||
Last Updated ( Monday, 05 February 2024 ) |