Skip to content

Variable Scope and Destructuring Assignment

In JavaScript, variables declared with var have a specific scope.

If a variable is declared inside a function body, its scope is the entire function body, and it cannot be referenced outside the function:

javascript
function foo() {
    var x = 1;
    x = x + 1;
}

x = x + 2; // ReferenceError! Cannot reference variable x outside the function

If two different functions declare the same variable, that variable only exists within its respective function. In other words, variables with the same name in different functions are independent and do not affect each other:

javascript
function foo() {
    var x = 1;
    x = x + 1;
}

function bar() {
    var x = 'A';
    x = x + 'B';
}

Due to JavaScript's ability to nest functions, an inner function can access variables defined in the outer function, but not vice versa:

javascript
function foo() {
    var x = 1;
    function bar() {
        var y = x + 1; // bar can access foo's variable x!
    }
    var z = y + 1; // ReferenceError! foo cannot access bar's variable y!
}

What happens if an inner function and an outer function have variables with the same name? Let's test it:

javascript
function foo() {
    var x = 1;
    function bar() {
        var x = 'A';
        console.log('x in bar() = ' + x); // 'A'
    }
    console.log('x in foo() = ' + x); // 1
    bar();
}

foo();

This shows that JavaScript functions search for variables starting from their own definitions, looking “inward” first. If the inner function defines a variable with the same name as the outer function's variable, the inner variable will "shadow" the outer variable.

Variable Hoisting

JavaScript has a unique feature where it scans the entire function body first, hoisting all var declared variables to the top:

javascript
function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
}

foo();

Even in strict mode, the statement var x = 'Hello, ' + y; does not throw an error because y is declared later. However, console.log shows Hello, undefined, indicating y is undefined. This happens because the JavaScript engine hoisted the declaration of y, but not its assignment.

For the above foo() function, the JavaScript engine interprets it as:

javascript
function foo() {
    var y; // Hoisting the declaration of y, now y is undefined
    var x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
}

Due to this quirky “feature,” when defining variables inside functions, always adhere to the rule of declaring all variables at the beginning. A common practice is to declare all variables used in a function with a single var:

javascript
function foo() {
    var
        x = 1, // x initialized to 1
        y = x + 1, // y initialized to 2
        z, i; // z and i are undefined
    // Other statements:
    for (i=0; i<100; i++) {
        ...
    }
}

If there’s no need for compatibility with older browsers, let can completely replace var for declaring variables.

Note

It’s recommended to use let for variable declaration to avoid the pitfalls of var.

Global Scope

Variables not defined within any function have global scope. In fact, JavaScript has a default global object window, so global scope variables are effectively properties of the window object:

javascript
var course = 'Learn JavaScript';
console.log(course); // 'Learn JavaScript'
console.log(window.course); // 'Learn JavaScript'

Thus, directly accessing the global variable course is the same as accessing window.course.

You might have guessed that functions defined as variables (e.g., var foo = function () {}) are also global variables, making top-level function definitions global variables bound to the window object:

javascript
function foo() {
    alert('foo');
}

foo(); // Directly calling foo()
window.foo(); // Calling via window.foo()

Boldly guessing further, the alert() function we call directly is actually also a variable of window:

javascript
window.alert('Calling window.alert()');
// Save alert to another variable:
let old_alert = window.alert;
// Assign a new function to alert:
window.alert = function () {}

alert('Cannot display using alert()!');

// Restore alert:
window.alert = old_alert;
alert('Alert can be used again!');

This shows that JavaScript effectively has a single global scope. If a variable (functions are also considered variables) cannot be found in the current function scope, it continues to search upward, and if not found in the global scope, it throws a ReferenceError.

Namespace

Global variables are bound to window, and if different JavaScript files use the same global variables or define functions with the same name, it can cause naming conflicts that are difficult to identify.

One method to reduce conflicts is to bind all your variables and functions to a single global variable. For example:

javascript
// Unique global variable MYAPP:
let MYAPP = {};

// Other variables:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// Other functions:
MYAPP.foo = function () {
    return 'foo';
};

By putting all your code into a unique namespace MYAPP, the likelihood of global variable conflicts is greatly reduced.

Many well-known JavaScript libraries do this: jQuery, YUI, Underscore, etc.

Local Scope

Since JavaScript's variable scope is actually within functions, we cannot define locally scoped variables within blocks like for loops:

javascript
function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // Variable i can still be referenced
}

To resolve block scope, ES6 introduces the new keyword let, which can declare block-scoped variables:

javascript
function foo() {
    let sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    // SyntaxError:
    i += 1;
}

Constants

Since both var and let declare variables, there was no way to declare a constant before ES6; we usually used all-uppercase variable names to indicate "this is a constant, do not modify its value":

javascript
let PI = 3.14;

The ES6 standard introduces a new keyword const to define constants. Both const and let have block scope:

javascript
const PI = 3.14;
PI = 3; // Some browsers do not throw an error but have no effect!
PI; // 3.14

Destructuring Assignment

Starting from ES6, JavaScript introduced destructuring assignment, allowing simultaneous assignment to a group of variables.

What is destructuring assignment? Let's first look at the traditional approach to assigning elements of an array to several variables:

javascript
let array = ['hello', 'JavaScript', 'ES6'];
let x = array[0];
let y = array[1];
let z = array[2];

Now, in ES6, we can use destructuring assignment to assign multiple variables at once:

javascript
// If the browser supports destructuring assignment, no error will be thrown:
let [x, y, z] = ['hello', 'JavaScript', 'ES6'];

// x, y, z are assigned the corresponding elements of the array:
console.log(`x = ${x}, y = ${y}, z = ${z}`);

Note that when destructuring array elements, multiple variables should be enclosed in [...].

If the array itself has nested elements, we can also destructure them as follows, keeping the nesting levels and positions consistent:

javascript
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

Destructuring assignment can also ignore certain elements:

javascript
let [, , z] = ['hello', 'JavaScript', 'ES6']; // Ignore the first two elements and assign the third to z
z; // 'ES6'

If we want to extract several properties from an object, destructuring assignment allows for quick access to specified object properties:

javascript
let person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};
let {name, age, passport} = person;

// name, age, passport are assigned to their corresponding properties:
console.log(`name = ${name}, age = ${age}, passport = ${passport}`);

When destructuring an object, we can also directly assign nested object properties, ensuring the corresponding levels are consistent:

javascript
let person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school',
    address: {
        city: 'Beijing

',
        street: 'No.1 Road',
        zipcode: '100001'
    }
};
let {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, because the property name is zipcode not zip
// Note: address is not a variable; it's to allow city and zip to obtain the properties of the nested address object:
address; // Uncaught ReferenceError: address is not defined

When using destructuring assignment on object properties, if a corresponding property does not exist, the variable will be assigned undefined, consistent with referencing a nonexistent property yielding undefined. If the variable name differs from the property name, we can use the following syntax:

javascript
let person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};

// Assign the passport property to the variable id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// Note: passport is not a variable; it's to let the variable id obtain the passport property:
passport; // Uncaught ReferenceError: passport is not defined

Destructuring assignment can also use default values, which avoids the issue of nonexistent properties returning undefined:

javascript
let person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678'
};

// If the person object does not have a single property, default to true:
let {name, single=true} = person;
name; // '小明'
single; // true

Sometimes, if a variable has already been declared, reassigning it will throw a syntax error:

javascript
// Declare variables:
let x, y;
// Destructuring assignment:
{x, y} = { name: '小明', x: 100, y: 200};
// Syntax error: Uncaught SyntaxError: Unexpected token =

This occurs because the JavaScript engine treats a statement starting with { as a block, making = no longer valid. The solution is to wrap it in parentheses:

javascript
({x, y} = { name: '小明', x: 100, y: 200});

Use Cases

Destructuring assignment can greatly simplify code in many situations. For example, to swap the values of two variables x and y, you can write:

javascript
let x=1, y=2;
[x, y] = [y, x];

Quickly obtaining the current page's domain and path:

javascript
let {hostname:domain, pathname:path} = location;

If a function receives an object as a parameter, destructuring allows you to directly bind the object's properties to variables. For example, the following function can quickly create a Date object:

javascript
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
    return new Date(`${year}-${month}-${day} ${hour}:${minute}:${second}`);
}

Its convenience lies in that the input object only needs to have the three properties year, month, and day:

javascript
buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)

You can also pass in hour, minute, and second properties:

javascript
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)

Using destructuring assignment can reduce code size, but it requires modern browsers that support ES6 destructuring features to function correctly. Currently, browsers like Chrome, Firefox, and Edge support destructuring assignment.

Variable Scope and Destructuring Assignment has loaded