中文 English

Three JavaScript Quirks Java/C Developers Should Know

Published: 2021-02-28
javascript js scoping hoisting function with closures

JavaScript can be a deceptive language and can cause a lot of pain because it’s not 100% consistent. It is known that it does have some bad, confusing or redundant features: the infamous with statement, 隐式全局变量 and 不稳定的比较 are probably the most famous.

JavaScript is one of the most successful flame generators in history! Aside from its flaws (which are partially addressed in the new ECMAScript specification), most programmers hate JavaScript for two reasons:

Because of this, JavaScript in general has a worse reputation than it deserves.

Over the course of my career, I’ve noticed several patterns: Most developers with a Java or C/C++ background work with language features that are assumed to be the same in JavaScript, yet completely different.

This article collects the most troublesome articles, compares the Java way to the JavaScript way to show the differences, and highlights JavaScript best practices.

Scope

Most developers start using JavaScript because they are forced to, so almost all of them start coding before taking a moment to learn the language. Every such developer has been tricked by JavaScript scopes at least once.

Because JavaScript’s syntax is very similar (intentionally) to C-family languages, with curly braces delimiting the function, if, and for bodies, one can reasonably expect lexical block-level scoping. Unfortunately, this is not the case.

First, in JavaScript, variable scope is determined by functions rather than square brackets. In other words, the if and for bodies do not create a new scope and actually hoist the variable declared in their body, i.e., create the variable at the beginning of the innermost function in which it is declared and otherwise create the variable in the global scope.

Second, the presence of the with statement forces JavaScript scope to be dynamic and cannot be determined until runtime. You may not be surprised to hear that with is deprecated: stripped JavaScript with is actually a lexically scoped language, i.e. the scope can be determined entirely by looking at the code.

Formally, in JavaScript, there are four ways to define a variable scope:

A further complexity arises from the implicit global scope that is assigned to variables declared (implicitly) without the var keyword. This madness is paired with the implicit allocation of the global scope to be referenced when calling a function without an explicit binding of this (more on this in the next section).

Before we delve into the details, let’s clearly state a good pattern that can be used to avoid confusion:

Use strict mode ('use strict';) and move all variable and function declarations to the top of each function; avoid declaring variables inside for and if blocks, and declaring functions inside these blocks (for different reasons, this is beyond the scope of this article).

Variable Hoisting (Hoisting)

Variable promotion is a simplified form that explains the actual behavior of a declaration. Lift variables are declared at the beginning of the function that contains them and initialized to undefined. Then, make the assignment on the actual line where the original declaration was.

Look at the following example: What value do you want printed to the console? Are you surprised by the following output? Inside the if block, the var statement does not declare a local copy of the variable i, but overwrites the previously declared copy. Note that the first console.log statement displays the actual value of the variable i, which has its initial value set to undefined. You can test this by using the "use strict"; directive as the first line of the function. In strict mode, a variable must be declared before it can be used, but you can check that the JavaScript engine won’t complain about the declaration. On a side note, be aware that you’ll get a redeclaration of var without complaining: if you want to catch errors like this, you should better handle code like JSHint or JSLint with lint.

Now, let’s look at another example to highlight another error-prone use of variable declaration:

Although you might expect otherwise, the if body still executes because notNull declares a local copy of the variable named inside the test() function and that body has been promoted. Type coercion also comes into play here.

function myFunction() {
  console.log(i);
  var i = 0;
  console.log(i);
  if (true) {
    var i = 5;
    console.log(i);
  }
  console.log(i);
}
undefined
0
5
5
var notNull = 1;
function test() {
  if (!notNull) {
    console.log("Null-ish, so far", notNull);
    for(var notNull = 10; notNull <= 0; notNull++){
      //..
    }
    console.log("Now it's not null", notNull);
  }
  console.log(notNull);
}

Function declaration and function expression

Hoisting applies not only to variables, function expressions (which are variables for all intents and purposes), but also to function declarations. This topic needs to be treated with more caution than this one, but in short, function declarations behave mostly like function expressions, except that their declaration is moved to the beginning of their scope.

Consider the following example, which shows the behavior of a function declaration:

Now, compare this with an example showing the behavior of function expressions: For further information on these concepts, see the Resources section.

The following example shows a case where scope can only be determined at runtime: If y has a field named x, the function foo() will return y.x, otherwise it will return 123. This coding practice can cause runtime errors, so it is strongly recommended that you avoid using the with statement.

function foo() {
    // A function declaration
    function bar() {
        return 3;
    }
    return bar();

    // This function declaration will be hoisted and overwrite the previous one
    function bar() {
        return 8;
    }
}
function foo() {
    // A function expression
    var bar = function() {
        return 3;
    };
    return bar();

    // The variable bar already exists, and this code will never be reached
    var bar = function() {
        return 8;
    };
}

Looking to the future: ECMAScript 6

The ECMAScript 6 specification will add a fifth way to add block-level scope: the let statement. Consider the following code: In ECMAScript 6, declaring i with let inside the body of if will create a new variable local to the if block. As a non-standard alternative, the let block can be declared as follows: In the above code, variables i and j will only exist inside the block. At the time of writing, support for let is limited, even for Chrome.

function foo(y) {
  var x = 123;
  with(y) {
    return x;
  }
}

Scopes summary

The following table summarizes the scopes in different languages:

Features|Java|Python|JavaScript|Warnings Scope | Lexical (Block) | Lexical (Function, Class or Module) | Yes | It works very differently than Java or C Block scope | Yes | No | let keyword (ES6) | Warning again: this is not Java! Lifting|Never! |No|Yes|For variable and function expressions, only dangling declarations are used. For function declarations, the definition will also be hoisted

function myFunction() {
  console.log(i);
  var i = 0;
  console.log(i);
  if (false) {
    let i = 5;
    console.log(i);
  }
  console.log(i);
}
var i = 6;
let (i = 0, j = 2) {
  /* Other code here */
}
// prints 6
console.log(i);

Functions

Another very misunderstood feature of JavaScript is functions, especially because in Java, an imperative programming language, there is no such concept.

In fact, JavaScript is a functional programming language. Well, not a purely functional programming language like Haskell - after all, it still has an imperative flavor and, like Scala, encourages rather than simply allows variability. Despite this, JavaScript can be used as a purely functional programming language, where function calls do not have any side effects.

#First-Class Citizens Functions in JavaScript can be treated like any other type, such as String and Number: they can be stored in variables, passed as arguments to functions, returned by functions, and stored in arrays. Functions can also have properties and can be changed dynamically because…

Objects

A very surprising fact for most JavaScript newbies is that functions are actually objects. In JavaScript, every function is actually a Function object. The Function constructor creates a new Function object: (Almost) equal to: I say they are nearly equivalent because using the Function constructor is less efficient, generates an anonymous function, and does not create a closure for the context it creates. Function objects are always created in the global scope.

The type of the Function function is based on Object. This can be easily seen by inspecting any function you declare:

This means that functions can and do have properties. Some of them have been assigned to functions at creation, such as name or length. These properties return the name and number of the parameters in the function definition respectively.

Consider the following example: But you can even set new properties for any function yourself:

Function summary

The following table describes the functions in Java, Python and JavaScript:

Features|Java|Python|JavaScript|Warnings Built-in Functions | Java 8 Lambdas | Yes | Yes | Callback/Command Pattern | Object (or lambda for Java 8) | Yes | Yes | Function (callback) has properties that can be modified by the “client” Dynamically created|not|not|eval - function object|eval has security concerns and Function objects may not work properly Properties | No | No | Can have properties | Cannot restrict access to function properties

closure

If I had to choose my favorite JavaScript feature, it would be closures without a doubt. JavaScript was the first mainstream programming language to introduce closures. As you know, closure functionality in Java and Python has been weak for a long time and you can only read (certain) values ​​from the enclosing scope.

For example, in Java, anonymous inner classes provide closure-like functionality, but with some limitations. For example, final local variables can only be used within their scope - better said, their values ​​can be read.

JavaScript allows full access to externally scoped variables and functions. They can be read, written and, if desired, even hidden in local definitions: you can see examples of all these cases in the Scope section.

What’s even more interesting is that functions created within closures remember the environment in which the function was created. By using closures in conjunction with function nesting, you can have outer functions return inner functions without executing them. Additionally, you can make the outer function’s local variables survive the closure of the inner function long after the execution of the declared inner function has ended. This is a very powerful feature, but it also has drawbacks, as it is a common cause of memory leaks in JavaScript applications.

Some examples will clarify these concepts: makeCounter()The function above creates and returns another function that keeps track of the environment in which it was created. Although execution ends when counter is allocated to the makeCounter() variable, the local variable i remains within the closure of displayCounter and can therefore be accessed inside its body.

If we run makeCounter again, it will create a new closure with a different entry i: To make it more interesting, we can update the makeCounter() function to take a parameter: The outer function parameters are also kept in the closure, so we don’t need to declare local variables this time. Each call to makeCounter() will remember the initial value we set and continue to use it.

Closures are the most important for many basic JavaScript patterns: namespaces, modules, private vars, memos being the most famous.

As an example, let’s see how to mock an object’s private variables: With this pattern, utilizing closures, we can create wrappers for property names with our own setters and getters. ES5 makes this process much easier because you can create objects using objects with getters and setters for properties and control access to the properties themselves in the most granular way.

var func = new Function(['a', 'b', 'c'], '');
function func(a, b, c) { }
function test() {}
//  prints  "object"
console.log(typeof test.prototype);
//  prints  function Function() { [native code] }
console.log(test.constructor);
function func(a, b, c) { }
//  prints "func"
console.log(func.name);
//  prints 3
console.log(func.length);
function test() {
  console.log(test.custom);
}
test.custom = 123;
//  prints 123
test();

Closure summary

The following table describes closures in Java, Python and JavaScript:

Features|Java|Python|JavaScript|Warnings Close | Weak class in anonymous inner class (read-only) | Weak function in nested def (read-only) | Yes | Memory leak Memory mode | Must use shared object | Possibly use list or dictionary | Yes | Better use lazy evaluation Namespace/Module Pattern|Not required|Not required|Yes| Private property pattern | not required | not possible | yes | can cause confusion

in conclusion

In this article, I introduce three features of JavaScript that are often misunderstood by developers coming from different languages, especially Java and C. In particular, we discussed concepts such as scope, hosting, functions, and closures. If you would like to delve deeper into these topics, please read the following list of articles:

Article source

function makeCounter () {
  var i = 0;

  return function displayCounter () {
    console.log(++i);
  };
}
var counter = makeCounter();
//  prints 1
counter();
//  prints 2
counter();
var counterBis = makeCounter();
//  prints 1
counterBis();
//  prints 3
counter();
//  prints 2
counterBis();
function makeCounter(i) {
  return function displayCounter () {
    console.log(++i);
  };
}
var counter = makeCounter(10);
//  prints 11
counter();
//  prints 12
counter();
function Person(name) {
  return {
    setName: function(newName) {
      if (typeof newName === 'string' && newName.length > 0) {
        name = newName;
      } else {
        throw new TypeError("Not a valid name");
      }
    },
    getName: function () {
      return name;
    }
  };
}

var p = Person("Marcello");

// prints "Marcello"
a.getName();

// Uncaught TypeError: Not a valid name
a.setName();

// Uncaught TypeError: Not a valid name
a.setName(2);
a.setName("2");

// prints "2"
a.getName();