Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
The principles of object-oriented javascript by nicholas c. zakas

The principles of Object-Oriented Javascript

TYPES

JS uses two kinds of types: primitive and reference. Primitive types are stored as simple data types. Reference types are stored as objects, which are really just references to locations in memory.

Primitive Types

There are five primitive types in JavaScript:

  1. Boolean true or false
  2. Number Any integer or floating-point numeric value
  3. String A character or sequence of characters delimited by either single or double quotes (JavaScript has no separate character type)
  4. Null A primitive type that has only one value, null
  5. Undefined A primitive type that has only one value, undefined (undefined is the value assigned to a variable that is not initialized) In JavaScript, a variable holding a primitive directly contains the primitive value (rather than a pointer to an object). When you assign a primitive value to a variable, the value is copied into that variable. This means that if you set one variable equal to another, each variable gets its own copy of the data.

The best way to identify primitive types is with the typeof operator, which works on any variable and returns a string indicating the type of data. The typeof operator works well with strings, numbers, Booleans, and undefined. The tricky part involves null. When you run typeof null, the result is "object". The best way to determine if a value is null is to compare it against null directly. Despite the fact that they’re primitive types, strings, numbers, and Booleans actually have methods. (The null and undefined types have no methods.) Strings, in particular, have numerous methods to help you work with them.

Reference Types

Reference values are instances of reference types and are synonymous with objects. An object is an unordered list of properties consisting of a name (always a string) and a value. When the value of a property is a function, it is called a method. Functions themselves are actually reference values in JavaScript, so there’s little difference between a property that contains an array and one that contains a function except that a function can be executed. There are a couple of ways to create, or instantiate, objects.

  • New: The first is to use the new operator with a constructor. By convention, constructors in JavaScript begin with a capital letter to distinguish them from nonconstructor functions.
  • Pointer means that if you assign one variable to another, each variable gets a copy of the pointer, and both still reference the same object in memory.
  • Dereferencing Objects: JavaScript is a garbage-collected language, so you don’t really need to worry about memory allocations when you use reference types. However, it’s best to dereference objects that you no longer need so that the garbage collector can free up that memory. The best way to do this is to set the object variable to null.

Adding or Removing Properties

Another interesting aspect of objects in JavaScript is that you can add and remove properties at any time

The built-in types are:

  • Array
  • Date
  • Error
  • Function
  • Object
  • RegExp
  • Literal Forms

A literal is syntax that allows you to define a reference value without explicitly creating an object, using the new operator and the object’s constructor. To create an object with object literal syntax, you can define the properties of a new object inside braces. Properties are made up of an identifier or string, a colon, and a value, with multiple properties separated by commas. You almost always define functions using their literal form. JavaScript also has regular expression literals that allow you to define regular expressions without using the RegExp constructor. The pattern is contained between two slashes, and any additional options are single characters following the second slash. Dot notation is the most common way to access properties in JavaScript, but you can also access properties on JavaScript objects by using bracket notation with a string.

Primitive wrapper Types

There are three primitive wrapper types (String, Number, and Boolean). The primitive wrapper types are reference types that are automatically created behind the scenes whenever strings, numbers, or Booleans are read (this process is called autoboxing). An object is always considered true inside a conditional statement.

Functions

There are actually two literal forms of functions. The first is a function declaration, which begins with the function keyword, The second form is a function expression, which doesn’t require a name after function. These functions are considered anonymous because the function object itself has no name. Instead, function expressions are typically referenced via a variable or property. Function declarations are hoisted to the top of the context. For instance, you can pass a function into another function as an argument. You can pass any number of parameters to any function without causing an error. The number of arguments a function expects is stored on the function’s length property. The length property indicates the function’s arity, or the number of parameters it expects.

Overloading

Most object-oriented languages support function overloading, which is the ability of a single function to have multiple signatures. JavaScript functions can accept any number of parameters, and the types of parameters a function takes aren’t specified at all. That means JavaScript functions don’t actually have signatures. A lack of function signatures also means a lack of function overloading.

Object methods

The syntax for a data property and a method is exactly the same

The this Object

Every scope in JavaScript has a this object that represents the calling object for the function. In the global scope, this represents the global object (window in web browsers). When a function is called while attached to an object, the value of this is equal to that object by default. There are three function methods that allow you to change the value of this. The first parameter of call() is the value to which this should be equal when the function is executed. All subsequent parameters are the parameters that should be passed into the function. The apply() method works exactly the same as call() except that it accepts only two parameters: the value for this and an array or array-like object of parameters to pass to the function (that means you can use an arguments object as the second parameter). bind()

Understanding objects

Defining Properties

When a property is first added to an object javascript creates an own property which simply indicates that the specific instance of the object owns that property. The property is stored directly on the instance, and all operations on the property must be performed through that object.

Detecting Properties

The if condition evaluates to true if the value is truthy (an object, a nonempty string, a nonzero number, or true) and evaluates to false if the value is falsy (null, undefined, 0, false, NaN, or an empty string).

The in operator looks for a property with a given name in a specific object and returns true if it finds it. In effect, the in operator checks to see if the given key exists in the hash table. The in operator checks for both own properties and prototype properties. The hasOwnProperty() method, which is present on all objects and returns true only if the given property exists and is an own property.

Removing Properties

The delete operator works on a single object property removing a key/value pair from a hash table. When the delete operator is successful, it returns true.

Enumeration

By default, all properties that you add to an object are enumerable, which means that you can iterate over them using a for-in loop. ate on an array of property names and for-in when you don’t need an array. There is a difference between the enumerable properties returned in a for-in loop and the ones returned by Object.keys(). The for-in loop also enumerates prototype properties, while Object.keys() returns only own (instance) properties. You can check whether a property is enumerable by using the propertyIsEnumerable() method.

Types of Properties

There are two different types of properties: data properties and accessor properties. Data properties contain a value. Accessor properties don’t contain a value but instead define a function to call when the property is read (called a getter), and a function to call when the property is written to (called a setter). Accessor properties are most useful when you want the assignment of a value to trigger some sort of behavior, or when reading a value requires the calculation of the desired return value. You don’t need to define both a getter and a setter; you can choose one or both. If you define only a getter, then the property becomes read-only, and attempts to write to it will fail silently in nonstrict mode and throw an error in strict mode. If you define only a setter, then the property becomes write-only, and attempts to read the value will fail silently in both strict and nonstrict modes.

Property attributes

There are two property attributes shared between data and accessor properties. One is [[Enumerable]], which determines whether you can iterate over the property. The other is [[Configurable]], which determines whether the property can be changed. You can remove a configurable property using delete and can change its attributes at any time. By default, all properties you declare on an object are both enumerable and configurable. If you want to change property attributes, you can use the Object .defineProperty() method. This method accepts three arguments: the object that owns the property, the property name, and a property descriptor object containing the attributes to set. When you are defining a new property with Object.defineProperty(), it’s important to specify all of the attributes because Boolean attributes automatically default to false otherwise. If you need to fetch property attributes, you can do so in Java Script by using Object.getOwnPropertyDescriptor(). If the property exists, you should receive a descriptor object with four properties: configurable, enumerable, and the two others appropriate for the type of property.

Preventing object modification

Objects, just like properties, have internal attributes that govern their behavior. All objects you create are extensible by default, meaning new properties can be added to the object at any time. you can prevent new properties from being added to an object. There are three different ways to accomplish this.

Preventing Extensions

One way to create a nonextensible object is with Object.preventExtensions(). You can check the value of [[Extensible]] by using Object.isExtensible()

Sealing Objects

The second way to create a nonextensible object is to seal the object. A sealed object is nonextensible, and all of its properties are nonconfigurable. If an object is sealed, you can only read from and write to its properties.

Freezing Objects

The last way to create a nonextensible object is to freeze it. If an object is frozen, you can’t add or remove properties, you can’t change properties’ types, and you can’t write to any data properties. In essence, a frozen object is a sealed object where data properties are also read-only.

Constructors and prototypes

Constructors

A constructor is simply a function that is used with new to create an object. Because a constructor is just a function, you define it in the same way. The only difference is that constructor names should begin with a capital letter, to distinguish them from other functions. Even though this relationship exists between an instance and its constructor, you are still advised to use instanceof to check the type of an instance. This is because the constructor property can be overwritten and therefore may not be completely accurate. You could also use Object.defineProperty() inside of a constructor to help initialize the instance Make sure to always call constructors with new; otherwise, you risk to change the global object instead of the newly created object. Constructors allow you to configure object instances with the same properties, but constructors alone don’t eliminate code redundancy, that means if you have 100 instances of an object, then there are 100 copies of a function that do the exact same thing, just with different data. It would be much more efficient if all of the instances shared one method. This is where prototypes come in.

Prototypes

You can think of a prototype as a recipe for an object. That prototype is shared among all of the object instances, and those instances can access properties of the prototype. An instance keeps track of its prototype through an internal property called [[Prototype]]. This property is a pointer back to the prototype object that the instance is using which can reduce code duplication. When a property is read on an object, the JavaScript engine first looks for an own property with that name. If the engine finds a correctly named own property, it returns that value. If no own property with that name exists on the target object, JavaScript searches the [[Prototype]] object instead. If a prototype property with that name exists, the value of that property is returned. If the search concludes without finding a property with the correct name, undefined is returned. The own property shadows the prototype property It’s much more efficient to put the methods on the prototype and then use this to access the current instance. You can also store other types of data on the prototype, but be careful when using reference values. Because these values are shared across instances, you might not expect one instance to be able to change values that another instance will access. The constructor property is specifically assigned on the prototype. It’s good practice to make this the first property on the prototype so you don’t forget to include it. Perhaps the most interesting aspect of the relationships among constructors, prototypes, and instances is that there is no direct link between the instance and the constructor. There is, however, a direct link between the instance and the prototype and between the prototype and the constructor. Because all instances of a particular type reference a shared prototype, you can augment all of those objects together at any time. That means you can literally add new members to a prototype at any point and have those changes reflected on existing instances. The ability to modify the prototype at any time has some interesting repercussions for sealed and frozen objects. When you use Object.seal() or Object.freeze() on an object, you are acting solely on the object instance and the own properties. You can’t add new own properties or change existing own properties on frozen objects, but you can certainly still add properties on the prototype and continue extending those objects.

Built-in Object Prototypes

At this point, you might wonder if prototypes also allow you to modify the built-in objects that come standard in the JavaScript engine. The answer is yes. All built-in objects have constructors, and therefore, they have proto types that you can change. While it may be fun and interesting to modify built-in objects to experiment with functionality, it’s not a good idea to do so in a production environment. Developers expect built-in objects to behave a certain way and have certain methods. Deliberately altering built-in objects violates those expectations and makes other developers unsure how the objects should work.

Inheritance

The object instances inherit properties from the prototype.Because the prototype is also an object, it has its own prototype and inherits properties from that. This is the prototype chain. All objects inherit from Object.prototype.

Methods Inherited from Object.prototype

  • hasOwnProperty() Determines whether an own property with the given name exists
  • propertyIsEnumerable() Determines whether an own property is enumerable
  • isPrototypeOf() Determines whether the object is the prototype of another
  • valueOf() Returns the value representation of the object
  • toString() Returns a string representation of the object

valueOf()

The valueOf() method gets called whenever an operator is used on an object. By default, valueOf() simply returns the object instance. The primitive wrapper types override valueOf() so that it returns a string for String, a Boolean for Boolean, and a number for Number. Likewise, the Date object’s valueOf() method returns the epoch time in milliseconds.

toString()

The toString() method is called as a fallback whenever valueOf() returns a reference value instead of a primitive value. It is also implicitly called on primitive values whenever JavaScript is expecting a string

All objects inherit from Object.prototype by default, so changes to Object.prototype affect all objects. The Object.create() method accepts two arguments. The first argument is the object to use for [[Prototype]] in the new object. The optional second argument is an object of property descriptors in the same format used by Object.defineProperties() You can also create objects with a null [[Prototype]] via Object.create(). Because the prototype property is writable, you can change the prototype chain by overwriting it. Always make sure that you overwrite the prototype before adding properties to it, or you will lose the added methods when the overwrite happens. YourConstructor is a subtype of Object, and Object is a supertype of YourConstructor.

Constructor stealing

Call the supertype constructor from the subtype constructor using either call() or apply() to pass in the newly created object. In effect, you’re stealing the supertype constructor for your own object, This two-step process is useful when you need to accomplish inheritance between custom types. You’ll always need to modify a constructor’s prototype, and you may also need to call the supertype constructor from within the subtype constructor. Generally, you’ll modify the prototype for method inheritance and use constructor stealing for properties. This approach is typically referred to as pseudoclassical inheritance because it mimics classical inheritance from class-based languages. You can directly access the method on the supertype’s prototype and use either call() or apply() to execute the method on the subtype object.

Private and privileged members

The Module Pattern

The basic approach is to use an immediately invoked function expression (IIFE) that returns an object. An IIFE is a function expression that is defined and then called immediately to produce a result. That function expression can contain any number of local variables that aren’t accessible from outside that function. Methods that access private data in this way are called privileged methods. The module pattern allows you to use regular variables as de facto object properties that aren’t exposed publicly. You accomplish this by creating closure functions as object methods. Closures are simply functions that access data outside their own scope. There is a variation of the module pattern called the revealing module pattern, which arranges all variables and methods at the top of the IIFE and simply assigns them to the returned object.

Private members for constructors

You can use a pattern that’s similar to the module pattern inside the constructor to create instance-specific private data. Placing methods on an object instance is less efficient than doing so on the prototype, but this is the only approach possible when you want private, instance-specific data.

Mixins

The first object (a receiver) actually receives the properties of the second object (the supplier) by copying those properties directly. Keep in mind that this is a shallow copy, so if a property contains an object, then both the supplier and the receiver will be pointing to the same object. This pattern is used frequently for adding new behaviors to JavaScript objects that already exist on other objects. Mixins are a powerful way to add functionality to objects while avoiding inheritance. A mixin copies properties from one object to another so that the receiving object gains functionality without inheriting from the supplying object. Unlike inheritance, mixins do not allow you to identify where the capabilities came from after the object is created. For this reason, mixins are best used with data properties or small pieces of functionality. Inheritance is still preferable when you want to obtain more functionality and know where that functionality came from.

Scope-safe constructors

A scope-safe constructor can be called with or without new and returns the same type of object in either case. When new is called with a function, the newly created object represented by this is already an instance of the custom type represented by the constructor. So you can use instanceof to determine whether new was used in the function call. JavaScript itself has several reference types with scope-safe constructors, such as Object, Array, RegExp, and Error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.