Search code examples
javascriptjsonbareword

Using bare words to declare a JavaScript object manually


I'm a beginner to JavaScript and am trying to refactor an API, but I'm running into a strange problem.

I use the node interpreter to test some of the code I'd like to incorporate in my redesign, but I've run into some unexpected behavior.

Consider the following entered into the node interpreter:

[In0] b

I expect a ReferenceError, which I get as seen below.

[Out0] 
ReferenceError: b is not defined
at repl:1:1
at REPLServer.defaultEval (repl.js:132:27)
at bound (domain.js:254:14)
at REPLServer.runBound [as eval] (domain.js:267:12)
at REPLServer.<anonymous> (repl.js:279:12)
at REPLServer.emit (events.js:107:17)
at REPLServer.Interface._onLine (readline.js:214:10)
at REPLServer.Interface._line (readline.js:553:8)
at REPLServer.Interface._ttyWrite (readline.js:830:14)
at ReadStream.onkeypress (readline.js:109:10)

Next

 [In1] 'b'
 [Out1] 'b' // this is OK because it's a string

Now consider a JSON-style object:

 [In2] var x = { 'b' : 'c' };
 [Out2] { b : 'c' }

Strangely enough, the quotes are dropped on the key of x. I'm willing to overlook this, though it is confusing.

Finally,

 [In3] var y = { b : 'c' };
 [Out3] { b : 'c' }

Without the quotes, the interpreter does not complain and yields the same results as with quotes.

I also noticed that this pattern, key is always returned as a string:

for(var key in obj) {
    key;
 }

Noticing this behavior, I tried to write a method, Bar within my class Foo, as defined as follows:

function Foo(a) {
    this.a = a;
    this.Bar = function(attr, val) {
        // attr doesn't need to be a string to be a key in object x
        var x = { attr : val };
        for(var key in x) {
            //key DOES need to be a string to be used like this
            this[key] = val;
        }
    }
}

Essentially Bar just adds the key-value pair of attr:val to an object of type Foo.

I attempted to test this code with the following:

[In4] var foo = new Foo('c');
[Out4] { a : 'c', Bar : [Function] } // this is OK. I expect this.

[In5] foo.Bar(hello, "World");
[Out5]
ReferenceError: hello is not defined
at repl:1:9
at REPLServer.defaultEval (repl.js:132:27)
at bound (domain.js:254:14)
at REPLServer.runBound [as eval] (domain.js:267:12)
at REPLServer.<anonymous> (repl.js:279:12)
at REPLServer.emit (events.js:107:17)
at REPLServer.Interface._onLine (readline.js:214:10)
at REPLServer.Interface._line (readline.js:553:8)
at REPLServer.Interface._ttyWrite (readline.js:830:14)
at ReadStream.onkeypress (readline.js:109:10)

Can anyone explain this behavior to a noob? It's very frustrating to not be able to use a bare word as a key within a method when it can be done manually.


Solution

  • Context matters a lot in language parsing.

    In JavaScript, property names are always either strings or Symbols (that last is new in ES6). In an object initializer, you can specify a property name without quotes (using a literal name, which must be an IdentifierName), as a numeric literal (which is converted to a string), or as a string literal (e.g., with quotes):

    var a = { b: 'c' };
    // is the same as
    var a = { 'b': 'c' };
    // is the same as
    var a = { "b": 'c' };
    

    (In ES6, you can also use computed property names in square brackets, but that's not relevant here, and in any case they end up being either Symbols or strings after being computed.)

    So in an object initializer, the context tells the parser that it's expecting a property name, for which either literal or string notation is allowed. But here:

    foo.Bar(hello, "World");
    

    ...the context tells it that hello is an identifier, because the grammar says that the individual argument values (contents of the parentheses) in a function call are expressions, and a single word on its own in an expression indicates an identifier. Since it's an identifier you haven't declared, you get a ReferenceError.

    Strangely enough, the quotes are dropped on the key of x. I'm willing to overlook this, though it is confusing

    That's the standard notation for showing an object's properties, when the property name is a valid IdentifierName.


    Re your comments:

    I'm a beginner to JavaScript and am trying to refactor an API

    Fair enough. It looks like there's no better way than to force the user to pass strings.

    Right, the correct way to pass around property names is as strings, sometimes numbers (as with array indexes), or Symbols, which you then use to access the properties using "brackets" notation:

    var a = {foo: "bar"};
    var name = "foo";
    console.log(foo[name]); // "bar"
    

    (When you do that with a number, it's coerced to a string. And yes, that happens [in theory] when you use array indexes with JavaScript's standard arrays, because they aren't really arrays at all.)

    That doesn't mean that the users of your API should be required to type string literals though. If there's a set of allowed property names, you can make those available as properties on an object.

    var API = {
        Things: {
            Foo: "Foo",
            Bar: "Bar"
        }
    };
    

    ...then using the API:

    someApiMethod(API.Things.Foo);
    

    You can even define those read-only to prevent accidental overwriting:

    var API = {Things: {}};
    ["Foo", "Bar"].forEach(function(name) {
        Object.defineProperty(API.Things, name, {
            enumerable: true, // Or not, whichever
            value: name
        });
    });
    

    By default, properties defined with defineProperty are read-only.

    In ES6, they can also be constants:

    const Foo = "Foo";
    const Bar = "Bar";