闭包

闭包是一个难懂又必须征服的概念。闭包的形成与变量的作用域以及变量的生存周期密切相关。

变量的作用域

当在函数中声明一个变量的时候,如果该变量前面没有var关键字,这个变量就会成为全局变量,这当然是一种容易造成命名冲突的做法。
另一种就是用var关键字声明函数中的变量,这个时候的变量就是局部变量,只有在该函数内部才能访问到这个变量,在函数外部访问不到的

代码

  1. var func = function () {
  2. var a = 1
  3. console.log(a)
  4. }
  5. func()
  6. console.log(a)

输出
  1. 1
  2. Uncaught ReferenceError: a is not defined

代码
  1. var func = function () {
  2. a = 1
  3. console.log(a)
  4. }
  5. func()
  6. console.log(a)

输出
  1. 1
  2. 1

变量的生命周期

除了变量的作用域之外,另外一个跟闭包有关的概念是变量的生存周期。

对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。

而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了 它们的价值,它们都会随着函数调用的结束而被销毁:

代码
  1. var func = function () {
  2. var a = 1
  3. console.log(a)
  4. }
  5. func()

输出
  1. 1

闭包代码

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

输出
  1. 2
  2. 3
  3. 4
  4. 5

闭包的作用

封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘积的
简单函数

代码
  1. var mult = function () {
  2. var a = 1
  3. for (var i = 0; i < arguments.length; i++) {
  4. a = a * arguments[i]
  5. }
  6. return a
  7. }
  8. mult(1,2,3,4,5)

输出
  1. 120

加入缓存代码

  1. var cache = {}
  2. var mult = function () {
  3. var args = Array.prototype.join.call(arguments, ',')
  4. if (cache[args]) {
  5. return cache[args]
  6. }
  7. var a = 1
  8. for (var i = 0; i < arguments.length; i++) {
  9. a = a * arguments[i]
  10. }
  11. return (cache[args] = a)
  12. }
  13. console.log(mult(1, 2, 3))
  14. console.log(mult(1, 2, 3))
  15. console.log(cache)
  16. console.log(mult(3, 2, 1))
  17. console.log(cache)

输出
  1. 6
  2. 6
  3. {1,2,3: 6}
  4. 6
  5. {1,2,3: 6, 3,2,1: 6}

上面的代码暴露在了全局作用域中

  1. var mult = (function () {
  2. var cache = {}
  3. return function () {
  4. var args = Array.prototype.join.call(arguments, ',')
  5. if (cache[args]) {
  6. return cache[args]
  7. }
  8. var a = 1
  9. for (var i = 0; i < arguments.length; i++) {
  10. a = a * arguments[i]
  11. }
  12. return (cache[args] = a)
  13. }
  14. })()
  15. console.log(mult(1, 2, 3))
  16. console.log(mult(1, 2, 3))
  17. console.log(mult(3, 2, 1))

输出
  1. 6
  2. 6
  3. 6

延续局部变量的寿命

  1. var report = function (src) {
  2. var img = new Image()
  3. img.src = src
  4. }
  5. report('http://xxxx')

问题

但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在 bug,在这些浏览器 下使用 report 函数进行数据上报会丢失 30%左右的数据,也就是说,report 函数并不是每一次 都成功发起了 HTTP 请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的 调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求 就会丢失掉。

现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:

  1. var report = (function () {
  2. var imgs = []
  3. return function (src) {
  4. var img = new Image()
  5. imgs.push(img)
  6. img.src = src
  7. }
  8. })()

闭包与内存管理

闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。

局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上看,闭包的确会使一些数据无法被及时 销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要 使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设为 null。

跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非 JavaScript 的问题。在 IE 浏览器中,由于 BOM 和 DOM 中的对象是使用 C++以 COM 对象 的方式实现的,而 COM 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。

同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null 即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运 行时,就会删除这些值并回收它们占用的内存。

高阶函数

高阶函数值指至少满足下列条件之一的函数。

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

    函数可以作为参数被传递

    回调函数
    ```javascript var getUserInfo = function (userId, callback) { $.ajax(‘http://xxxxxxx?userId=‘ + userId, function (data) { callback(data) }) }

getUserInfo(123, function (data) { console.log(data) })

  1. 回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这 些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。<br />比如,我们想在页面上创建100div节点,然后把这些节点设置为隐藏
  2. <a name="m5593"></a>
  3. ##### 原来代码
  4. ```javascript
  5. var appendDiv = function () {
  6. for (var i = 0; i < 100; i++) {
  7. var div = document.createElement('div')
  8. dix.innerHTML = i
  9. document.body.appendChild('div')
  10. div.style.display = 'none'
  11. }
  12. }
  13. appendDiv()
  1. div.style.display = 'none'写在方法里面显得有些不合理

委托后代码
  1. var appendDiv = function (callback) {
  2. for (var i = 0; i < 100; i++) {
  3. var div = document.createElement('div')
  4. dix.innerHTML = i
  5. document.body.appendChild('div')
  6. if (typeof callback === 'function') {
  7. callback(div)
  8. }
  9. }
  10. }
  11. appendDiv(function (node) {
  12. node.style.display = 'none'
  13. })

Array.prototype.sort

  1. [1, 2, 3, 4, 5, 6].sort(function (a, b) {
  2. return a - b
  3. })
  4. // 从大到小排序

逗号运算符
  • 上面的代码我写错了
  • 逗号运算符,返回最后一个
    1. [(1, 2, 3, 4, 5, 6)].sort(function (a, b) {
    2. return a - b
    3. })
    4. // 最后返回的是:6
    1. [1, 2].reduce((t,c) => (t+=c, t), 0)

    函数作为返回值输出

    判断数类型

    代码
    ```javascript var isString = function (obj) { return Object.prototype.toString.call(obj) === ‘[object String]’ }

var isArray = function (obj) { return Object.prototype.toString.call(obj) === ‘[object Array]’ }

var isNumber = function (obj) { return Object.prototype.toString.call(obj) === ‘[object Number]’ }

  1. - 我们发现大多数的代码都是相同的
  2. <a name="lUMXh"></a>
  3. ##### 优化后的代码
  4. ```javascript
  5. var isType = function (type) {
  6. return function (obj) {
  7. return Object.prototype.toString.call(obj) === '[object ' + type + ']'
  8. }
  9. }
  10. var isString = isType('String')

高阶函数的其他应用

函数节流

  • throttle

    分时函数

    一个例子是创建 WebQQ 的 QQ 好友列表。列表中通常会有成百上千个好友,如果一个好友 用一个节点来表示,当我们在页面中渲染这个列表的时候,可能要一次性往页面中创建成百上千 个节点。

在短时间内往页面中大量添加 DOM 节点显然也会让浏览器吃不消,我们看到的结果往往就 是浏览器的卡顿甚至假死。

代码
  1. var arr = []
  2. for (var i = 1; i < 1000; i++) {
  3. arr.push(i)
  4. }
  5. var renderFriendList = function (data) {
  6. for (var i = 0; i < data.length; i++) {
  7. var div = document.createElement('div')
  8. div.innerHTML = i
  9. document.body.appendChild(div)
  10. }
  11. }
  12. renderFriendList(arr)
  • 优化,每200毫秒创建8个节点
    优化后代码
    1. var timeChunk = function (arr, fn, count) {
    2. var obj
    3. var t
    4. var len = arr.length
    5. var start = function () {
    6. for (var i = 0; i < Math.min(count || 1, arr.length); i++) {
    7. var obj = arr.shift()
    8. fn(obj)
    9. }
    10. }
    11. return function () {
    12. t = setInterval(function () {
    13. if (arr.length === 0) {
    14. return clearInterval(t)
    15. }
    16. start()
    17. }, 200)
    18. }
    19. }
    测试代码
    1. var ary = []
    2. for (var i = 1; i <= 1000; i++) {
    3. ary.push(i)
    4. }
    5. var renderFriendList = timeChunk(
    6. ary,
    7. function (n) {
    8. var div = document.createElement('div')
    9. div.innerHTML = n
    10. document.body.appendChild(div)
    11. },
    12. 8
    13. )
    14. renderFriendList()

    惰性加载函数