For updated documentation, please see the GJS repository.

Coding style

Our goal is to have all JavaScript code in GNOME follow a consistent style. In a dynamic language like JavaScript, it is essential to be rigorous about style (and unit tests), or you rapidly end up with a spaghetti-code mess.

Semicolons

JavaScript allows omitting semicolons at the end of lines, but don't. Always end statements with a semicolon.

js2-mode

If using Emacs, try js2-mode. It functions as a "lint" by highlighting missing semicolons and the like.

Imports

Use CamelCase when importing modules to distinguish them from ordinary variables, e.g.

const Big = imports.big;
const GLib = imports.gi.GLib;

Variable declaration

Always use one of "const", "var", or "let" when defining a variable. Always use "let" when block scope is intended; in particular, inside for() and while() loops, let is almost always correct.

// Iterating over an array
for (let i = 0; i < 10; i++) {
    let foo = bar(i);
}

// Iterating over an object's properties
for (let prop in someobj) {
    ...
}

If you don't use "let" then the variable is added to function scope, not the for loop block scope. See What's new in JavaScript 1.7

A common case where this matters is when you have a closure inside a loop:

for (let i = 0; i < 10; i++) {
    mainloop.idle_add(function() { log('number is: ' + i); });
}

If you used "var" instead of "let" it would print "10" a bunch of times.

Inside functions, "let" is always correct instead of "var" as far as we know. "var" is useful when you want to add something to the with() object, though... in particular we think you need "var" to define module variables, since our module system loads modules with the equivalent of "with (moduleObject)"

"this" in closures

"this" will not be captured in a closure; "this" is relative to how the closure is invoked, not to the value of this where the closure is created, because "this" is a keyword with a value passed in at function invocation time, it is not a variable that can be captured in closures.

To solve this, use Lang.bind, eg:

const Lang = imports.lang;

let closure = Lang.bind(this, function() { this._fnorbate() });

A more realistic example would be connecting to a signal on a method of a prototype:

const Lang = imports.lang;

MyPrototype = {
    _init: function() {
        fnorb.connect('frobate', Lang.bind(this, this._onFnorbFrobate));
    },

    _onFnorbFrobate: function(fnorb) {
        this._updateFnorb();
    },
};

Object literal syntax

JavaScript allows equivalently:

foo = { 'bar': 42 };
foo = { bar: 42 };

and

var b = foo['bar'];
var b = foo.bar;

If your usage of an object is like an object, then you're defining "member variables." For member variables, use the no-quotes no-brackets syntax, that is, { bar: 42 } and foo.bar.

If your usage of an object is like a hash table (and thus conceptually the keys can have special chars in them), don't use quotes, but use brackets, { bar: 42 } and foo['bar'].

Variable naming

  • We use javaStyle variable names, with CamelCase for type names and lowerCamelCase for variable and method names. However, when calling a C method with underscore-based names via introspection, we just keep them looking as they do in C for simplicity.

  • Private variables, whether object member variables or module-scoped variables, should begin with "_".
  • True global variables (in the global or 'window' object) should be avoided whenever possible. If you do create them, the variable name should have a namespace in it, like "BigFoo"

  • When you assign a module to an alias to avoid typing "imports.foo.bar" all the time, the alias should be "const TitleCase" so "const Bar = imports.foo.bar;"

  • If you need to name a variable something weird to avoid a namespace collision, add a trailing "_" (not leading, leading "_" means private).
  • For GObject constructors, always use the lowerCamelCase style for property names instead of dashes or underscores.

Whitespace

  • 4-space indentation (the Java style)
  • No trailing whitespace.
  • No tabs.
  • If you chmod +x .git/hooks/pre-commit it will not let you commit with messed-up whitespace (well, it doesn't catch tabs. turn off tabs in your text editor.)

JavaScript "classes"

Keep in mind that JavaScript does not "really" have classes in the sense of C++ or Java; you can't create new types beyond the built-in ones (Object, Array, RegExp, String). However, you can create object instances that share common properties, including methods, using the prototype mechanism.

Each JavaScript object has a property __proto__; if you write obj.foo and "foo" is not in "obj", JavaScript will look for "foo" in __proto__. If several objects have the same __proto__, then they can share methods or other state.

You can create objects with a constructor, which is a special function. Say you have:

function Foo() {}
let f = new Foo();

For "new Foo()" JavaScript will create a new, empty object; and execute Foo() with the new, empty object as "this". So the function Foo() sets up the new object.

"new Foo()" will also set __proto__ on the new object to Foo.prototype. The property "prototype" on a constructor is used to initialize __proto__ for objects the constructor creates. To get the right __proto__ on objects, we need the right prototype property on the constructor.

You could think of "f = new Foo()" as:

let f = {}; // create new object
f.__proto__ = Foo.prototype; // doing this by hand isn't actually allowed
Foo.call(f); // invoke Foo() with new object as "this"

Our pattern for writing classes is:

function Foo(arg1, arg2) {
    this._init(arg1, arg2);
}

Foo.prototype = {
    _init: function(arg1, arg2) {
        this._myPrivateInstanceVariable = arg1;
    },
    myMethod: function() {

    },
    myClassVariable: 42,
    myOtherClassVariable: 'Hello'
}

This pattern means that when you do "let f = new Foo()", f will be a new object, f.__proto__ will point to Foo.prototype, and Foo.prototype._init will be called to set up the object.

  • Note: Again, on the JavaScript language level, Foo is not a class in the sense of Java or C++; it's just a constructor function, which means it's intended for use with the "new Foo()" syntax to create an object. Once the object is created, from a JavaScript language perspective its type is the built-in type "Object" - though we're using it and thinking of it as if it had type "Foo", JavaScript doesn't have a clue about that and will never do any type-checking based on which constructor was used to create something. All typing is "duck typing." The built-in types, such as Object, String, Error, RegExp, and Array, are real types, however, and do get type-checked.

  • Note: If a constructor function has a return value, it is used as the value of "new Foo()" - replacing the automatically-created "this" object passed in to the constructor. If a constructor function returns nothing (undefined), then the passed-in "this" is used. In general, avoid this feature - constructors should have no return value. But this feature may be necessary if you need the new instance to have a built-in type other than Object. If you return a value from the constructor, "this" is simply discarded, so referring to "this" while in the constructor won't make sense.

JavaScript "inheritance"

There are lots of ways to simulate "inheritance" in JavaScript. In general, it's a good idea to avoid class hierarchies in JavaScript. But sometimes it's the best solution.

Our preferred approach is to use a Spidermonkey-specific extension and directly set the __proto__ member of the subclass's prototype to point to the prototype of the base class. Looking up a property in the subclass starts with the properties of the instance. If the property isn't there, then the prototype chain is followed first to the subclass's prototype and then to the base class's prototype.

const Lang = imports.lang;

function Base(foo) {
    this._init(foo);
}

Base.prototype = {
    _init: function(foo) {
        this._foo = foo;
    },
    frobate: function() {
    }
};

function Sub(foo, bar) {
    this._init(foo, bar);
}

Sub.prototype = {
    __proto__: Base.prototype,

    _init: function(foo, bar) {
        // here is an example of how to "chain up"
        Base.prototype._init.call(this, foo);
        this._bar = bar;
    }

    // add a newMethod property in Sub.prototype
    newMethod: function() {
    }
}
  • Note: You cannot use this mechanism to inherit from a built-in type, such as String or Error, because the methods on those objects expect them to have primitive type String or Error, while your constructor will create an Object instead.
  • Note: You cannot use this mechanism to inherit from a GObject. For that to work, we would need to create a new GType in C that was a subclass of the base class GType, and we'd need to override the class methods in the new GType so they called into JavaScript. Tons of work. For now, GObject subclasses must be implemented in C.

In portable JavaScript code you'll often see a different technique used to get this prototype chain:

function Base(foo) ...

Base.prototype = ...

function Sub(foo, bar) ...

// Do NOT do this - do not use an instance of Base as the Sub prototype
Sub.prototype = new Base();

The problem with this pattern is that you might want to have side effects in the Base() constructor. Say Base() in its constructor creates a window on the screen, because Base() is a dialog class or something. If you use the pattern that some instances of Base() are just prototypes for subclasses, you'll get extra windows on the screen.

The other problem with this pattern is that it's just confusing and weird.

JavaScript attributes

Don't use the getter/setter syntax when getting and setting has side effects, that is, the code:

foo.bar = 10;

should not do anything other than save "10" as a property of foo. It's obfuscated otherwise; if the setting has side effects, it's better if it looks like a method.

In practice this means the only use of attributes is to create read-only properties:

get bar() {
    return this._bar;
}

If the property requires a setter, or if getting it has side effects, methods are probably clearer.

Attic/Projects/GnomeShell/Gjs_StyleGuide (last edited 2020-04-09 23:44:36 by AndyHolmes)