JavaScript object model in one line of code

Level: introductory

Take a look at the following JS code snippet:

  1..toString(); // "1"

Being small, this one-liner describes many major topics from the mysterious JavaScript language, which we’re about to cover in today’s note.

Note: if you’d like to take a deeper dive into ECMAScript ecosystem, you may consider the JavaScript. The Core article.

Parsing lookaheads

Let’s start from the weird .. double-dots method access. Is it a parse error?

Actually, the opposite. The parse error would be had we only one dot:

  1.toString(); // Syntax Error: Invalid or unexpected token

The reason is, ECMAScript numbers are based on the floating point IEEE 754 format, which means:

1 === 1.0; // true

The fractional part can be omitted, and due to JS parsing grammar, the dot followed immediately after a number is considered the fractional part delimiter:

1 === 1.; // true

The integer part can also be omitted:

.1 === 0.1; // true

(Wait, what’s the === triple-equal? — Go here)

So the parser looks ahead for a dot following a number and treats it as the fractional part delimiter.

And the next dot after the fractional part is already toString property access on the number object, hence the weird “double-dots method call” in this case:

  1..toString(); // "1"

If the fractional part is explicitly defined, the parser doesn’t fail:

1.5.toString(); // "1.5"

Takeaway: this is purely ECMAScript design flaw in the parsing grammar. For example, the issue doesn’t exist in other languages, such as Rust, where we can normally write the:

1.to_string();   // "1"

1.5.to_string(); // "1.5"

Beside the (mainly academical interest for the) double-dots case, the JS parse error can be bypassed by some other syntactic constructs, the most practical of which is extracting raw numbers into constants:

(1).toString();   // "1"

const RATIO = 1;
RATIO.toString(); // "1"

Including even weirder cases of:

1['toString']();  // "1"

1.['toString'](); // "1"

Property access: dot and square brackets

Yes, JavaScript has dot . and square bracket [] property access. The former is used when the property or method name is a valid identifier and is known before runtime. The later can be used for array access, when the property name is not a valid identifier, when a property is a symbol, or when it’s dynamic:

const data = [1, 2, 3];

// data.0;   // error, not a valid identifier

data[0];     // 1
data['0'];   // 1

const k = 0; 

data.k;      // undefined, "k" name doesn't exist
data[k];     // 1, OK dynamic access

Takeaway: JavaScript doesn’t distinguish property access and “subscript” operators as some other languages do (e.g. Ruby). Both, the dot and the square bracket notation, are just property access and resolve to the same toString method in our initial example — just inside the square brackets can be any expression (variables, strings, function calls), which evaluate to a property name.

An arrow function is created, immediately invoked, returning the 'toString' value as the property name:

1[(() => 'toString')()](); // "1"

However, this article is not about parsing issues in JS, rather about other (hidden from the first glance) ECMAScript internals, so let’s get down to them.

Primitives and Objects

“Everything in JS is an object.”wrong.

The fact we are able to access the toString method directly from the 1 literal value, may mistakenly bring us to quick conclusions that “everything in JS is an object”.

JS however distinguish primitive values and objects, and the only reason why we can access methods from primitives is the temporary objects wrapping. In other languages the process is also known as “auto-boxing”, i.e. a boxed (heap-allocated) value of some class is created, and can now provide access to the methods of this class.

Our initial example is equivalent to:

1..toString(); // "1"

// Same as:

const __temp = new Number(1);
__temp.toString(); // "1"

// later __temp is destroyed by GC

Yes, every time we access methods or properties on primitives, a temporary wrapper object is created, and which is then destroyed by a garbage collector.

This can be demonstrated on the following example:

const n = 1;

n.toString(); // yay, everything in JS is an object!

n.prop = 2; // wow, can even create new properties!

n.prop; // wait, where is it? - undefined

Knowing about the auto-boxing process, we can describe what’s happened using the following example:

const n = 1;

const __temp1 = new Number(n);
__temp1.toString(); // access toString on __temp1

const __temp2 = new Number(n);
__temp2.prop = 2; // create prop on __temp2

const __temp3 = new Number(n);
__temp3.prop; // access prop on __temp3, but it's not here

Observing the Number constructor is called three times, it might be tempting to create an actual object only once, for the performance optimizations:

const RATIO = new Number(1);
RATIO.toString(); // "1"

However, JavaScript VMs usually may optimize such common cases and avoid allocating the wrappers. So in general try to avoid premature micro-optimizations and leave it to JS internals.

This example though with methods and objects takes us deeper to JS world of Object-oriented programming, classes, prototype and inheritance. Let’s take a look.

Dynamic objects

The line from the example above:

const __temp2 = new Number(n);
__temp2.prop = 2; // create prop on __temp2

shows that JS objects are completely dynamic and can hold any properties they need, regardless on what property shape is defined on their “class”:

class Number extends Object {
  constructor(value) {
    super();
    // The class shape defines only _value property:
    this._value = value;
  }
  toString() {
    // implementation
  }
}

const n = 1;

const __temp2 = new Number(n);
__temp2.prop = 2; // create prop on __temp2

Once a property is not needed, it can be deleted:

delete __temp2.prop;

However, the toString method we call from 1 is not actually defined on the number instance, instead it’s inherited.

Prototypes

To confirm that the toString method is not own for the number instances, we can do the following:

const n = new Number(1);

n.hasOwnProperty('toString'); // false

n.toString(); // OK, "1"

The fact that we instantiate the Number class (or function-constructor in legacy terms) gives us a hint that the toString method should sit there. And it’s true, however it’s not directly on the class, but on the extra storage for shared methods, known as the prototype.

The prototype is accessible via the special property __proto__ from any object (which on practice should be replaced with Reflect.getPrototypeOf API method):

n.__proto__; // object

n.__proto__.hasOwnProperty('toString'); // true

The rule is simple: if a property (the toString in this case) is not found directly on the object, it’s searched in the prototype. Further in the prototype of the prototype, etc — until the whole prototype chain is considered. If eventually a property is not found in the chain, the special undefined value is returned:

n.toSuperString; // undefined

And this is how inheritance mechanism is implemented in JavaScript — through the prototype chain, also known as delegation chain in computer science.

The Number class stores the reference to the prototype of its instances in the explicit prototype property:

Number.prototype === n.__proto__; // true

Number.prototype.toString === n.toString; // true

This value

Since objects in JavaScript are dynamic, we can extend (“monkey-patch” on jargon) any object with needed properties and methods. For example, let’s define our custom method for all numbers:

const n = new Number(1);

const prefix = 'Super: ';

Number.prototype.toSuperString = function () {
  return prefix + this.toString();
};

n.toSuperString(); // "Super: 1"

Note: never monkey-patch standard classes with custom methods — to avoid backward compatibility issues and to make the coding predictable. The dynamic extension only makes sense when you need to patch a missing method for older JS engines.

There are some interesting observations here.

The toSuperString method is accessible from n instance even if it’s defined after the n was created itself:

n.toSuperString(); // "Super: 1"

This underlines dynamic nature of the prototype chain, and the property lookup at runtime.

Next, our new method is normally can access the prefix variable defined outside:

return prefix + this.toString();

This brings us to the wonderful topic of closures, detailed descriptions of which in the view of the Funarg problem, you can find here.

Yes, a closure is when a function can access variables not being local to this function, nor being parameters of this functions (so called, free variables), and not only “when a function is returned from another function”.

And we also see the current instance is accessible via this value inside methods:

 this.toString();
 

which again brings us to the topic of inheritance, since toString is not an own method of number instances, and is found on the same Number.prototype.

The topic of this value is one of the most complex topics in JS, and there are some related caveats. For example, there is no auto-binding in JS as e.g. in Python:

n.toSuperString(); // OK

// Same as:

Number.prototype.toSuperString.call(n); // OK

// Extract the method to a variable:

const toSuperString = Number.prototype.toSuperString;

toSuperString(); // not OK :(

// Explicitly bind `this` value as `n`:

const toSuperStringBound = Number.prototype.toSuperString.bind(n);

toSuperStringBound(); // OK

The issue above is known as the problem of losing correct context object. In most of the cases explicit binding helps, or mentioned arrow functions have the lexical this and capture it automatically as other closured values (note: this use-case doesn’t fit much for methods though, where this still should be dynamic).

Conclusion

Of course we only scratched the surface of the interesting world of JavaScript, and there are many more details and related topics. In this article I wanted to show that even such small line of code as 1..toString() may hold inside tones of aspects of JS internals. I hope you enjoyed the reading, and if you got interested in JS deeper, I invite you to explore its Fundamental Core.

Written by: Dmitry Soshnikov
Published on: May 29, 2021

JavaScript. The Core: 2nd Edition

Available coupons:

Read this article in: Russian, German.

This is the second edition of the JavaScript. The Core overview lecture, devoted to ECMAScript programming language and core components of its runtime system.

Note: see also Essentials of Interpretation course, where we build a programming language similar to JavaScript, from scratch.

Continue reading

Note 6. ES6: Default values of parameters

Read this article in Chinese, Russian.

In this small note we’ll cover another ES6 feature, function parameters with default values. As we will see there are some subtle cases there. Continue reading

ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.

Read this article in Chinese.

Introduction

In this chapter we continue our consideration of lexical environments. In the previous sub chapter 3.1 we clarified the general theory related with the topic. In particular we have learned that the concept of environments is closely related with concepts of static scope and closures. Continue reading

JavaScript. Array “extras” in detail.

This is an external article written specially for Opera software and placed on the Opera’s developer center website.

In this article we’ll look at the functionality made available by the new methods of array objects standardized in ECMA-262 5th edition (aka ES5). Most of the methods discussed below are higher-order (we’ll clarify this term shortly below), and related to functional programming. In addition, most of them have been added to different JavaScript implementations since version 1.6 (SpiderMonkey), although these were only standardized in ES5.
Continue reading

JavaScript. Ядро.

Read this article in: English, German, French, Chinese.

Обратите внимание: доступна обновленная вторая редакция данной статьи.

Данная обзорная лекция является обобщением того, что мы изучили в курсе “Тонкости ECMA-262-3“. Каждый раздел статьи содержит ссылки на соответствующие главы цикла ES3, который вы, в случае желания и интереса, можете рассмотреть подробно, получив более глубокие и детальные описания тем. Continue reading

ECMA-262-5 in detail. Chapter 3.1. Lexical environments: Common Theory.

Introduction

In this chapter we’ll talk in detail about lexical environments — a mechanism used in some languages to manage the static scoping. In order to understand this concept completely, we’ll also discuss briefly the alternative — dynamic scoping (which though isn’t used directly in ECMAScript). We’ll see how environments help to manage lexically nested structures of a code and to provide complete support of closures. Continue reading