Making Closure less verbose with goog.scope
Type fewer characters and wrap fewer lines.
Many names in Closure are very long due to namespacing - take for example:
goog.dom.browserrange.AbstractBrowserRange
or
goog.ui.AutoComplete.EventType.SELECT
Names of this length are arduous to type, which slows developer productivity. Furthermore, they cause line wraps which reduce code readability.
We aim to provide a way that these examples could be referred to as just AbstractBrowserRange or SELECT, similar to imports in Java.
When uncompiled, the code should still work in all browsers. When compiled, there should be no size penalty compared to not using imports.
Non-Goals
Lexical hiding: goog.scope should not be used for preventing ‘locals’ for leaking into the global scope. Anonymous functions already solve this problem.
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.scope(function() {
var createElement = goog.dom.createElement;
var dom = goog.dom;
document.body.appendChild(createElement(dom.TagName.DIV));
});
For uncompiled code, the implementation is trivial.
At compile time, names will be expanded to their full form in a compiler pass that occurs before current passes. The compiler will throw away the function wrapper. If there are local variables that cannot be inlined, then the compiler will emit an error.
Why do it this way?
What isn’t great about this approach?
NOTE - these are the proposed style rules and are no longer accurate. See https://engdoc.corp.google.com/eng/doc/javascriptguide.xml?cl=head#goog-scope
goog.scope(function() {
var Button = goog.ui.Button; // good
var dom = goog.dom; // good.
var tree = goog.ui.TreeControl; // BAD!
document.body.appendChild(createElement(dom.TagName.DIV));
});
You auto-migrate your code to use goog.scope in 3 easy steps
1) Build the main js binary of your app, and save the compiled binary in some safe place.
2) Run the scopify.py script in your main app directory. It will automatically walk every JS file in the directory, g4 edit it, wrap it in a goog.scope, and create any obvious aliases. The scopify script is in the bin/ directory.
3) Build the main js binary of your app again, and diff the result against the binary you built in step (1). If they are the same, then the migration successfully scopified your code. Congratulations!
There are some gotchas that you should watch out for:
When goog.scope creates an alias, it captures the value of that global name. If the global variable is modified after it is aliased, then it may break the uncompiled code (but not the compiled code).
Indenting a file 2 spaces usually causes a merge conflict with any open files. Make sure your co-workers know that you are doing this before you begin. In many cases, merging the scopify change manually will be too hard, and it will be easier for them to reject changes, and re-run the scopify script in their local client.
Appendix 1 - working proposals that were considered
Manual aliasing with an annotation
goog.require('goog.dom');
goog.require('goog.dom.TagName');
/** @package */
(function() {
var dom = goog.dom;
var createElement = goog.dom.createElement;
document.body.appendChild(createElement(dom.TagName.DIV));
})();
Why not use this:
Integration with goog.require
(function() {
var DIV = goog.require('goog.dom.TagName.DIV');
var createElement = goog.require(’goog.dom.createElement’);
mypackage.myfunction = function() {
document.body.appendChild(createElement(DIV));
};
// other declarations go here
})();
Why not use this:
List match aliasing
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.usingImports(
goog.dom.createElement, goog.dom.TagName.DIV,
function(createElement, DIV) {
document.body.appendChild(createElement(DIV));
});
How it works: Objects from the first list are matched with parameters to the function.
Why not use this:
Parameter-name aliasing
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.usingImports(function(
goog_dom,
createElement, DomHelper,
goog_dom_TagName,
DIV, SPAN, A) {
document.body.appendChild(createElement(DIV));
// ...
});
How it works: The function is converted to a string, and the argument names are parsed from the string. Parameter names containing underscores (that are not all upper case and therefore just static constants) are assumed to be namespaces for all following imports until the next namespace.
Why not use this:
Appendix 2 - broken proposals that were considered
Explicit sub-tree in require
To import goog.dom.createElement as ce and goog.dom.TagName.DIV as myDiv, use the following which displays the import tree with local identifier names as leaves.
function () {
var ce, myDIV;
goog.require({
goog: {
dom: {
createElement: 'ce',
TagName: { DIV: 'myDIV' }
}
}
});
...
}
This can be implemented in non-compiled code as:
goog.require = function (__imports) {
function __doImport(__prefix, __imports) {
if (typeof __imports === 'string') {
// TODO: assert __imports is a simple left hand side
eval(__imports + ' = this' + __prefix);
return
}
for (var k in __imports) {
if (!{}.hasOwnProperty.call(__imports, k)) { continue; }
__doImport(__prefix + '.' + k, __imports[k]);
}
}
__doImport('', __imports);
}
Why it’s broken:
Doesn’t work due to evaling in the wrong scope
Temporary variable replacement
goog.require('goog.dom');
goog.defineClass('A',
goog.imports('goog.dom.getElement'),
goog.constructor(function(name) {
this.name = name;
}),
function show(id) {
getElement(id).innerHTML = this.name;
});
goog.defineClass('B',
goog.imports('goog.dom'), goog.superClass(A),
goog.constructor(function(name) {
base(name);
}),
goog.member('id', null),
function show(id) {
base(id);
this.id = id;
},
function louder() {
dom.getElement(this.id).style.fontWeight = 'bold';
});
goog.defineClass('C',
goog.superClass(B),
goog.constructor(function(name) {
base(name + '!!!');
}),
function show(id) {
base(id);
this.louder();
});
var a = new A('Hello World!');
a.show('div');
var b = new B('Bold');
b.show('div2');
b.louder();
var c = new C('Exclaim');
c.show('div3');
How it would work:
Every function has a pre and post step added to it that stores values of imported names from window, then replaces them, then calls the function, then restores them.
Why it’s broken:
It breaks in the presence of closures
[a]What about cases where you want to create two aliases, but both namespaces have the same last property? E.g. foo.bar.Button and herp.derp.Button.