作用域和闭包

作用域

[[scope]]:

  • 函数创建时,生成的一个JS内部的隐式属性
  • 函数存储【作用域链】的容器
    • 作用域链:
      • AO - 函数的执行期上下文
      • GO - 全局的执行期上下文
  • 函数执行完成以后,AO会被销毁,再执行会重新创建一个新的AO
  • 全局执行的前一刻 GO -> 函数声明已经定义

🌈 案例

1
2
3
4
5
6
7
8
9
function a() {
function b() {
var b = 2;
}
var a = 1;
b();
}
var c = 3;
a();

当a函数被定义时(每一个函数被定义的时候【还没被执行】,[[scope]]就存在了,作用域链第一项此刻已经被保存了,是GO):

当a函数被执行时(前一刻,也就是预编译阶段)(生成了自己的AO,并且把AO存在了作用域链的最顶端,把GO挤下去了)

a函数执行,内部b函数立即被定义

当b函数被执行时(前一刻,预编译),把自身AO排顶端,先前的被挤到下边

b函数执行完毕以后,b的ao销毁;b的scope回归b被定义时的状态

b函数执行完毕的同时,也代表a函数执行完毕。此时a的AO销毁,由于a的AO中包含b,而且被销毁,所以 b的[[scope]]也不存在了

分析作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function a(){
function b(){
function c(){
}
c();
}
b();
}
a();

a定义: a.[[scope]] -> 0: GO
a执行: a.[[scope]] -> 0:a的AO
1:GO
b定义: b.[[scope]] -> 0:a的AO
1:GO
b执行: b.[[scope]] -> 0:b的AO
1:a的AO
2:GO
c定义: c.[[scope]] -> 0:b的AO
1:a的AO
2:GO
c执行: c.[[scope]] -> 0: c的AO
1:b的AO
2:a的AO
3:GO
c结束:c.[[scope]] -> 0:b的AO
1:a的AO
2:GO
b结束:b.[[scope]] -> 0:a的AO
1:GO


a结束:c.[[scope]] 销毁
b.[[scope]] 销毁
a.[[scope]] -> 0: GO

🌈 函数默认值带来的影响

事实上,如果一些(至少有一个)参数具有默认值,ES6 会定义一个中间作用域用于存储参数,并且这个作用域与函数体的作用域不共享。这是与 ES5 存在主要区别的一个方面。我们用例子来证明:

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 1;

function foo(x, y = function() { x = 2; }) {
var x = 3;
y(); // `x` 被共用了吗?
console.log(x); // 没有,依然是 3,不是 2
}

foo();

// 并且外部的 `x` 也不受影响
console.log(x); // 1

在这个例子中,我们有三个作用域:全局环境,参数环境,以及函数环境:

1
2
3
foo : -> { x: 3 }
参数 : -> { x: undefined, y: function() { x = 2; } }
全局 : -> { x: 1 }

我们可以看到,当函数 y 执行时,它在最近的环境(即参数环境)中解析 x,函数作用域对其并不可见。

来源:https://juejin.cn/post/6844903839280136205#heading-5

总结

  1. 只要函数被定义,就生成作用域(scope)和相应的作用域链(scope chain),并把GO放进去
  2. 只要函数被执行的那一刻(准确的说应该是执行前的预编译阶段),就生成AO,然后把自身的AO放进作用域链,并自身排首位,把之前的作用域们(其他AO和GO)依次往下挪
  3. 函数执行完毕,销毁自身AO;此时自身作用域回归被定义时的状态

闭包

案例

1
2
3
4
5
6
7
8
9
10
11
12
function test1() {
function test2() {
var b = 2;
console.log(a);
}
var a = 1;
return test2;
}

var c = 3;
var test3 = test1();
test3();

当test1被定义时:(此时全局的test3是test1内部的test2)

当test1被执行时,test2被定义,所以此刻他们俩的作用域相同

当test1执行完毕时,本来要销毁自身的AO,但由于test1返回了test2,导致test2被全局GO引用,所以test2的作用域链中的test1的AO此刻还不能销毁,因此此AO仍然存在,没有被销毁,只是test1的作用域链中的自身AO连接切断了。
test1的AO不能被销毁的理由是,test2现在在全局函数,而且test2的作用域链中还攥着test1的ao,所以test1的ao不能被销毁。由于垃圾回收的标记清除,导致test2是全局的,还被引用着,不能被销毁。

当test3执行时,

当test3执行完毕后,

总结

当内部函数被返回到外部并保存时,一定会产生闭包。闭包会导致原来的作用域链不释放。过度的闭包可能会导致内存泄漏(因为常驻内存),或加载过慢

for循环的i值打印

代码执行后的结果是什么,为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function test() {
var arr = [];
for(var i = 0; i < 10; i++) {
arr[i] = function() {
document.write(i + ' ');
}
}
return arr;
}

var myArr = test();

for(var j = 0; j < 10; j++) {
myArr[j]();
}

// 打印 10个10
// 首先最上边test函数可以改造为下面形式:
function test() {
var arr = [];
var i = 0; // 把for 循环的变量 i 声明提出来
for(;i < 10;) {
arr[i] = function() {
document.write(i + ' ');
}
i++; // 每次循环完毕后i++,所以提下来
}
return arr;
}

🌈 分析

当此 for 循环执行完后,arr 中存储了 10 个匿名函数,这 10 个匿名函数还没执行,所以先不看匿名函数里边的内容。
由于 test 执行完毕后把 arr 返回了出去形成了闭包,所以每个匿名函数其实都保留了外层test函数的AO。
所以每个匿名函数就能拿到 test AO 中的变量 i ,而 i 在 for 循环结束后已经累加为了 10
所以最终在循环执行每个匿名函数时,都拿到的是最终的 i 的值,也就是 10

如何改造才能使其打印 0 - 9

在外层使用立即执行函数包裹
1
2
3
4
5
6
7
8
function test() {
for(var i = 0; i < 10; i++) {
(function() {
document.write(i + ' ');
})();
}
}
test();
直接函数尾部加括号,因为前边有 arr[i] = ,是个表达式
1
2
3
4
5
6
7
8
9
10
11
function test() {
var arr = [];
for(var i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i + ' '); // 0 - 9
}();
}
return arr;
}

console.log(test()); // [undefined, ...] 10 个 undefined
循环立即执行函数+把传参传进匿名函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// or 

function test() { // 最常用的方案
var arr = [];
for(var i = 0; i < 10; i++) {
(function(j) {
arr[j] = function() {
document.write(j + ' ');
}
})(i);
}
return arr;
}

var myArr = test();

for(var j = 0; j < 10; j++) {
myArr[j]();
}


// or 利用函数传参(耍赖)
function test() {
var arr = [];
for(var i = 0; i < 10; i++) {
arr[i] = function(num) {
document.write(num + ' ');
}
}
return arr;
}

var myArr = test();

for(var j = 0; j < 10; j++) {
myArr[j](j);
}

window 和 return

return 形成闭包

1
2
3
4
5
6
7
8
9
10
11
12
function test() {
var a = 1;
function add1() {
a++;
console.log(a);
}
return add1;
}
var add = test();
add(); // 2
add(); // 3
add(); // 4

window 形成闭包

1
2
3
4
5
6
7
8
9
10
11
12
function test() {
var a = 1;
function add1() {
a++;
console.log(a);
}
window.add = add1;
}
test();
add(); // 2
add(); // 3
add(); // 4
  • 自执行函数
1
2
3
4
5
6
7
8
9
10
11
var add = (function () {
var a = 1;
function add() {
a++;
console.log(a);
}
return add;
})();
add();
add();
add();
1
2
3
4
5
6
7
8
9
10
11
(function () {
var a = 1;
function add() {
a++;
console.log(a);
}
window.add = add;
})();
add();
add();
add();

JS 插件写法

1
2
3
4
5
6
7
8
(function() {
function Test() {

}
Test.prototype = {}
window.Test = Test;
})();
var test = new Test();