【进阶 2 期】作用域闭包

零、总结

作用域:变量的生命周期(变量在生命周期内才有效)

  • 全局作用域
  • 局部作用域

闭包:一个函数的返回值是另外一个函数,那么这就是闭包。

闭包的两个特点:

  • 是函数
  • 定义在一个函数的内部,可以访问这个函数的内部变量

一、作用域

变量的生命周期(变量在生命周期内才有效)

可以理解成,{} 包起来的范围,就是作用域的范围。超出这个范围,变量就失效了。

1.1 全局作用域

全局变量:能被整个程序的任何函数访问。

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
myName = 'zhongxia'

function getName() {
/*
执行的时候,JS引擎先找当前作用域是否有该变量,有就使用,没有的话,则往外面找,找到外层作用域有一个`myName`的变量,因此这里输出 `zhongxia`
*/
console.log(myName) // zhongxia
}

function getInnerName() {
/*
因为 `var` 声明变量,会存在一个变量提升的现象。
不管声明的代码在哪一行,都会提升到该作用域的最前面(和声明函数一样)
*/
console.log(myName) // undefined
var myName = 'inner'
score = 100
console.log(myName) // inner
}

getName()
getInnerName()
console.log('outer:', myName) // zhongxia

/*
在 getName 里面声明 score变量,因为没有加 var或者 let,const, 因此认为是声明全局变量。
*/
console.log(score) // 100

1.2 局部作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var myName = 'zhongxia'

function getName() {
console.log(myName) // undefined
var myName = 'inner'
var age = '99'
console.log(myName) // inner
}

getName()
console.log(myName) // zhongxia

/*
getName 函数中声明的 age变量,只有在getName的作用域内 `{}内`才起作用
*/
console.log(age) // Uncaught ReferenceError: age is not defined

1.3 varletconst 声明变量的区别

  1. let ,const 是 ES6 新增的语法,声明的变量属于块级作用域,不存在变量提升
  2. var 声明的变量属于全局作用域,存在变量提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myName = 'zhongxia'

// 可以重复声明同名变量,下面的会覆盖上面的
var myName = 'modify-myName'

let age = 99

// 不能重复声明同名的变量
// let age = 80 // Uncaught SyntaxError: Identifier 'age' has already been declared

const score = 100
// const score = 99 // Uncaught SyntaxError: Identifier 'score' has already been declared

// const声明的变量,如果是值类型的变量,不能修改值
// score = 12 // Uncaught TypeError: Assignment to constant variable.

const obj = { myName: 'zhongxia' }
obj.myName = 'obj zhongxia'
console.log(obj) // {myName: "obj zhongxia"}

// 如果是引用类型,不能修改引用变量的引用地址
obj = {} // Uncaught TypeError: Assignment to constant variable.

var,let 作用域,变量提升的验证

变量提升:

  • JavaScript 中,函数及变量的声明都将被提升到函数的最顶部。
  • JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// var 变量提升
console.log(myName) // zhongxia
var myName = 'zhongxia'

// let 没有变量提升,因此
// console.log(age) // VM81:4 Uncaught ReferenceError: age is not defined
let age = 100

// 声明的是全局作用域,因此出了代码块外(`{}`范围),变量还能用
for (var i = 0; i < 5; i++) {
console.log(i) //分别输出 0,1,2,3,4
}
console.log('end:', i) // 5

// 声明的是块级作用域(局部作用域),因此出了代码块,变量就失效了,因此后面就报变量未声明的错误了
for (let j = 0; j < 5; j++) {
console.log(j) //分别输出 0,1,2,3,4
}
console.log(j) // Uncaught ReferenceError: j is not defined

1.4 为什么会出现闭包?

JavaScript 可以让函数内部访问外部的变量,但是如果需要让函数外部访问函数内部的变量

如何实现?

这个就是闭包要做的事情。

二、闭包是什么

闭包是函数和声明该函数的词法环境的组合。 —- MDN

这句话看了也不懂是什么意思。

闭包

  • 目的:为了让函数外部可以访问函数内部的值
  • 表现形式:一个函数返回值是另外一个函数
  • 原理:因为 JS 引擎的内存回收机制是引用计数法,在闭包里面有对 count变量的引用,因此变量不会被回收,因此常驻在内存中。

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getClosure() {
var myName = 'zhongxia'
var count = 0
return function() {
count++
console.log(count)
}
}

// console.log(myName) // Uncaught ReferenceError: count is not defined
// console.log(count) // Uncaught ReferenceError: count is not defined

var closure = getClosure()

closure() // 1
closure() // 2
closure() // 3
closure() // 4

三、应用场景

  • 自执行函数,封装第三方库,避免污染变量
  • 函数防抖,函数节流
  • 实现单例模式
  • 设置私有变量

3.1 自执行函数,用于控制全局变量作用范围,暴露接扣, 比如: Jquery

内部使用的变量名不会外部变量名污染,对外抛出可以使用的接口。

1
2
3
4
5
6
var jQuery = function() {
var jQuery = function() {
//TODO
}
return (window.$ = window.jQuery = jQuery)
}

3.2 函数防抖、函数节流

  • 函数防抖:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。
  • 函数节流:指定时间间隔内只会执行一次任务;

比如监听滚动事件,可以添加一个函数节流避免频繁触发, 注册用户的时候,需要实时判断用户名是否注册,可以使用函数防抖。

1
2
3
4
5
6
7
8
9
10
11
12
// 函数防抖
function debounce(fn, interval = 300) {
let timeout = null
return function() {
clearTimeout(timeout)
timeout = setTimeout(() => {
fn.apply(this, arguments)
}, interval)
}
}

$('input').on('change', debounce(fn, 300))

3.3 使用闭包设计单例模式

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
class CreateUser {
constructor(name) {
this.name = name;
this.getName();
}
getName() {
return this.name;
}
}

// 代理实现单例模式
var ProxyMode = (function() {
var instance = null;
return function(name) {
if(!instance) {
instance = new CreateUser(name);
}
return instance;
}
})();

// 测试单体模式的实例
var a = ProxyMode("aaa");
var b = ProxyMode("bbb");

// 因为单体模式是只实例化一次,所以下面的实例是相等的
console.log(a === b); //true

3.4 设置私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let _width = Symbol()

class Private {
constructor(s) {
this[_width] = s
}
foo() {
console.log(this[_width])
}
}

var p = new Private('50')
p.foo()
console.log(p[_width]) //可以拿到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//赋值到闭包里
let sque = (function() {
let _width = Symbol()

class Squery {
constructor(s) {
this[_width] = s
}
foo() {
console.log(this[_width])
}
}
return Squery
})()

let ss = new sque(20)
ss.foo()
console.log(ss[_width])

四、存在的坑

闭包使用的场景很多,正常的使用问题是没有问题,但是使用不当的话,可能会造成内存泄露。
对于浏览器端,内存泄露问题还不大,毕竟长时间停留在一个页面的用户比较少的。
但是对于需要持续运行的后端服务来说,由于使用闭包使用不当,造成的内存泄露问题就比较严重了,可能造成内存溢出。

4.1 什么是内存泄漏?

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

上面内存泄露解释来自《JavaScript 内存泄漏教程》

4.2 闭包使用不当,造成内存泄露的案例

闭包内,不当的变量引用导致内存无法释放

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
var theThing = null

function LeakClass() {}

function MakeLeakObjects(count) {
var arr = []
for (var i = 0; i < count; ++i) {
arr.push(new LeakClass())
}
return arr
}

var replaceThing = function() {
var unused = function() {
if (originalThing)
// 这里使用了originalThing,导致 originalThing 无法释放
console.log('hi')
}

var originalThing = theThing
theThing = {
leaks: MakeLeakObjects(1000),
someMethod: function() {}
}
}

setInterval(replaceThing, 1000)

把这段代码复制到 chrome 控制台执行, 然后点击 Memory=>Profiles=》Heap snapshot 查看内存使用情况。

刚开始运行

LeadClass 实例变成 5000 个了

几十秒后,变成了 36000 个了,随着程序运行时间越长,内存泄露的越来越大

10 分钟后

去掉 unused 函数后, 查看内存快照中, LeakClass 的数量一直是 1000 个,内存泄露问题就解决了。

浏览器内存泄露一点无所谓,但是持续运行的后端服务, 就有问题了。

不过也不用太担心,造成内存泄露的,不是闭包,而是写代码的你,写出了一个 BUG。

五、参考内容

  • 《MDN-作用域》
  • 《MDN-Closures》
  • 《学习 Javascript 闭包(Closure)》
  • 《从作用域链谈闭包》