在JavaScript的立即执行的具名函数A内修改A的值时到底发生了什么?

如下代码:

(function A() {
    console.log(A); // [Function A]
    A = 1;
    console.log(window.A); // undefined
    console.log(A); // [Function A]
}())

可以看到注释里的输出。
这也就是说A = 1这一步什么都没有发生。它既没有改变A的值,也没有在window中添加新的属性。


而如下代码:

(function A() {
    console.log(A); // undefined
    var A = 1;
    console.log(window.A); // undefined
    console.log(A); // 1
}())

这个还是很好理解的,var语句被hoist到函数顶端,函数内定义了变量A但是没有赋值,所以第一个log是undefined,因为有var,并没有向global添加属性,因此window.A也是undefined。


接着,

function A() {
    console.log(A); // [Function A]
    A = 1;
    console.log(window.A); // 1
    console.log(A); // 1
}
A();

不在立即执行这个函数,结果也很符合直觉,打第一个log时,函数作用域内没有找到A,因此向上层查找,找到函数A;然后A = 1使得window对象多了一个属性,第二个log结果是1,第三个log显然也是1。


那么问题来了,第一段代码里,A = 1到底做了什么呢?

阅读 17.8k
5 个回答

先说结论: 第一段代码中 A = 1 试图修改自由变量 A的值,但没有生效!

(function A() {
    console.log(A); // [Function A]
    A = 1;
    console.log(window.A); // undefined
    console.log(A); // [Function A]
}())

这是一个立即执行的函数表达式(Immediately-invoked function expression, IIFE),更特殊的,该函数表达式是一个具名函数表达式(Named function expression, NFE)。

NFE 有两个好玩的特性:

  • 作为函数名的标识符(在这里是 A )只能从函数体内部访问,在函数外部访问不到 (IE9+)。(kangax
    有一篇 博客 详细讨论了 NFE 以及 IE6~8 的 JScript bug,推荐阅读!) ES5 Section
    13
    特别提及了这一点:

The Identifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the Identifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.

  • 绑定为函数名的标识符(在这里是A)不能再绑定为其它值,即该标识符绑定是不可更改的(immutable),所以在 NFE 函数体内对 A 重新赋值是无效的。ES5 Section 13 详细描述了创建 NFE 的机制:

The production FunctionExpression : function Identifier (
FormalParameterListopt ) { FunctionBody }
is evaluated as follows:

  1. Let funcEnv be the result of calling NewDeclarativeEnvironment passing the running execution context’s Lexical Environment as the argument
  2. Let envRec be funcEnv’s environment record.
  3. Call the CreateImmutableBinding concrete method of envRec passing the String value of Identifier as the argument.
  4. Let closure be the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in funcEnv as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.
  5. Call the InitializeImmutableBinding concrete method of envRec passing the String value of Identifier and closure as the arguments.
  6. Return closure.

注意步骤 3 和 5,分别调用了 createImmutableBinding 和 InitializeImmutableBinding 内部方法,创建的是不可更改的绑定

要理解这两个特性,最重要的是搞清楚标识符 A的绑定记录保存在哪里。让我们问自己几个问题:

  1. 标识符 A 与 该 NFE 是什么关系? 两层关系:首先,该 NFE 的 name 属性是 字符串 'A';更重要的是,A是该 NFE 的一个自由变量。在函数体内部,我们引用了 A,但 A 既不是该 NFE 的形参,又不是它的局部变量,那它不是自由变量是什么!解析自由变量,要从函数的 [[scope]] 内部属性所保存的词法环境 (Lexical Environment) 中查找变量的绑定记录。

  2. 标识符 A 保存在全局执行环境(Global Execution Context)的词法环境(Lexical Environment)中吗? 答案是否。如果你仔细看过 ES5 Section 13 这一节,会发现创建 NFE 比创建 匿名函数表达式 (Anonymous Function Expression, AFE) 和 函数声明 (Function Declaration) 的过程要复杂得多:

对于 创建 AFE 和 FD, 步骤是这样:

The production
FunctionDeclaration : function Identifier ( FormalParameterListopt ) { FunctionBody }
is instantiated as follows during Declaration Binding instantiation (10.5):

Return the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt, and body specified by FunctionBody. Pass in the VariableEnvironment of the running execution context as the Scope. Pass in true as the Strict flag if the FunctionDeclaration is contained in strict code or if its FunctionBody is strict code.

The production
FunctionExpression : function ( FormalParameterListopt ) { FunctionBody }
is evaluated as follows:

Return the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in the LexicalEnvironment of the running execution context as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.

比创建 NFE 简单多了有木有?那为么子创建 NFE 要搞得那么复杂呢?就是为了实现 NFE 的只能从函数内部访问A,而不能从外部访问这一特性!咋实现的? 创建 NFE 时,创建了一个专门的词法环境用于保存 A 的绑定记录(见上面步骤 1~3)!对于 NFE, 有如下关系:

A.[[scope]] --->  Lexical Environment {'environment record': {A: function ...}, outer: --}----> Lexical Environment of Global Context {'environment record': {...}, outer --}---> null

可见,A 的绑定记录不在全局执行上下文的词法环境中,故不能从外部访问!

找到了,在http://www.ecma-international.org/ecma-262/5.1/#sec-13,关于具名表达式的一段:

The production
FunctionExpression : function Identifier ( FormalParameterListopt ) { FunctionBody }
is evaluated as follows:
1.Let funcEnv be the result of calling NewDeclarativeEnvironment passing the running execution context’s Lexical Environment as the argument
2.Let envRec be funcEnv’s environment record.
3.Call the CreateImmutableBinding concrete method of envRec passing the String value of Identifier as the argument.
4.Let closure be the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in funcEnv as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.
5.Call the InitializeImmutableBinding concrete method of envRec passing the String value of Identifier and closure as the arguments.
6.Return closure.

NOTE The Identifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the Identifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.

上面第3和5条提到的CreateImmutableBinding和InitializeImmutableBinding两个方法(见Declarative Environment Records)会将具名函数的名称绑定到相应的函数对象(见Creating Function Objects)上,这个绑定是不可变的。

因此在函数表达式内可以通过函数名读取函数本身,但是无法改变函数名的引用。

由于JavaScript作用域,你的这个立即执行的函数表达式A被封在了内部。

我尝试着修改A的引用,但是失败了:

(function a(){
    alert(a);
    a = b;
    alert(a);   
    function b(){
        alert('b');
    }
})();

两个log依然是Function a

后来我尝试着解封a

function a(){
    alert(a);
    a = b;
    alert(a);
    function b(){
        alert('b');
    }
}

a();

解封成功!a的引用被改变!

后来我得到启发,尝试这样:

var a  = function b(){
    alert(b);
    b = c;
    alert(b);
    function c(){
        alert('c');
    }
};
a();

解封失败。

而这样:

b = c;
function c(){    
    alert('c');
}
var a  = function b(){}

就可以成功的改变函数的引用。

现在我也挺疑惑的。。。

强行记忆一波:在自执行的具名函数中,修改函数引用无效。哈哈哈...

新手上路,请多包涵

函数标识符可变,特殊情况不可变,

查资料的时候翻到了词法环境,其中提到了环境记录项:共有 2 类环境记录项: 声明式环境记录项 和 对象式环境记录项 。声明式环境记录项用于定义那些将 标识符 与语言值直接绑定的 ECMA 脚本语法元素,例如 函数定义 , 变量定义 以及 Catch 语句。对象式环境记录项用于定义那些将 标识符 与具体对象的属性绑定的 ECMA 脚本元素,例如 程序 以及 With 表达式 。

这里我觉得闭包内的具名函数是符合声明式环境记录项的定义,采纳中的英文也频繁出现了closure (闭包)这个单词

声明式环境记录项给出了两个额外的创建方法,其中关键词:不可变绑定

CreateImmutableBinding(N) 在环境记录项中创建一个未初始化的不可变绑定。其中字符串 N 指定绑定名称。
InitializeImmutableBinding(N,V) 在环境记录项中设置一个已经创建但未初始化的不可变绑定的值。其中字符串 N 指定绑定名称。V 用于指定绑定的值,可以是任何 ECMA 脚本语言的类型。

词法环境解释的地址:https://www.w3cschool.cn/wsqz...

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏