0%

作用域与闭包

作用域

作用域有三种:

  • 全局作用域
  • 函数作用域
  • let\const 与 {} 形成的块级作用域

变量引用规则

  • 编译器运行时会将变量定义在:当前所在的作用域
  • 使用变量时会从当前作用域开始向上查找变量
  • 作用域就像攀亲亲一样,晚辈总是可以向上辈要些东西
  • 默认请况下:父级作用域不能够访问子级作用域的变量(使用闭包解决),但是子级作用域却可以访问祖先作用域的变量。

使用规范

​ 作用域链只向上查找,找到全局window即终止,应该尽量不要在全局作用域中添加变量。

函数被执行后其环境变量将从内存中删除。下面函数在每次执行后将删除函数内部的total变量。

1
2
3
4
function count() {
let total = 0;
}
count();

函数每次调用都会创建一个新作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
let site = '阿顺';

function a() {
let as = 'ashuntefannao.com';

function b() {
let cms = 'Ashun.com';
console.log(as);
console.log(site);
}
b();
}
a();

如果子函数被外部(window作用域)使用时,父级环境将被保留,因此在全局作用域能够间接访问函数内的变量。这就是闭包的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
function as() {
let n = 1;
return function() {
let b = 1;
return function() {
console.log(++n);
console.log(++b);
};
};
}
let a = as()();
a(); //2,2
a(); //3,3

构造函数也是很好的环境例子,子函数被外部使用父级环境将被保留。

通过这两个例子可以发现,闭包可以保护某些变量,不被外部直接访问,而是让外部通过接口(子函数、方法)进行间接访问。

1
2
3
4
5
6
7
8
9
10
11
function User() {
let a = 1;
this.show = function() {
console.log(a++);
};
}
let a = new User();
a.show(); //1
a.show(); //2
let b = new User();
b.show(); //1

let/const

使用 let/const 可以将变量声明在块作用域中(放在新的环境中,而不是全局中)

1
2
3
4
5
6
7
8
{
let a = 9;
}
console.log(a); //ReferenceError: a is not defined
if (true) {
var i = 1;
}
console.log(i);//1

也可以通过下面的定时器函数来体验

1
2
3
4
5
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 500);
}

for 循环中使用let/const 会在每一次迭代中重新生成不同的变量

1
2
3
4
5
let arr = [];
for (let i = 0; i < 10; i++) {
arr.push((() => i));
}
console.log(arr[3]()); //3 如果使用var声明将是10

在没有let/const 的历史中使用以下方式产生作用域

1
2
3
4
5
6
7
8
//自行构建闭包
var arr = [];
for (var i = 0; i < 10; i++) {
(function (a) {
arr.push(()=>a);
})(i);
}
console.log(arr[3]()); //3

闭包

闭包指子函数可以访问外部作用域变量的函数特性,即使在子函数所在的作用域外也可以访问。如果没有闭包那么在处理事件绑定,异步请求时都会变得困难。

  • 闭包一般在子函数本身作用域以外执行。
  • 闭包一般的形式:函数套函数,在子函数中使用父级作用域的变量,将子函数return出去。
  • 但闭包的形式并不是固定的,其本质是:**当前作用域块存在对父级作用域块的引用**

基本示例

前面在讲作用域时已经在使用闭包特性了,下面再次重温一下闭包。

  • 以下三例代码虽然形式不同,但都实现了内部作用域的变量,间接的让外部访问。
1
2
3
4
5
6
7
8
9
10
11
function first() {
let a = 1;
return function second() {
return ++a;
};
}

let func = first();
console.log(func());//2
console.log(func());//3
console.log(func());//4
1
2
3
4
5
6
7
8
9
10
11
12
function first() {
let a = 1;
function second() {
return ++a;
}
window.func = second;
}

first();
console.log(func());//2
console.log(func());//3
console.log(func());//4
1
2
3
4
5
6
7
8
9
10
let fuc;
function first() {
let a = 0;
fuc = function () {
console.log(++a);
};
}
first();
fuc();
fuc();

使用闭包返回数组区间元素

1
2
3
4
5
6
7
let arr = [3, 2, 4, 1, 5, 6];
function between(a, b) {
return function(v) {
return v >= a && v <= b;
};
}
console.log(arr.filter(between(3, 5)));

下面是在回调函数中使用闭包,当点击按钮时显示当前点击的是第几个按钮。

  • 利用立即执行函数创建作用域,形成闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<button message="ashun">button</button>
<button message="SHUN">button</button>
</body>
<script>
var btns = document.querySelectorAll("button");
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = (function(i) {
return function() {
alert(`点击了第${i + 1}个按钮`);
};
})(i);
}
</script>
  • 使用普通函数结合call\apply
1
2
3
4
5
6
7
8
9
……
var btns = document.querySelectorAll("button");
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
return function () {
alert(`点击了第${i + 1}个按钮`);
};
}.call(btns[i], i);
}

移动动画

计时器中使用闭包来获取独有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<button message="阿顺">Ashun</button>
<button message="阿顺特烦恼">Ashuntefannao</button>
</body>
<script>
let btns = document.querySelectorAll("button");
btns.forEach(function (elem) {
let checked = false;
let L = 0;
elem.addEventListener("click", function () {
!checked &&
(checked = setInterval(function () {
elem.style.marginLeft = ++L + "px";
}, 40));
});
});
</script>

闭包排序

下例使用闭包按指定字段排序

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
let lessons = [
{
title: "媒体查询响应式布局",
click: 89,
price: 12
},
{
title: "FLEX 弹性盒模型",
click: 45,
price: 120
},
{
title: "GRID 栅格系统",
click: 19,
price: 67
},
{
title: "盒子模型详解",
click: 29,
price: 300
}
];
function order(field) {
return (a, b) => (a[field] > b[field] ? 1 : -1);
}
console.table(lessons.sort(order("price")));

内存泄漏

闭包特性中上级作用域会为函数保存数据,从而造成的如下所示的内存泄漏问题

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div desc="ashun">阿顺</div>
<div desc="Ashuntefannao">阿顺特烦恼</div>
</body>
<script>
let divs = document.querySelectorAll("div");
divs.forEach(function(item) {
item.addEventListener("click", function() {
console.log(item.getAttribute("desc"));
});
});
</script>

下面通过清除不需要的数据解决内存泄漏问题

1
2
3
4
5
6
7
8
let divs = document.querySelectorAll("div");
divs.forEach(function(item) {
let desc = item.getAttribute("desc");
item.addEventListener("click", function() {
console.log(desc);
});
item = null; //及时清空引用。
});