8
\$\begingroup\$

Edit: I updated the JavaScript modules in http://enlargeyourpassword.com to use scope(). You can see the result in the source of the page, and get an idea of the process by looking at the commit history on GitHub.

As a JavaScript developer, I want to write modules in a format that is well supported today and that will still work in five years.

I designed an open-source library to this end, scope or not, with two goals in mind. It should be:

  1. simple to declare JavaScript modules with dependencies
  2. simple to implement your own version of the library to customize its behavior

There is a single function named scope(). This is a null implementation:

function scope(code, needs, name){
  return null;
}

The parameter code is required. It is a function that defines a module. For example:

scope(function(){
  var module = {};
  // ...
  return module;
});

The scope() function has different implementations with different behaviors: it may create a module in the global scope, in a private scope, synchronously or asynchronously after loading dependencies.

When the scope() function is synchronous, it returns the module just created, which allows to use scope() as a direct replacement of the Immediately Invoked Function Expression pattern typically used to declare modules in JavaScript:

var myLib = myLib || scope(function(){
  return {
    // myLib API
  };
});

The last two parameters needs and name are optional. The name of a module is a string which identifies the module and allows to reference it in the list of needs of other modules:

scope(function(){
  var moduleA = {};
  // ...
  return moduleA;
},[],"moduleA");

scope(function(context){
  // ...
  return moduleB;
},["moduleA"],"moduleB");

For each dependency in the array of needs, a property of the same name is set on the context object which is provided as argument to the function:

scope(function(context){
  var
    moduleA = context.moduleA, // a local alias for "moduleA"
    moduleB = {};
  // ...
  return moduleB;
},["moduleA"],"moduleB");

To use the scope() function, you can write your own or build it from building blocks available in the scope or not project. The building blocks are sorted in levels. The level 1 provides the bootstrap, it must be loaded first. One building block may be picked from each level. Building blocks from lower levels are expected to be loaded first, but they are all optional.

Building blocks in higher levels use the scope() function of the bootstrap to define a replacement function "scope", declared as a module. For example:

// from scope-level2-shared.js
scope(function(parentContext){
  var
    // private field
    privateContext = {};

  /*
    Function: scope(code[,needs[,name]]): any
    Run code immediately in a private context, and set the return value,
    if any, to a property with given name in the private context
    (...)
  */
  function scope(code,needs,name){
    var result = code(privateContext);

    if (typeof name !== "string"){
      return result;
    }

    privateContext[name] = result;

    if (name === "scope"){
      // replace the current implementation of scope() in parent context
      parentContext.scope = result;
    }
    return result;
  }

  return scope;
},[],"scope");

When you implement the scope() API yourself, you may either create a building block on top of the scope bootstrap and other building blocks, or rewrite the scope() function from scratch, replacing the bootstrap and all building blocks altogether.

In a browser, you can load building blocks with script tags, then load your own modules:

<script src="scopeornot/scope-level1-global.js"></script>
<script src="scopeornot/scope-level2-shared.js"></script>
<script src="moduleA.js"></script>
<script src="moduleB.js"></script>
...

You may later change the building blocks to load your modules asynchronously instead:

<script src="scopeornot/scope-level1-global.js"></script>
<script src="scopeornot/scope-level2-shared.js"></script>
<script src="requirejs/require.js"></script>
<script src="scopeornot/scope-level3-amd.js"></script>
<script src="setup-and-startup-top-level-module-with-require.js"></script>

Please review the usability of this API from the point of view of a developer who uses the API and from the point of view of a developer who implements the API.

\$\endgroup\$
4
  • \$\begingroup\$ If you know about AMD and RequireJS etc.. why did you create this? \$\endgroup\$
    – Esailija
    Jul 30, 2012 at 17:09
  • \$\begingroup\$ @Esailija I designed scope() to be 100% compatible with the define() function of AMD, but without all the semantics attached. While you would expect a call to define() to be compliant with the AMD specification, scope() can follow your own conventions, e.g. synchronous definition in the global scope by default. \$\endgroup\$ Jul 31, 2012 at 10:59
  • 1
    \$\begingroup\$ My only usability concern is a very simple and pragmatic one: Having dependencies and name as the 2nd and 3rd arguments means they end up at the very end of a file. To me, it'd make a lot more sense to have that at the top, simply to keep the code more readable; "this thing is called 'foo', depends on 'bar' and 'baz', and is defined thusly...". A little tricky to do with optional args, of course, but well worth the effort in my opinion. \$\endgroup\$
    – Flambino
    Nov 4, 2012 at 2:24
  • \$\begingroup\$ @Flambino Thanks for the feedback. You are right, this is a compromise between ease of use and ease of implementation. Another rationale was to make the signature compatible with AMD but different. I have started to identify other shortcomings, that I will develop as an answer to my own question. \$\endgroup\$ Nov 5, 2012 at 12:43

2 Answers 2

1
\$\begingroup\$

I could suggest you to have a look at requirejs (http://requirejs.org/) and plugin (https://github.com/tbranyen/use.js) for it for synchronous loading scripts to respect dependencies

\$\endgroup\$
3
  • \$\begingroup\$ Please clarify. How do you see this RequireJS plugin as relevant to evaluate the usability of scope()? \$\endgroup\$ Aug 3, 2012 at 16:18
  • \$\begingroup\$ @Eric What I was saying is that your approach does not care about asynchronous loading of the scripts and also seems to be reinventing the wheel. I am not agains scopes but from the terms of usability it looks inconsistent with the way requirejs handles dependency declaration. \$\endgroup\$ Aug 3, 2012 at 17:55
  • \$\begingroup\$ There is no need to run all scripts asynchronously all the time. For example, scripts are loaded synchronously on node.js. As for use.js, it has different goals than scope(): "It simply ensures the proper dependencies have loaded and attaches the specified global object to the module definition.". scope() is not a module loader or a plugin for a module loader. It is a layer of indirection that you can use to define modules in an abstract way, leaving the choice of the module loader open. AMD is a great specification for asynchronous module loading, but it ties you to one kind of loader. \$\endgroup\$ Aug 3, 2012 at 19:34
1
\$\begingroup\$

Like the define() method in AMD API, which is very similar, the scope() function mixes separate concerns in a single call :

  • private scope: prevent declarations inside the module from polluting global namespace
  • definition of the module dependencies (imports)
  • definition of shared symbols (exports)
  • resolution of module names to paths of scripts
  • deferred loading of scripts
  • access to shared symbols (imports)

I am now considering ways to separate these concerns into several functions and even several independent libraries.

For example, scope() could be restricted to a single argument, to cover only the first concern:

scope(function(){
  ... // private scope
});

A separate function/library could be defined to declare dependencies, another for synchronous/asynchronous loading, and yet another to import and export symbols in a shared context.

These separate functions may communicate and interact through a common event system (pub/sub).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.