What does the dot do in JavaScript?

Ah, that little dot! Every line of JavaScript you write contains something like foo.bar, foo.bar(), or foo.bar = baz. But do you know what it does? Two concepts interact subtly here: prototypical inheritance and accessor properties (getters/setters). There are some weird corner cases, and I bet you don’t know all of them! In this post series, I explain the behavior of foo.bar, foo.bar(), and foo.bar = baz by reimplementing them in plain JavaScript.

First off, foo.bar, foo.bar(x,y), and foo.bar = baz are all “just” syntax sugar for the foo['bar'] syntax form:

x = foo.bar;    /* is sugar for */  x = foo['bar'];
foo.bar = baz;  /* is sugar for */  foo['bar'] = baz;
foo.bar(x,y);   /* is sugar for */  foo['bar'](x,y);

Unfortunately, it remains to explain foo[bar], foo[bar](x,y) and foo[bar] = baz, and these are not trivial. I actually had to consult the frumpy ECMAScript spec to get a reasonable description. In the spec, these forms are called Get, Set, and Invoke. Let’s re-implement them as JavaScript functions. First, here’s Get:

// You should be able to replace `obj[prop]` with `Get(obj,prop)`
function Get(obj, prop) {
  prop = typeof prop === 'symbol' ? prop : String(prop);
  for (let ancestor = obj; ancestor !== null; ancestor = Object.getPrototypeOf(ancestor)) {
    const desc = Object.getOwnPropertyDescriptor(ancestor, prop);
    if (desc) {
      return desc.get ? desc.get.call(obj)
           : desc.set ? undefined             // Accessor property with just a setter
           : desc.value;                      // Assuming data property
    }
  }
  return undefined;
}

Surprisingly, if we find an accessor with no getter, we just return undefined, rather than searching further up the chain:

const parent = { x: 5 };
const child = { set x(newX) { this.x = newX; } };
Object.setPrototypeOf(child, parent);

console.log(child.x)  // Logs undefined, not 5!

Next, let’s reimplement the “method call” syntax, foo[bar](x,y). This is mercifully short, because it re-uses the Get function:

// You should be able to replace `obj[prop](x,y)` with `Invoke(obj,prop, [x,y])`.
// Idiomatic adaptation of https://www.ecma-international.org/ecma-262/10.0/index.html#sec-invoke
function Invoke(obj, prop, argumentsList = []) {
  const method = Get(obj, prop);
  if (!(method instanceof Function)) throw new TypeError(`someObj.${prop} is not a function`);
  return method.call(obj, ...argumentsList);
}

Surprisingly, this means that you can define a getter that returns the function to bind in the method call:

const obj = {
  i: 10,
  get add() {
    // Return the method to be called
    return function(x) {
      this.i += x;  // `this` will be `obj`
    };
  }
};
obj.add(2);
console.log(obj.i);  // Logs 12

Finally, the worst of the bunch: foo[bar] = baz. This is called Set in the spec. It’s full of corner cases. Here’s my attempt at a reimplementation in plain JavaScript:

// You should be able to replace `obj[prop] = x` with `Set(obj,prop,x)`.
// Idiomatic adaptation of https://www.ecma-international.org/ecma-262/10.0/index.html#sec-set-o-p-v-throw
// and https://www.ecma-international.org/ecma-262/10.0/index.html#sec-property-accessors
export function Set(obj, prop, val) {
  prop = typeof prop === 'symbol' ? prop : String(prop);

  // First, try to update existing own property.
  const ownPropDesc = Object.getOwnPropertyDescriptor(obj, prop);
  if (ownPropDesc) {
    if (ownPropDesc.set) {
      ownPropDesc.set.call(obj, val);
      return val;
    }
    else if (ownPropDesc.get) {
      // Note: we end here, rather than going up the chain looking for a setter.
      throw new TypeError(`Cannot set property ${prop} of #<Object> which has only a getter`);
    }
    else {  // must be data prop; update it
      Object.defineProperty(obj, prop, { value: val });
      return val;
    }
  }
  else {
    // Not an own property. Search the prototype chain.
    for (let ancestor = Object.getPrototypeOf(obj); ancestor !== null; ancestor = Object.getPrototypeOf(ancestor)) {
      const ancestorPropDesc = Object.getOwnPropertyDescriptor(ancestor, prop);
      if (ancestorPropDesc) {
        if (ancestorPropDesc.set) {
          ancestorPropDesc.set.call(obj, val);
          return val;
        }
        else if (ancestorPropDesc.get) {
          throw new TypeError(`Cannot set property ${prop} of #<Object> which has only a getter`);
        }
        else {  // must be data prop
          if (ancestorPropDesc.writable) {
            // Note: despite the writable check, we _don't_ write to the ancestor, or continue up the chain.
            Object.defineProperty(obj, prop, { value: val, writable: true, enumerable: true, configurable: true });
            return val;
          }
          else {
            throw new TypeError(`Cannot assign to read only property '${prop}' of object '#<Object>'`);
          }
        }
      }
    }

    // Not on the prototype chain either. Just set a new own property.
    Object.defineProperty(obj, prop, { value: val, writable: true, enumerable: true, configurable: true });
    return val;
  }
}

Before ES5 created accessor properties, the foo[bar] = baz notation would basically just set an own property. It would not traverse the prototype chain. But now, if there is no own property, it has to go up the chain looking for a potential setter function. If it doesn’t find find one, it falls back to creating a new own property.

Here’s the next post on how prototypes work.

Tagged #programming, #javascript, #fave.

Similar posts

More by Jim

Want to build a fantastic product using LLMs? I work at Granola where we're building the future IDE for knowledge work. Come and work with us! Read more or get in touch!

This page copyright James Fisher 2020. Content is not associated with my employer. Found an error? Edit this page.