There are several flavors of JavaScript function. All are JSObjects of the same class, js_FunctionClass.

(But note that objects of other classes can be callable and can even have typeof obj == "function".)

Script functions

Functions written in JavaScript and compiled to bytecode. There are four variations that affect how NameExpressions are evaluated. (NameExpressions are basic expressions like String and x that would eat up a huge amount of run time if the engine weren't smart enough to avoid symbol table lookups.)

General closures are the base case. When the function object is created, its parent is set to the first object on the scope chain. When a name is evaluated that doesn't refer to a local variable, the interpreter consults the scope chain to find the variable. When with or eval are used, we have to do this for correctness.

This is slow, not only because walking the scope chain is a drag, but also because we'd rather avoid actually creating the scope chain at all, if possible. General closures force the interpreter to verify the whole scope chain. So we optimize this when we can.

These optimizations depend on being sure that we will never have to walk the scope chain, so with and eval inhibit them all.

Null closures. If we can prove at compile time that a function does not refer to any locals or arguments of enclosing functions, it is a null closure. Since it will never need to walk the scope chain, its parent is the global object. This is the best case. Barring with and eval, all outermost functions are null closures.

Null closures with upvars. A nested function is algol-like if it is only ever defined and called, and it isn't accessed in any other way (and it is not a generator-function). Such a function is guaranteed never to be called again after the enclosing function exits. An algol-like function may read the local variables and arguments of its immediate enclosing function from the stack, as if by magic. (JSContext::display caches the enclosing function's stack frame.) If that function is also algol-like, its child can read locals and variables from the next enclosing function, and so on. The compiler detects these cases and makes such functions null closures too.

Flat closures. Suppose a function reads some variables from enclosing functions but is not algol-like. If the function does not assign to any closed-on vars/args, and it only reads closed-on local variables and arguments that never change value after the function is created, then the function can be implemented as a flat closure. When a flat closure is created, all the closed-on values are copied from the stack into reserved slots of the function object. To evaluate a name, instead of walking the scope chain, we just take the value from the reserved slot. The function object's parent is the global object.

Flat closures and Null closures have been removed:

https://bugzilla.mozilla.org/show_bug.cgi?id=730497

https://bugzilla.mozilla.org/show_bug.cgi?id=739808

Name lookups

In order of speed, fastest to slowest.

  1. ARG and LOCAL instructions. If a name definitely refers to an argument or local variable of the immediately enclosing function, it can be accessed using JSOP_{GET,SET,CALL}{ARG,LOCAL} instructions. Some of these can even be fused with other operations into combo instructions like JSOP_GETARGPROP, JSOP_FORLOCAL, and JSOP_INCLOCAL. Because arguments and locals can't be deleted, this optimization is available to all functions, and eval does not interfere with it. But a with block can:

    function f(s) {
        eval(s);
        print(s);  // s can be loaded with GETARG
        with (obj) {
            print(s);  // no GETARG here; s might refer to obj.s
        }
    }
    
  2. UPVAR instructions. JSOP_{GET,CALL}UPVAR (in algol-like functions only, I think?) TODO

  3. DSLOT instructions. JSOP_{GET,CALL}DSLOT (in flat closures only) TODO

  4. GVAR instructions. Outside all functions, if a name definitely refers to a global for which we have seen a var, const, or function declaration, then we emit a JS_DEFVAR instruction in the script prelude and access the global using JSOP_{GET,SET,CALL}GVAR. This is fast if the global either doesn't exist before the script runs (the script creates it) or it's a non-configurable data property (which amounts to the same thing). Otherwise the GVAR instructions are as slow as NAME instructions.

    There are also combo instructions JSOP_{INC,DEC}GVAR and JSOP_GVAR{INC,DEC}.

  5. NAME instructions. In the worst case we emit JSOP_{,SET,CALL}NAME. These are totally general. They can still be optimized via the property cache; in the worst case we walk the scope chain.

    In some cases, the JIT can optimize a JSOP_NAME instruction that refers to a variable in an enclosing scope to pull the value directly out of the Call object's dslots.

    For the expression delete name we always emit JSOP_DELNAME. It's not worth optimizing.