ES6 Concise Methods: Lexical Or Not?

A minor twitter debate raged earlier today over a nuance of the upcoming ES6, and it prompted me to write this quick blog post to defend my position, even though a member of the Babel project (formerly 6to5) insists on disagreeing.

ES6 Concise Methods

ES6 adds a new feature called “concise methods” to object literal definitions, and it looks like this:

var o = {
   foo() { .. }
};

And that’s shorthand for the more familiar:

var o = {
   foo: function() { .. }
};

Pretty straightforward, huh? Not quite.

See how the function expression foo: function() is anonymous… it doesn’t say foo: function foo(). That is, plain and simple, what the spec says, and the fact of it isn’t really up for debate — though early on in the debate it seems James did apparently try unsuccessfully to contest that fact, or the communication was at least confused. Thankfully, the spec settled it.

What difference does the lack of a lexical identifier in the function() expression make? It means you will not have a lexical identifier to make recursive calls or do event binding/unbindings, etc. In other words, concise method syntax won’t be appropriate for those sorts of functions.

OK, so let’s not debate if it does or it doesn’t create a lexical identifier binding — it doesn’t. Also, I don’t care to debate any further if this is a mistake on ES6’s part — I think it is, but that battle is already lost.

How Babel Transpiles It

So what’s the point of my blog post, other than to clear up the fact of there being no lexical name?

The Babel transpiler takes this code:

var o = {
   foo() { }
};

… and produces this:

var o = {
   foo: function foo() {}
};

Spot the difference? They insert a foo into function foo(). They’re inserting a lexical identifier, when the ES6 specs no such lexical binding is created.

I call this an inconsistency with the strict interpretation of the ES6 spec. I stand by that label.

Why don’t I call it a “bug” in Babel? For a couple important reasons:

  • They know about this behavior, and it’s intentional.
  • They’re doing this because they’re trying to accommodate another ES6 feature called “function name inference”. For an otherwise anonymous function expression, name inference sets a function object’s name property based on the assignment context. If you say var foo = function(){ .. }, ES6 infers that the name should be foo, and so it sets that function object’s name property value to "foo", accordingly.

    Why does foo.name === "foo" matter? Mostly for debugging purposes, so that the name foo will show up in stack traces instead of anonymous. There’s a few other nuanced uses of the name property, but that’s besides the point here.

  • I think it’s admirable that Babel is trying to stay true to the ES6 function name inference behavior. Unfortunately, adding a lexical name is the only way to reliably set a function expression’s name property, so to do so, that’s why they put the lexical identifier foo in.

So, it should be clear that the Babel folks are not just idiots or misunderstanding the spec. They’re smart, they know the ES6 spec, and know it well, and are making a conscious choice here. I support them on that front.

Gotta Remove foo

But there’s more to the story. If Babel inserted the foo identifier always, the following code (which will fail in ES6!) would incorrectly work in their transpiled version, and that would definitely qualify as a “bug”.

var o = {
   foo(x) {
     if (x < 3) return foo(3);
     return x * 2;
   }
};

o.foo(1);   // should throw ReferenceError in real ES6

The good news is, they don't blindly insert foo here. They play another set of intricate tricks, and instead produce this code:

var o = {
  foo: (function (_foo) {
    var _fooWrapper = function foo() {
      return _foo.apply(this, arguments);
    };

    _fooWrapper.toString = function () {
      return _foo.toString();
    };

    return _fooWrapper;
  })(function (x) {
    if (x < 3) return foo(3);
    return x * 2;
  })
};

I'm not going to belabor explaining how what they're producing here works, or why they do it this way. They should write a blog post on their techniques -- I'm sure it would be fascinating to read.

But the takeaway here is that in this part of the code:

  })(function (x) {
    if (x < 3) return foo(3);
    return x * 2;
  })

...there correctly is no foo identifier, so they've preserved spec compliance by not binding a name where it shouldn't be bound. All the other stuff in their transpilation is their workaround for still setting a name property without the lexical name binding.

Great, right!? I agree, good solution they came up with. Very innovative.

Base Case Inconsistency

But let's go back to the original base case:

var o = {
   foo() { }
};

Remember, they currently produce:

var o = {
   foo: function foo() {}
};

That extra foo in there isn't observable via code, and their transpiler takes special care to eliminate it in cases where it would be (like the recursion we described).

However, when you just look at their simple transpilation, it undeniably gives a false impression that all concise methods get a lexical name binding. We've already proven they don't, so that false impression misleading is, presently, my concern.

Is it actually misleading? Yes.

I'm not just being academically nuanced here. Lots and lots of people use the output of various ES6 transpilers to understand the ES6 spec. I do sometimes, though never in isolation without reading to verify.

I'm not just making up a hypothetical misleading. Several times now, including just this morning, someone will take the output of a tool like Babel and get confused comparing that output with my assertions (from my YDKJS: ES6 & Beyond book) about ES6 not producing a lexical binding. Similar confusions have happened no fewer than half a dozen times to me over the last 6-12 months.

It seems quite common that someone tends to trust what they see from a transpiler, without much further investigation. This is a troubling trend, I'd say. Others agree.

In today's tweet, the questioner assumed that perhaps Babel had a bug. I declined to call it a "bug", and instead called it an inconsistency, and went on to elaborate at length about why that inconsistency is, in my opinion, sometimes misleading to developers who don't stop to dig into all the nuance.

Others in the past have flat out claimed I'm incorrect about the spec and that Babel proves it by its adding of foo in this base case, and I have to re-clarify all over again.

The Babel folks don't like that I'm calling their simpler handling of this base case an "inconsistency". That's fine, they're entitled to their own opinions.

I still stand by it, because I think it accurately describes the gray area nuance between "totally correct and clear" and "obviously wrong and buggy".

Put simply: just because the base case I've shown doesn't let the inconsistency be observed in code doesn't mean the inconsistency isn't observable by drive-by developers glancing at quick snippets from a transpiler demo.

Stack Trace Side Note

As a side note, while the Babel transpilation is admirable in its attempts to stay true to ES6 spec, there's one minor deviation it produces.

The transpiled recursive snippet I showed earlier produces this stack trace (in Chrome 43 currently), since correctly foo cannot be found:

ReferenceError: foo is not defined
    at Object. (test-babel.js:15)
    at Object.foo (test-babel.js:6)
    at test-babel.js:21

Notice how the last entry at the top of the stack is an Object.<anonymous> reference? As far as I understand it, a true native ES6 run of the original non-transpiled code (if such an engine currently existed to test it on) would have instead produced a stack trace more ike this:

ReferenceError: foo is not defined
    at Object.foo (test-babel.js:4)
    at test-babel.js:11

Is this a big deal in terms of variation? Not really. It's a minor annoyance, a slight deviation, but one that's at least worth noting, in the interest of being completely frank and honest.

Summary

I don't hate Babel or its creators/maintainers. I think they're fantastic folks. And I think Babel is great.

But I stand by my assertion that some of the code they produce in their transpilation is somewhat misleading and, in a sense, not entirely consistent with the spirit of the specification. It's good that this inconsistency is not observable in code, but that doesn't make it not a slight inconsistency, nonetheless, nor does it insulate them from perhaps misleading folks unintentionally.

The alternative would be for them to always produce the longer IIFE based translation, so there's never any misleading or apparent inconsistency. But I'm not going to lose any sleep if they elect not to change anything. I'll just have more confused tweets to deal with in the future.

Posted in: JavaScript by getify No Comments ,