Manual:Coding conventions/JavaScript
This page documents a MediaWiki development guideline, crafted over time by developer consensus (or sometimes by proclamation from a lead developer) |
This page describes coding conventions for JavaScript in the MediaWiki codebase. See also the general conventions.
Linting
We use ESLint as our code quality tool, with the eslint-config-wikimedia preset to encode most of our coding style and code quality rules. Refer to Integrations on eslint.org for many text editors or IDEs with plugins to provide live feedback as you type.
Linter configuration
To exclude files or directories from the analysis (e.g. third-party libraries), you can configure ignore patterns in ESLint, through an .eslintignore
file.[1]
Note that node_modules
is excluded by default, so most repos would not need to set up any ignore rules.
Every repo needs an .eslintrc.json
file in the root directory of the repository.
The following shows an example ESLint config file:
{
"root": true,
"extends": [
"wikimedia/client",
"wikimedia/jquery",
"wikimedia/mediawiki"
],
"globals": {
// Project-wide globals.
},
"rules": {
// Project-wide rule variations. Keep to a minimum.
}
}
See .eslintrc.json in MediaWiki and VisualEditor for live examples.
Remember to set "root": true
to avoid unintended "magic inheritance" of ESLint configurations in unrelated parent directories that you or the CI server might have set up on the disk (for example between a MediaWiki extension and MediaWiki core, or between your project directory and something in your home directory).
The eslint-config-wikimedia preset provides several profiles that projects can choose from as they see fit, such as different language flavours, or run-time environments globals. For example:
wikimedia/client
- for browser code in the current default ES environment (currently ES6, so an alias ofwikimedia/client-es6
)wikimedia/client-es5
- for browser code that expects ES5 supportwikimedia/client-es6
- for browser code that expects ES6 support
wikimedia/server
- for Node.js 10+ code and expects ES2018 supportwikimedia/qunit
- a mixin for QUnit tests
You should expect to use multiple .eslintrc.json
files in a repo to set different environmental expectations for different subdirectories.
This makes sure that you don't accidentally use window
methods in server code you expect to run on the server, or reference QUnit in production code.
.eslintrc.json
files in subdirectories automatically inherit from the parent directory's configuration, so they only need to contain the things that are specific to that directory (example file).
Continuous integration
Projects are encouraged to enforce ESLint passing by including it their npm test
script via the "test" command in package.json
.
See Continuous integration/Entry points for more information about that.
If your project has a fairly heavy test pipeline, one could define an npm run lint
or even npm run lint:js
script in package.json
to make it easy to run just those from the command-line during local development (so that one doesn't have to open each file in an editor and/or to wait for CI).
To expose other command-line features from ESLint (such as linting individual files outside a text editor, or using the --fix feature), define "eslint" as its own script in package.json without any argument. You can then invoke it from the command-line as follows:
# Entire directory, recursive
$ npm run eslint -- .
$ npm run eslint -- resources/
# Single file
$ npm run eslint -- resources/ext.foo/bar.js
# Fixer
$ npm run eslint -- --fix resources/ext.foo/
Whitespace
Spaces
We use the following conventions:
- Indentation with tabs.
- No trailing whitespace.
- Use blank lines to separate one block of logically related code from another.
- One space on both sides of binary operators and assignment operators.
- Keywords followed by a "
(
" (left parenthesis) must be separated by one space. This gives visual distinction between keywords and function invocations. - There should be no space between the function name and left parenthesis of an argument list.
- There should be one space on the insides of parentheses (such as in if statements, function calls, and arguments lists).
- Don't use operators as if they are functions (such as
delete
,void
,typeof
,new
,return
, ..).
These and other aspects of our style guide are enforced with ESLint.
Correct | Wrong |
---|---|
a.foo = bar + baz;
if ( foo ) {
foo.bar = doBar();
}
function foo() {
return bar;
}
foo = function () {
return 'bar';
};
foo = typeof bar;
function baz( foo, bar ) {
return 'gaz';
}
baz( 'banana', 'pear' );
foo = bar[ 0 ];
foo = bar[ baz ];
foo = [ bar, baz ];
|
a.foo=bar+baz;
if( foo ){
foo.bar = doBar () ;
}
function foo () {
return bar;
};
foo = function() {
return('bar');
};
foo = typeof( bar );
function baz(foo, bar) {
return 'gaz';
}
baz('banana', 'pear');
foo = bar[0];
foo = bar[baz];
foo = [bar,baz];
|
Line length
Lines should wrap at no more than 80–100 characters. If a statement does not fit on a single line, split the statement over multiple lines. The continuation of a statement should be indented one extra level.
Function calls and objects should either be on a single line or split over multiple lines with one line for each segment. Avoid closing a function call or object at a different indentation than its opening.
Yes |
---|
// One line
if ( mw.foo.hasBar() && mw.foo.getThis() === 'that' ) {
return { first: 'Who', second: 'What' };
} else {
mw.foo( 'first', 'second' );
}
// Multi-line (one component per line)
if (
// Condition head indented one level.
mw.foo.hasBar() &&
mw.foo.getThis() === 'that' &&
!mw.foo.getThatFrom( 'this' )
) {
// ↖ Closing parenthesis at same level as opening.
return {
first: 'Who',
second: 'What',
third: 'I don\'t know'
};
} else {
mw.foo(
[ 'first', 'nested', 'value' ],
'second'
);
}
|
No |
// No: Mixed one line and multi-line
if ( mw.foo.hasBar() && mw.foo.getThis() === 'that' &&
!mw.foo.getThatFrom( 'this' ) ) {
// No: varying number of segments per line.
return { first: 'Who', second: 'What',
third: 'I don\'t know' };
} else {
mw.foo( 'first', 'second',
'third' );
// No: Statements looking like they are split over multiple lines but still on line.
// Visualy looks like a call with one parameter, or with an array as first parameter.
mw.foo(
'first', 'second', 'third'
);
mw.foo(
[ 'first', 'nested', 'value' ], 'second'
);
}
|
Structure
In a nutshell:
// Variables that have literal and cheap initial values
const baz = 42;
const quux = 'apple';
// Local functions
function local( x ) {
return x * quux.length;
}
// Main statements
const foo = local( baz );
const bar = baz + foo;
Closure
If a module bundle can't or otherwise isn't yet registered using package files , then its individual JavaScript files should have a file-level closure around its code.[2] This gives the code its own scope, and avoids leakage of variables from or to other files, including in debug mode, and in a way that is understood by static analysis. This pattern is known as an immediately-invoked function expression (or "iffy").[3]
For package files this is not needed, as they are executed as a "module" file rather than a "script" file, which naturally have their own local scope.
ESLint should also be configured accordingly (set "parserOptions": { "sourceType": "commonjs" }
, like in this example).
Declarations
Variables must be declared before use. Each assignment must be on its own line. Variables may be declared near or at their first assignment.
const waldo = 42;
const quux = 'apple';
let foo, bar;
const flob = [ waldo ];
if ( isFound( waldo ) ) {
foo = 1;
bar = waldo * 2;
} else {
foo = 0;
bar = 0;
}
for ( let i = 0; i < flob.length; i++ ) {
// ...
}
Functions should be declared before use. In the function body, function declarations should go after variable declarations and before any main statements.
Comments
Comments should be on their own line and go over the code they describe.
Within a comment, the opening syntax (e.g. slash-slash, or slash-star) should be separated from the text by a single space, and the text should start with a capital letter. If the comment is a valid sentence, then a full stop should be placed at the end of it.
Use line comments (// foo
) within functions and other code blocks (including for multi-line comments).
Use block comments (/* foo */
) only for documentation blocks.
This helps maintain consistent formatting for inline comments (e.g. not some as blocks and some as multi-line comments, or having to convert from one to the other).
It also avoids confusing documentation engines.
It also makes it easy to disable parts of the code during development by simply moving the end-comment notation a few lines down, without being short-circuited by an inline block comment.
Be liberal with comments and don't fear file size because of it. All code is automatically minified by ResourceLoader before being shipped.
Documentation comments
- Text in free-form blocks should be sentence case (e.g. description of methods, parameters, return values etc.)
- Start sentences with a capital letter.
- Continue sentences belonging to an annotation on the next line, indented with one additional space.
/**
* Get the user name.
*
* Elaborate in an extra paragraph after the first one-line summary in
* the imperative mood.
*
* @param {string} foo Description of a parameter that spans over on the
* next line of the comment.
* @param {number} bar
* @return {string} User name
*/
ES5 classes
To document a class that uses ES5 syntax, with the class and constructor defined together as function MyClass(…) {…}
, use:
- a
@classdesc
tag to document the class - a
@description
tag to document the constructor
/**
* @classdesc Class description.
*
* @description Constructor description.
*
* @param {string} myParam
* @return {string} Description
*/
ES6 classes
To document a class that uses ES6 syntax, with the constructor defined using constructor()
, use separate comments to document the class and the constructor.
/**
* Class description.
*/
class myClass {
/**
* Constructor description.
*
* @param {string} myParam
* @return {string} Description
*/
constructor() {...}
}
Generated documentation
Use JSDoc to build documentation (see doc.wikimedia.org). To set up and publish JSDoc documentation, see JSDoc .
Equality
- Use strict equality operators (
===
and!==
) instead of (loose) equality (==
and!=
). The latter does type coercion. - No Yoda conditionals.
Type checks
- string:
typeof val === 'string'
- number:
typeof val === 'number'
- boolean:
typeof val === 'boolean'
- Function:
typeof val === 'function'
- object:
val === Object( val )
- plain object:
jQuery.isPlainObject( val )
- array:
Array.isArray( val )
- HTMLElement:
obj.nodeType === Node.ELEMENT_NODE
- null:
val === null
- undefined:
val === undefined
Strings
Use single quotes instead of double quotes for string literals.
Remember there are no "magic quotes" in JavaScript i.e. \n
and \t
work everywhere.
To extract part of a string, use the slice()
method for consistency.
Avoid the substr()
, or substring()
methods which are redundant, easily mistaken, and may have unexpected side effects.[4][5][6]
Exporting
Use globals exposed by the browser (such as document, location, navigator) directly and not as properties of the window object.
This improves confidence in code through static analysis and may also power other IDE features.
In addition to browser globals, the only globals that are safe to use are mw
, $
and OO
.
Avoid creating new global variables. Avoid modifying globals not "owned" by your code. For example, built-in globals such as String or Object must not be extended with additional utility methods, and similarly functionality relating to OOjs or jQuery should not be assigned to those globals.
To publicly expose functionality for re-use, use module.exports
from Package files , and/or assign properties within the mw
hierarchy, e.g. mw.echo.Foo
.
Note that configuration variables exposed by MediaWiki must be accessed via mw.config
.
Environment
Modifying built-in protypes such as Object.prototype
is considered harmful.
This is not supported in MediaWiki code, and doing so will likely result in breakage of unrelated features.
Naming
All variables must be named using CamelCase starting with a lowercase letter, or use all caps (with underscores for separation) if the variable represents some kind of constant value.
All functions must be named using CamelCase, generally starting with a lowercase letter unless the function is a class constructor, in which case it must start with an uppercase letter.
Method functions are preferred to start with verb, e.g. getFoo()
instead of foo()
.
Acronyms
Names with acronyms in them should treat the acronym as a normal word and only uppercase the first letter as-needed.
This applies to two-letter abbreviations as well, such as Id
.
For example, getHtmlApiSource
as opposed to "getHTMLAPISource".
jQuery
Distinguish DOM nodes from jQuery objects by prefixing variables with a dollar sign if they will hold a jQuery object, e.g. $foo = $( '#bar' )
.
This helps reduce mistakes where conditions use incorrect conditional checks, such as if ( foo )
instead of if ( $foo.length )
.
Where DOM methods often return null (which is falsey), jQuery methods return an empty collection object (which, like native arrays and other objects in JavaScript, are truthly).
npm
When publishing a standalone project to npmjs.org, consider publishing it under the @wikimedia
namespace.
Note that some standalone projects, that are aimed at use outside the Wikimedia community and have a sufficiently unique name, currently use an unnamespaced package name (e.g. "oojs" and "visualeditor").
T239742
Creating elements
To create a plain element, use the simple <tag>
syntax in the jQuery constructor:
$hello = $( '<div>' )
.text( 'Hello' );
When creating elements based on the tag name from a variable (which may contain arbitrary html):
// Fetch 'span' or 'div' etc.
tag = randomTagName();
$who = $( document.createElement( tag ) );
Only use $('<a title="valid html" href="#syntax">like this</a>');
when you need to parse HTML (as opposed to creating a plain element).
Collections
Different types of collections sometimes look similar but have different behaviour and should be treated as such. This confusion is mostly caused by the fact that arrays in JavaScript look a lot like arrays in other languages, but are in fact just an extension of Object. We use the following conventions:
Intent | Arrays | Plain Objects | jQuery Objects |
---|---|---|---|
Declaration and empty initialisation | x = [];
|
x = {};
|
$x = $( [] );
|
Access value | x[ 0 ];
|
x.key;
|
element = $x[ 0 ]; or $y = $x.eq( 0 );
|
Size | x.length;
|
Object.keys( x ).length;
|
$x.length;
|
Iteration | for ( i = 0; i < x.length; i++ ) {} or: x.forEach( ( value, i ) => {} );
|
for ( key in x ) {}
|
$x.each( ( i, element ) => {} );
|
Avoid using a for-in
loop to iterate over an array (as opposed to a plain object), because for-in
will result in many unexpected behaviours, including: keys as strings, unstable iteration order, indexes may skip gaps, iteration may include other non-numerical properties.
Storage
Keys in localStorage and/or sessionStorage should be accessed via mw.storage
or mw.storage.session
.
Keys
Keys should start with mw
and use camel case and/or hyphens.
Do not use underscores or other separators.
Examples of real keys:
mwuser-sessionId
mwedit-state-templatesUsed
mwpreferences-prevTab
Beware that contrary to cookies via mw.cookie, there is no wiki prefix or cookie prefix added by default.
If values must vary by wiki, you must manually include wgCookiePrefix
as part of the key.
Values
Values must be strings. Beware that attempting to store other value types will silently cast to a string (e.g. false
would become "false"
).
Space is limited. Use short and concise values over object structures where possible. A few example:
- For boolean state (true/false, expanded/collapsed) use "1" or "0".
- For values that are always numbers, store them as-is and cast with
Number
on the way out (avoidparseInt
). - For values that are always strings, store them as-is.
- For lists of software-defined values, consider comma-separated or pipe-separated strings to reduce space and processing cost.
- For lists of values that may be user-generated or are otherwise complex in nature, use JSON.
Eviction strategy
Remember that Local storage does not have any eviction strategy by default. Therefore the following should be avoided:
- Avoid using user-generated input as part of a key name.
- Avoid keys containing identifiers for user-generated entities (e.g. user names, category names, page ids, or other user-provided or system-provided variables).
- In general avoid approaches that involve creating a potentially large number of storage keys.
For example, if a feature needs to store state about a variable entity (e.g. current page), it might make sense to use a single key for this feature as a whole and to limit the stored information only to the last few iterations (LRU). The slight increase in retrieval cost (the whole key, instead of separate smaller ones) is considered worth it, as otherwise the number of keys would grow uncontrollably.
Even if the keys are not dependent on user-input, you may still want to use a single key for your feature because otherwise past versions of your software will have stored data that you can't clean up. By using a single key you can roundtrip it in a way that naturally doesn't persist unknown sub properties.
Use of a tracking key is also an anti-pattern and does not avoid the above issues, as this would leak due to race conditions in HTML5 web storage being shared and non-atomic between multiple open browser tabs.
When feature's use of Local storage is to be removed, be sure to implement an eviction strategy first to clean up old values. Typically mw.requestIdleCallback is used to gracefully look for the key and remove it. See T121646 for a more scalable approach.
Personal information
Avoid storing personal information in Local storage as it remains when a user logs out or closes their browser. Use Session storage instead. See T179752.
Asynchronous code
Asynchronous code should follow, and be compatible with, the Promise standard.
When you define an asynchronous method for other code to call, you may internally construct the returned thenable object either using $.Deferred or native Promise.
When you call an asynchronous method, use only standard Promise-compatible methods such as then()
and catch()
.
Avoid using jQuery-specific methods like done()
or fail()
, which could stop working without warning if the method you are calling internally transitions from $.Deferred to native Promise.
Note that there are also subtle legacy behaviours in the done
and fail
callbacks.
Pay close attention when migrating existing code from done()
to then()
as doing so may cause the code to stop working correctly.
Specifically, the done
and fail
callbacks invoke your callback synchronously if the Deferred was already settled by the time you attach your callback.
This means your callback may be invoked before the attaching statement is finished running.
For example:
function getSqrt( num ) { return $.Deferred().resolve( Math.sqrt( num ) ); }
console.log( "A" );
getSqrt( 49 ).done( ( val ) => {
console.log( "C" ); // can be A C B, or, A B C
} );
console.log( "B" );
console.log( "A" );
getSqrt( 49 ).then( ( val ) => {
console.log( "C" ); // always A B C
} );
console.log( "B" );
const y = getSqrt( 49 ).then( ( val ) => {
console.log( y.state(), val ); // "resolved", 7
} );
const x = getSqrt( 49 ).done( ( val ) => {
console.log( x.state(), val ); // Uncaught TypeError: x is undefined
} );
const z = getSqrt( 49 );
z.done( ( val ) => {
console.log( x.state(), val ); // "resolved", 7
} );
jQuery
jQuery is still supported in MediaWiki and it is a useful tool, but it is slow and dated, and there are some functions and patterns to avoid. The following emit warnings or errors in eslint-config-wikimedia.
- Avoid functions in the jQuery library that are deprecated, since these will be removed from jQuery at some point or already have been. Examples:
.addSelf
,.bind
,.unbind
,.boxModel
,.browser
,.camelCase
,.context
,.delegate
,.undelegate
, etc.[7] - Avoid functions in the jQuery library that have ES5/ES6 equivalents. The ES5/ES6 equivalents are faster. Examples:
.each
,.grep
,.inArray
,.map
,.trim
,.error
,.extend
,.noop
.[8] - Avoid functions in the jQuery library that have CSS equivalents. The CSS equivalents are faster. Examples:
.animate
,.stop
,.finish
,.fadeIn
,.fadeOut
,.fadeTo
,.fadeToggle
,.slideDown
,.slideToggle
,.slideUp
, etc.[8] - Avoid using jQuery to query the DOM, for performance reasons and to avoid selecting incorrect elements by mistake.
- Avoid sizzle selectors, for performance reasons and because some sizzle syntax is already deprecated. For example, avoid
$( '#mw-specialmute-form input:checkbox' )
and$input.is( ':visible' )
. - Avoid
.globalEval
. - Generate complex HTML using multiple jQuery functions instead of just a string in the
$()
function. For example, prefer$( '<a>' ).attr( 'href', 'https://test.com' ).text( 'Test' )
to$( '<a href="https://test.com">Test</a>' )
. The latter is prone to errors that linters cannot detect, and triggers jQuery's HTML parser, which is slow.[11]
Final notes
Reuse ResourceLoader modules
Don't reinvent the wheel. Much JavaScript functionality and MediaWiki-related utilities ship with MediaWiki core that are stable and you can (literally) depend on them. See ResourceLoader/Core modules before rolling your own code.
Pitfalls
- Be careful to preserve compatibility with left-to-right and right-to-left languages (i.e.
float: right
ortext-align: left
), especially when styling text containers. Putting those declarations in CSS file will allow them to be automatically flipped for RTL-languages by CSSJanus in ResourceLoader . - Use
attr()
andprop()
appropriately.
Read more: - Consistently quote attribute selector values:
[foo="bar"]
instead of[foo=bar]
(jqbug 8229). - As of jQuery 1.4 the jQuery constructor has a new feature that allows passing an object as second argument, like:
$( '<div>', { foo: 'bar', click: () => {}, css: { .. } } );
. Don't use this. It makes code harder to follow, fails on attributes (such as 'size') that are also methods, and is unstable due to this mixing of jQuery methods with element attributes. A future jQuery method or plugin called "title" might convert an element into a heading, which means the title attribute can also no longer be set through this method. Be explicit and call.attr()
,.prop()
,.on()
etc. directly.
Use CSS for styling many elements
Don't apply styling to lots of elements at once; this has poor performance.
Instead use a common parent's class (or add one) and apply CSS in a .css
or .less
file.
Thanks to ResourceLoader, this will all be loaded in the same HTTP request, so there's no performance penalty for having a separate CSS file.
Do not set CSS into inline "style" attributes, don't insert "style" elements from JS either.
References
- JavaScript Style Guide, Contribute to jQuery
- Code Conventions for JavaScript, Douglas Crockford
- Programming Style & Your Brain, Douglas Crockford on YouTube
- ↑ Decide which "eslint ignore" method to use, April 2020.
- ↑ T50886: Remove obsolete file closures, April 2019.
- ↑ http://benalman.com/news/2010/11/immediately-invoked-function-expression/
- ↑ Should I substr(), substring(), or slice()?
- ↑ Strings in JavaScript, Mastering JS.
- ↑ Change 158108 - mediawiki/core, , gerrit.wikimedia.org
- ↑ https://github.com/wikimedia/eslint-config-wikimedia/blob/91e9a0ba09bc77471d21c1e92d24fa190109a08e/jquery.json#L8
- ↑ 8.0 8.1 https://github.com/wikimedia/eslint-config-wikimedia/blob/91e9a0ba09bc77471d21c1e92d24fa190109a08e/jquery.json#L11-L28
- ↑ https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/no-global-selector.md
- ↑ https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/no-class-state.md
- ↑ https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/no-parse-html-literal.md