Confusion about different [[Scope]] between Function Declaration and Function Expression

# Kang-Hao (Kenny) Lu (13 years ago)

(Resending on behalf of 张立理 <otakustay at live.com> because his mail

didn't go through.)

According to section 13 Function Definition of ECMAScript-262 5.1, the Function Declaration production and the Function Expression production are nearly the same, except when they are in the process of Create Function Objects, in which case they pass different Scope argument: Function Declaration passes the VariableEnvironment while Function Expression passes the LexicalEnvironment. This confuses me a little.

Suppose we have this code:

function test() {
    var x = 1;
    var o = { x: 2 };
    with (o) {
        eval('function foo() { console.log(x); }');
        eval('var bar = function() { console.log(x); }');
    }
    foo();
    bar();
}
test();

If I understand correctly, here are 2 facts:

  • outside with statement, the VariableEnvironment and LexicalEnvironment refer to the same object, here we name it outerEnv.
  • inside with statement, we have a new LexicalEnvironment (object environment), the VariableEnvironment stays the same, here we name the new LexicalEnvironment innerEnv.

The problem lies in the 2 direct eval calls. Without the strict flag, the eval function shares the same LexicalEnvironment and VariableEnvironment as its calling context. When the eval is invoked in a with statement, VariableEnvironment and LexicalEnvironment are different, so we expect foo and bar outputs different numbesr: foo should output 1 and bar should output 2.

The fact is, the edge version of Chrome, Firefox and IE7-10, all output 2 and 2.

Why is ECMAScript-262 5.1 documenting a behavior that's different from nearly all modern browsers? Should these browsers consider their implementations buggy, or should our standard change a little? Anyway, I could not image a situation when Function Declaration and Function Expression should use different objects as their [[Scope]] internal property, why not use LexicalEnvironment of its calling context as all browsers do?

The catch statement has the same problem:

function test() {
    var x = 1;
    try {
        throw 2;
    }
    catch (x) {
        eval('function foo() { console.log(x); }');
        eval('var bar = function() { console.log(x); }');
    }
    foo();
    bar();
}
test();

Thanks.

Gray Zhang

电子邮件:otakustay at live.com 微博:@otakustay 博客:otakustay.com

# Allen Wirfs-Brock (13 years ago)

ECMAScript 5.1 does not syntactically permit a FunctionDeclaration to directly appear nested within a Block. The semantics provided by the specification are those for a function declaration at the top level of a function. The only standard way to evaluate a function declaration within a nested lexical environment (established by either a with statement or a catch clause) is via a direct eval. Direct eval uses the normal semantics for binding FunctionDeclaration so the function binds to the top level of the enclosing function.

Most ECMAScript implementation are extended (beyond what is in the standard) to allow a FunctionDeclaration to appear within a nested scope. While this syntactic extension is common, there isn't a single common semantics for the extension. Some implementations bind the function name scoped to the block others bind the name scoped to the enclosing function. Regardless, if an implementation supports inner scope syntactic FunctionDeclarations is makes sense that they would use the same semantics if the FunctionDeclaration is introduced via a direct eval call. That is presumably what you are observing.

There may be a reasonable argument to be made that ES5.1 should be scoping such inner scope direct eval introduced FunctionDeclaration to the inner scope. Or, that ES5.1 should not allow such declaration to be introduced via direct eval. However, the point will soon be moot as ES6 will allow inner scoped FunctionDeclarations and will provided a standard semantics that scopes them to the immediately inclosing scope.

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

Some implementations bind the function name scoped to the block others bind the name scoped to the enclosing function.

Detailed aside: no standard implementation actually binds to a block scope (yet -- this is proposed for ES6 and implementations are appearing, e.g. under a flag in V8 in Chrome).

Rather, the implementations bind the function "sub-statement" by its name in the outer function's activation but either always, as if hoisted independent of control flow (last in source order among several for the same name wins), or (SpiderMonkey in Firefox, Rhino, possibly others) binding the name as if by assignment if and only if control flow reaches the nested function declaration.

Web content relies on the intersection semantics. E.g.,

function outer() { ... if (cond) { function inner() { ... } obj.callback = inner; } ... }

Fortunately, ES6's block-scoped semantics for functions declared in blocks is in this intersection.