JavaScript as an Expressive Language and its Flexibility

JavaScript as an Expressive Language and its Flexibility

JavaScript is a widely used language that has achieved immense popularity worldwide. Its wide distribution is attributed to its integration into modern browsers, making it an essential component of our daily lives. It powers the websites we visit and enables the creation of dynamic interfaces on the Web. Despite its significance, some individuals still regard it as a trivial language that needs more professionalism associated with programming. This perception arises from a need for more awareness of the language's full capabilities and unique features in today's programming world. JavaScript stands out as a highly expressive language that includes several unconventional features not commonly found in the C family of languages. This article explores some of these features, highlighting how the language allows multiple approaches to achieve the same task and how it incorporates functional programming concepts that offer an alternative approach to object-oriented programming.

The Flexibility of JavaScript

JavaScript's flexibility is one of its most remarkable attributes. As a JavaScript programmer, you have the freedom to make your programs as simple or complex as you desire. The language also supports several programming styles, including functional and object-oriented styles, and it is possible to write relatively intricate programs without any knowledge. Although this is why some people may view JavaScript as a trivial language, it is an advantage because it enables programmers to complete valuable tasks using a small, easy-to-learn subset of the language. Additionally, as you become a more advanced programmer, JavaScript scales up to accommodate your needs.

JavaScript enables you to emulate patterns and idioms from other languages and even create some of your own. It offers all the object-oriented features present in more conventional server-side languages. To illustrate, let's consider different methods for organizing code to initiate and terminate an animation.

At this point, you can view this as an example of the various techniques JavaScript employs to accomplish a task.

/* Start and stop animations using functions. */
 function startAnimation() { ... }

 function stopAnimation() { ... }

This approach is straightforward, but it doesn’t allow you to create animation objects, which can store states and have methods that act only on this internal state. This next piece of code defines a class that lets you create such objects:

/* Anim class. */
let Anim = function() { ... }; 
Anim.prototype.start = function() { ... }; 
Anim.prototype.stop = function() { ... }; 

/* Usage. */ 
let myAnim = new Anim(); 

myAnim.start(); 
myAnim.stop();

This defines a new class called Anim and assigns two methods to the class’s prototype property. If you prefer to create classes encapsulated in one declaration, you might instead write the following:

This may look a little more familiar to classical object-oriented programmers who are used to seeing a class declaration with the method declarations nested within it. If you’ve used this style before, you might want to give this next example a try. Again, don’t worry if there are parts of the code you don’t understand:

/* Add a method to the Function object that can be used to declare methods. */
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn;
};
/* Anim class, with methods created using a convenience method. */
var Anim = function() {...};
Anim.method('start', function() {...});
Anim.method('stop', function() {...});

Function.prototype.method allows you to add new methods to classes. It takes two arguments. The first is a string to use as the name of the new method, and the second is a function that will be added under that name. You can take this a step further by modifying Function.prototype.method to allow it to be chained. To do this, you simply return this after creating each method.

/* This version allows the calls to be chained. */
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn;
    return this;
};
/* Anim class, with methods created using a convenience method and chaining. */
var Anim = function() {...};
Anim.
method('start', function() {...}).
method('stop', function() {...});

You have just seen five different ways to accomplish the same task, each using a slightly different style. Depending on your background, you may find one more appealing than another. This is fine; JavaScript allows you to work in the style that is most appropriate for the project at hand. Each style has different characteristics with respect to code size, efficiency, and performance.

A Loosely Typed Language

JavaScript is a fun-loving language that likes to keep things loose! Unlike other languages, you don't have to declare a type when defining a variable but don't worry, and variables are still typed based on what data they contain. There are three primitive types - booleans, numbers, and strings - but what's unique is that JavaScript treats integers and floats as the same type!

There are functions, like little bundles of executable code, and objects, which are fancy composite datatypes (arrays are just a special type of object that holds a bunch of values in a specific order).

Now, let's talk about null and undefined - they're the wallflowers of the datatype world, but they are still important. Primitive data types are passed by value, while all other datatypes are passed by reference. So if you're not careful, you might run into some unexpected side effects!

Here's where things get really interesting - in JavaScript, a variable can change its type depending on what value is assigned to it! It's like the chameleon of the programming world. And if you need to cast a datatype from one type to another, JavaScript has got you covered - use the toString method to convert a number or boolean to a string or the parseFloat and parseInt functions to convert strings to numbers. And if you want to cast a string or number to a boolean, double negate it! Easy peasy, lemon squeezy!

let bool = !!num;

JavaScript is the ultimate wingman - it's all about being loose and flexible! You don't have to stress about type errors with loosely typed variables because JavaScript can convert types as needed. Talk about a smooth operator!

But wait, there's more! In JavaScript, functions are first-class objects - they're like the Beyoncé of the programming world. You can store them in variables, pass them around as arguments, return them as values, and even create them at run-time. This gives you tons of flexibility and expressiveness when it comes to dealing with functions.

And if you're feeling extra adventurous, you can even create anonymous functions using the function() { ... } syntax. They're like the undercover agents of the function world - no name, no problem! Just assign them to a variable and let them work their magic. Here's an example:

/* An anonymous function, executed immediately. */
(function() {
   const foo = 10;
   const bar = 2;
   alert(foo * bar);
})();

This function is defined and executed without ever being assigned to a variable. The pair of parentheses at the end of the declaration execute the function immediately. They are empty here, but that doesn’t have to be the case:

/* An anonymous function with arguments. */
(function(foo, bar) {
    alert(foo * bar);
})(10, 2);

This anonymous function is equivalent to the first one. Instead of using const to declare the inner variables, you can pass them in as arguments. You can also return a value from this function. This value can be assigned to a variable:

/* An anonymous function that returns a value. */
const baz = (function(foo, bar) {
    return foo * bar;
})(10, 2);
// baz will equal 20.

The most interesting use of the anonymous function is to create closure. A closure is a protected variable space created by using nested functions. JavaScript has function-level scope. This means that a variable defined within a function is not accessible outside of it. JavaScript is also lexically scoped, which means that functions run in the scope they are defined in, not the scope they are executed in. These two facts can be combined to allow you to protect variables by wrapping them in an anonymous function. You can use this to create private variables for classes:

/* An anonymous function used as a closure. */
let baz;
(function() {
    let foo = 10;
    let bar = 2;
    baz = function() {
    return foo * bar;
};
})();

baz(); // baz can access foo and bar, even though it is executed outside of the // anonymous function.

The variables foo and bar are defined only within the anonymous function. Because the function baz was defined within that closure, it will have access to those two variables even after the closure has finished executing.

The Mutability of Objects

In JavaScript, everything is an object (except for the three primitive datatypes, and even they are automatically wrapped with objects when needed). Furthermore, all objects are mutable. These two facts mean you can use some techniques that wouldn’t be allowed in most other languages, such as giving attributes to functions:

function displayError(message) {
    displayError.numTimesExecuted++;
    alert(message);
};
displayError.numTimesExecuted = 0;

/* It also means you can modify classes after they have been defined and objects after they
have been instantiated: */

/* Class Person. */
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype = {
  getName: function() {
  return this.name;
},
  getAge: function() {
   return this.age;
  }
}
/* Instantiate the class. */
let alice = new Person('Alice', 93);
let bill = new Person('Bill', 30);
/* Modify the class. */
Person.prototype.getGreeting = function() {
    return 'Hi ' + this.getName() + '!';
};
/* Modify a specific instance. */
alice.displayGreeting = function() {
    alert(this.getGreeting());
}

In this example, the getGreeting method is added to the class after the two instances are created, but these two instances still get the method due to the way the prototype object works. alice also gets the displayGreeting method, but no other instance does.

Related to object mutability is the concept of introspection. You can examine any object at run-time to see what attributes and methods it contains. You can also use this information to instantiate classes and execute methods dynamically without knowing their names at development time (this is known as reflection). These are important techniques for dynamic scripting and are features that static languages (such as C++) lack.

In JavaScript, everything can be modified at run-time.

This is an enormously powerful tool and allows you to do things that are not possible in those other languages. It does have a downside, though. It isn’t possible to define a class with a particular set of methods and be sure that those methods are still intact later on. This is part of the reason why type-checking is done so rarely in JavaScript.

Inheritance

Inheritance is more complex in JavaScript than in other object-oriented languages. JavaScript uses object-based (prototypal) inheritance; this can be used to emulate class-based (classical) inheritance. You can use either style in your code, and we cover both styles in this book. Often one of the two will better suit the particular task at hand. Each style also has different performance characteristics, which can be an essential factor in deciding which to use.

The fact that JavaScript is so expressive allows you to be very creative in how design patterns are applied to your code. There are three main reasons why you would want to use design patterns in JavaScript:

1. Maintainability: Design patterns help to keep your modules more loosely coupled. This makes it easier to refactor your code and swap out different modules. It also makes it easier to work in large teams and to collaborate with other programmers.

2. Communication: Design patterns provide a common vocabulary for dealing with different types of objects. They give programmers shorthand for describing how their systems work. Instead of long explanations, you can just say, “It uses the factory pattern.” The fact that a particular pattern has a name means you can discuss it at a high level without having to get into the details.

3. Performance: Some of the patterns we cover in this book are optimization patterns. They can drastically improve the speed at which your program runs and reduce the amount of code you need to transmit to the client.

Summary

The expressiveness of JavaScript provides an enormous amount of power. Even though the language lacks certain useful built-in features, its flexibility allows you to add them yourself.

You can write code to accomplish a task in many different ways, depending on your background and personal preferences. JavaScript is loosely typed; programmers do not declare a type when defining a variable. Functions are first-class objects and can be created dynamically, which allows you to create closures. All objects and classes are mutable and can be modified at run-time. You can use two styles of inheritance, prototypal and classical, and each has its own strengths and weaknesses.