# 零、总结

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

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

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

闭包的两个特点:

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

# 一、作用域

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

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

# 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
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

# 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 1.3 varletconst 声明变量的区别

  1. let ,const 是 ES6 新增的语法,声明的变量属于块级作用域,不存在变量提升
  2. 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.
1
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
1
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
1
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)
}
1
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))
1
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
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

# 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]) //可以拿到
1
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])
1
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)
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

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

刚开始运行

LeadClass 实例变成 5000 个了

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

10 分钟后

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

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

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

# 五、参考内容

Last Updated: 12/22/2019, 4:19:58 PM