具名IIFE的意义

具名IIFE的意义

之前,我以为它们一样

在今天之前的相当长一段时间,我都觉得IIFE没什么大不了的,无非就是把两段代码写成了一段,只是一种简写方式。

像这样的代码:

(function hello() {
    console.log("hello");
})();

我是觉得完全可以写成这样的形式:

function hello() {
    console.log("hello");
}
hello();

事实上我也一直是这么做的:当我不怎么想用IIFE的时候,我就会选择这种看上去更清楚的分开写的方法,从来没有出过什么岔子。

直到今天,我发现了问题

今天在V站摸鱼的时候,发现了东哥的兄弟出的一道面试题,题目如下:

var a = 1;
(function a(){
a = 2;
console.log(a)})()

问打印的a是多少?

我自认为之前写了那篇关于词法环境的文章之后,这些问题都难不倒我。然而我还是错了。一开始我没有注意到函数和全局变量a同名,得出答案是2。但是,这么简单的答案,肯定是错的。然后我就开始看下面的讨论,第一个靠谱的回答是这样的:

rabbbit   6 小时 26 分钟前   ♥ 3

打印结果是 a 函数
---
比如,当访问函数内的 foo 变量时,JavaScript 会按照下面顺序查找:
当前作用域内是否有 var foo 的定义。
函数形式参数是否有使用 foo 名称的。
函数自身是否叫做 foo。
回溯到上一级作用域,然后从 #1 重新开始。
---
摘自 js 秘密花园

打印的a是函数a,这个答案够意外,那么标准答案肯定是它了。而且还给出了解释,很完美。但是我觉得事情肯定没有他解释的那么简单。于是我又接着看其他热心网友的回答。

摘抄几条:

JenJieJu   7 小时 46 分钟前

等价于:a1 和 a 代表不同指向;
window.a = undefined;
var a1 = 1;
window.a = function (){ a1 = 2; console.log(a) }
ayase252   7 小时 26 分钟前 via iPhone

https://developer.mozilla.org/en-US/docs/web/JavaScript/Reference/Operators/function

看 named function expresssion 那一节。把其他值赋值给 name 也改变不了 name
troywith77   6 小时 56 分钟前 via Android

a 函数内部的 a 指向函数本身,a=2 不生效,所以打印 a 本身
iMusic   5 小时 37 分钟前

(function a () {})

这是个函数表达式,这个 a 就是函数名称,它的特点是作为函数体(作用域内)的本地变量,不能被修改,也不能被外部访问。

上面的回答,只有最后一位老哥的“表达式”三个字给了我一点灵感。其余的人除了调侃JS都在说一件事情:在函数体内不能改变函数标识符绑定的值。那么真的是这样吗?我要做一个实验:

function a() {
    console.log(a); // [Function: a]
    a = 100;
    console.log(a); // 100
}
a();
console.log(a); // 100

结果与他们所说的相反,我可以很轻松地在函数a的函数体中将100赋值给a,这样一来,原来全局环境中的a就变成了100,我们再也找不到函数a。

是不是非严格模式的关系呢?

function b() {
    "use strict";
    console.log(b); // [Function: b]
    b = 100;
    console.log(b); // 100
}
b();
console.log(b); // 100

上面的代码证明:严格模式下也是可以给a重新赋值的。

是不是因为没有用IIFE呢?题目中是用的IIFE。

(function c() {
    console.log(c); // [Function: c]
    c = 100;
    console.log(c); // [Function: c]
})();
console.log(c); // ReferenceError: c is not defined

上面的代码不仅证明了非严格模式下,在IIFE中确实不可以给a重新赋值,而且还让我有了一个意外的发现,那就是作为IIFE执行的那个函数,在全局环境中并没有binding。说白了就是,全局环境中从来都没有出现过函数声明,从来没有一个名叫c的函数。

为什么呢?这就要牵扯到上面那个老哥说的“表达式”。我的理解就是,表达式产生一个值。一般情况下,用括号括起来的东西就是一个表达式,比如 (1 + 1),就是一个表达式,它产生了一个值是2。

(1 + 1)  // 2
(function c() {
    console.log(c); // [Function: c]
    c = 100;
    console.log(c); // [Function: c]
})

那么上面这段代码其实也是一个表达式,而不是一个赋值语句。它产生了一个值,这个值是一个函数,准确地说是一个函数的执行过程。在这个表达式后面加一对括号"()",就是执行这段过程。很明显,“生成一段过程并立即执行”这样一个操作,并没有向全局环境变量中添加函数名为c的函数——IIFE里的具名函数,既没有改变当前环境,又可以调用自身,很像是箭头函数却能做到箭头函数做不到的事。我觉得搞清这一点,对我们解题的帮助是巨大的。

好了,言归正传,由这段代码

(function c() {
    console.log(c); // [Function: c]
    c = 100;
    console.log(c); // [Function: c]
})();
console.log(c); // ReferenceError: c is not defined

我们可以知道,在非严格模式下,只有当函数是以IIFE形式运行时,才不能对函数标识符变量(这里是c)重新赋值。那么再看最初的题目:

var a = 1;
(function a(){
    a = 2;
    console.log(a)
})();

一切都是那么清晰:全局环境中的a始终没变过(一直是1),IIFE中的a = 2赋值语句静默失败了,所以a还是一个表达式生成的一个过程。这个过程打印出来就是 [Function: a]。(这里还有一些哲学的问题...以后填坑吧...)

附上我做的一些实验

function a() {
console.log(a); // [Function: a]
a = 100;
console.log(a); // 100
}
a();
console.log(a); // 100


function b() {
"use strict";
console.log(b); // [Function: b]
b = 100;
console.log(b); // 100
}
b();
console.log(b); // 100


(function c() {
console.log(c); // [Function: c]
c = 100;
console.log(c); // [Function: c]
})();
console.log(c); // ReferenceError: c is not defined


(function d() {
"use strict";
console.log(d); // TypeError: Assignment to constant variable.
d = 100;
console.log(d);
})();
console.log(d); // ReferenceError: d is not defined
var a = 100;
var b = 999;

(function a() {
console.log(b); // 999 根据词法环境规则,可以拿到外层的 b 的值

b = 888; // 也可以修改外层的 b 的值

a = 200; // 非严格模式 && IIFE, 此时 a 的值无法被修改, 静默失败

console.log(a); // [Function: a]

})(); //并没有在全局环境声明函数 a,而是用表达式产生一段程序,立即执行

console.log(a); // 100
console.log(b); // 888

上面也有一些哲学问题,有空我会去翻ECMAScript规范再回来更新的...