Scope Assignment

Video

Overview

The purpose of this assignment is to understand scoping in Javascript and Node.js. You will also learn how to access environmental variables in a Node.js program.

Assignment folder

Create a directory named scope in your repository to hold the work for this assignment. When you complete this assignment, the contents of this folder will be the following.

Global objects

Global references in Javascript are attributes of an object called the global namespace object. In browsers, a reference to global namespace object is window; in Node.js it's global. To see this, create a file global-scope-1.html with the following contents and then load it in a browser.

global-scope-1.html
<script>
  window.x = 3;
  document.write(x); // 3
</script>

Contrast this with the following Node.js script. Put the following in a file named global-scope-1.js and run it with command node global-scope-1.js.

global-scope-1.js
global.x = 3;
console.log(x); // 3

In browsers, top-level declarations create references with global scope. Examine the following when loaded in a browser.

global-scope-2.html
<script>
  var x = 3;
  document.writeln(window.x);  // 3
</script>

However, in Node.js, top-level declarations do not create references in global scope. To place a reference into global scope, you need to explicitly make the reference a property of the global object. To see this, create and run the following.

global-scope-2.js
var x = 3;
console.log(global.x);  // undefined
global.x = x;
console.log(global.x); // 3

The process object

In Node.js, the process object has global scope. Create and test the following to see this.

process.js
console.log(process !== undefined); // true

The process object is useful for accessing environmental variables. Add the following to process.js and run to see that x is undefined.

console.log(process.env.x);

Now run the code with the environmental variable x set to "Hello there.".

Under OS X and Linux ...

x="Hello there." node process

Under Windows ...

set x="Hello there." & node process

The ability to access environmental variables through process.env is useful for running a system with different parameters, such as database connection strings, passwords, URLs, etc. This allows developers to run the system in testing, staging and production environments without modifying the source code.

If you have many variables to set, you can use a shell script to run the program and set each variable on it's own line using the system's line continuation character. As an example, suppose we wanted to run process.js with values set for variables x, y, z.

Under OS X and Linux ...

Create a file named run.sh with the following contents.

x="Do you like my hat?"   \
y="No I don't."           \
z=42                      \
node process

Make the shell script executable.

chmod +x run.sh

Under Windows ...

Create a file named run.bat with the following contents.

set x="Do you like my hat?"   ^
set y="No I don't."           ^
set z=42                      ^
node process

Modify process.js to display x, y, and z. Execute your run script to test.

Module scope

Rather than going into global scope, top-level declarations in Node.js go into module scope, which is a scope added to the language by Node.js. It works similarly to RequireJS and other Javascript libraries that try to provide a module scope that sits between function scope and global scope. Read the Modules section of the Node.js documentation to get a better idea of how Node.js modules work.

References to module instances are obtained by a call to the globally scoped function require. Objects in one module can be made accessible to other modules by adding the references to the module's exports property. To see how this works, create files mod1.js and main1.js with the following contents, and then run main1.js.

mod1.js
var x = 3;
var y = 3;
exports.x = x;
main1.js
var mod1 = require('./mod1');
console.log(mod1.x === 3);
console.log(mod1.y === undefined);

Be careful with the above example. The line exports.x = x; adds an attribute named x to the object exports and copies the value 3 into it. The following example shows how this pattern can lead to unexpected results.

mod2.js
var x = 3;
exports.x = x;
exports.getX = function() {
  return x;
};
main2.js
var mod2 = require('./mod2');
console.log(mod2.x === 3);
mod2.x = 10;
console.log(mod2.getX() === 3); // true

If we take x out of module scope, we get more intuitive code.

mod3.js
exports.x = 3;
exports.getX = function() {
  return exports.x;
};
main3.js
var mod3 = require('./mod3');
console.log(mod3.x === 3);
mod3.x = 10;
console.log(mod3.getX() === 10); // true

Although more intuitive, the code is still not well designed for any purpose I can think of other than illustrating module scope. If you're going to expose x to code outside the module, there's no reason to have a getX function.

Function scope

Unlike most popular languages, Javascript does not have block scope, which is scoping determined by braces. Instead, it uses function scope and hoisting. Read this article about scoping and hoisting and then try to understand what is going on in the following.

function-scope.js
var x = 1;
var y = 2;

console.log(x === 1);
console.log(y === 2);

function test() {
  x = 10;
  y = 20;
  var y;
}

test();

console.log(x === 10);
console.log(y === 2);

Closure scope

Closure scope results from Javascript's support for closures. You can think of closures as private spaces that are shared by one or more functions. In the following code, function getX forms a closure with variable x.

closure-scope-1.js
function getGetXFunction() {
  var x = 3;
  function getX() { return x; }
  return getX;
}

console.log(typeof(x) === 'undefined');

var getX = getGetXFunction();

console.log(getX() === 3);

The code demonstrates that variable x is only visible to getX. The variable x has closure scope in this example.

I wrote the above code to resemble code that you would see in other popular languages. To show you a style that is more common to Javascript programmers, study the following code that illustrates the same closure.

closure-scope-2.js
var getX = (function() {
  var x = 3;
  return function() { return x; }
})();

console.log(typeof(x) === 'undefined');

console.log(getX() === 3);

In both of the above examples x is hidden from all code except the function getX. The following shows how multiple functions would have access to x.

closure-scope-3.js
var obj = (function() {
  var x = 3;
  return {
    getX: function() { return x; },
    setX: function(value) { x = value; },
    getNegativeX: function() { return -x; }
  };
})();

console.log(typeof(x) === 'undefined');

console.log(obj.getX() === 3);
console.log(obj.getNegativeX() === -3);
obj.setX(4);
console.log(obj.getX() === 4);

This last example shows how to construct what would be called a singleton in languages that use classes, such as Java, C#, PHP, Python, and C++. A singleton is a class that is intended to be instantiated once in a program.

In Javascript, you can control the visibility of data using closures.

Rewrite examples

Rewrite the following modules to follow the pattern presented in the assert assignment.

These programs should simply print All tests passed when they run. Use strict equality === in your assert statements.