# 零、总结
作用域:变量的生命周期(变量在生命周期内才有效)
- 全局作用域
- 局部作用域
闭包:一个函数的返回值是另外一个函数,那么这就是闭包。
闭包的两个特点:
- 是函数
- 定义在一个函数的内部,可以访问这个函数的内部变量
# 一、作用域
变量的生命周期(变量在生命周期内才有效)
可以理解成,{}
包起来的范围,就是作用域的范围。超出这个范围,变量就失效了。
# 1.1 全局作用域
全局变量:能被整个程序的任何函数访问。
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
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
# 1.2 局部作用域
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.3 var
,let
,const
声明变量的区别
- let ,const 是 ES6 新增的语法,声明的变量属于块级作用域,不存在变量提升
- var 声明的变量属于全局作用域,存在变量提升
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.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# var,let
作用域,变量提升的验证
变量提升:
- JavaScript 中,函数及变量的声明都将被提升到函数的最顶部。
- JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.4 为什么会出现闭包?
JavaScript 可以让函数内部访问外部的变量,但是如果需要让函数外部访问函数内部的变量。
如何实现?
这个就是闭包
要做的事情。
# 二、闭包是什么
闭包是函数和声明该函数的词法环境的组合。 ---- MDN
这句话看了也不懂是什么意思。
闭包
- 目的:为了让函数外部可以访问函数内部的值
- 表现形式:一个函数返回值是另外一个函数
- 原理:因为 JS 引擎的内存回收机制是引用计数法,在闭包里面有对
count
变量的引用,因此变量不会被回收,因此常驻在内存中。
看个例子:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 三、应用场景
- 自执行函数,封装第三方库,避免污染变量
- 函数防抖,函数节流
- 实现单例模式
- 设置私有变量
# 3.1 自执行函数,用于控制全局变量作用范围,暴露接扣, 比如: Jquery
内部使用的变量名不会外部变量名污染,对外抛出可以使用的接口。
var jQuery = function() {
var jQuery = function() {
//TODO
}
return (window.$ = window.jQuery = jQuery)
}
2
3
4
5
6
# 3.2 函数防抖、函数节流
- 函数防抖:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。
- 函数节流:指定时间间隔内只会执行一次任务;
比如监听滚动事件,可以添加一个函数节流避免频繁触发, 注册用户的时候,需要实时判断用户名是否注册,可以使用函数防抖。
// 函数防抖
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))
2
3
4
5
6
7
8
9
10
11
12
# 3.3 使用闭包设计单例模式
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
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
# 3.4 设置私有变量
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]) //可以拿到
2
3
4
5
6
7
8
9
10
11
12
13
14
//赋值到闭包里
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])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 四、存在的坑
闭包使用的场景很多,正常的使用问题是没有问题,但是使用不当的话,可能会造成内存泄露。 对于浏览器端,内存泄露问题还不大,毕竟长时间停留在一个页面的用户比较少的。 但是对于需要持续运行的后端服务来说,由于使用闭包使用不当,造成的内存泄露问题就比较严重了,可能造成内存溢出。
# 4.1 什么是内存泄漏?
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
上面
内存泄露
解释来自《JavaScript 内存泄漏教程》
# 4.2 闭包使用不当,造成内存泄露的案例
闭包内,不当的变量引用导致内存无法释放
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)
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
把这段代码复制到 chrome 控制台执行, 然后点击 Memory=>Profiles=》Heap snapshot 查看内存使用情况。
刚开始运行
LeadClass 实例变成 5000 个了
几十秒后,变成了 36000 个了,随着程序运行时间越长,内存泄露的越来越大
10 分钟后
去掉 unused
函数后, 查看内存快照中, LeakClass 的数量一直是 1000 个,内存泄露问题就解决了。
浏览器内存泄露一点无所谓,但是持续运行的后端服务, 就有问题了。
不过也不用太担心,造成内存泄露的,不是闭包,而是写代码的你,写出了一个 BUG。