闭包

521 阅读7分钟

题目:什么是闭包?

解答

概念

  • 有权访问另一个函数作用域中的变量的函数,一般情况就是在一个函数中包含另一个函数

实现原理

  • 在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量

作用

  • 访问函数内部变量、保持函数在环境中一直存在,不会被垃圾回收机制处理

应用

在函数内使用函数外的变量:函数作为返回值

  • 闭包作用:避免变量被环境污染
funciton F1() {
  var a = 100
  return function() {
    console.log(a)
  }
}
var f1 = F1()
var a = 200
f1() // 100

函数作为参数传递

function F1() {
  var a = 100
  return function() {
    console.log(a)
  }
}
var f1 = F1()
function F2(fn) {
  var a = 200
  fn()
}
F2(f1) // 100

将函数与其所操作的某些数据关联起来

  • 通常,你使用只有一个方法的对象的地方,都可以使用闭包

用闭包模拟私有方法

循环里面的闭包

怎样才能实现输层0-5呢?
题目
for(var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}
// 55555
方法一:为每一个回调创建一个新的词法环境
function makeCb(i) {
  return function() {
      console.log(i)
  }
}
for(var i = 0; i < 5; i++) {
  setTimeout(makeCb(i), 1000)
} // 01234
方法二:使用匿名闭包
for(var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i)
    })
  })(i)
}
// 01234
使用let声明变量
for(let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}
// 01234

知识点

  • 词法作用域和动态作用域
  • js的作用域和作用域链
  • js执行上下文栈
  • 堆栈溢出和内存泄漏
  • 柯里化

词法作用域和动态作用域

词法作用域
  • 函数的作用域在函数定义的时候决定
动态作用域
  • 函数的作用域在函数调用的时候决定
js采用词法作用域
  • js函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的
var scope = 'global scope'
function checkscope() {
  var scope = 'local scope'
  function f() {
    return scope
  }
  return f()
}
checkscope()
// local scope
var scope = 'global scope'
function checkscope() {
  var scope = 'local scope'
  function f() {
    return scope
  }
  return f()
}
checkscope()()
// local scope

js的作用域和作用域链

作用域
全局作用域
  • 全局作用域在代码中任何地方都能被访问
局部作用域
  • 局部作用域一般只在固定的代码片段内可以被访问
作用域访问规则
  • 在作用域中访问变量时,首先会在当前作用域中查找
    • 如果找到则直接使用
    • 没找到就向外层作用域查找
      • 如果找到则直接使用
      • 没找到就继续向外层作用域查找,直到全局作用域
        • 如果全局作用域也没有找到,则报错
作用域链
  • 当查找变量的时候,会先从当前上下文的变量对象中查找
  • 如果没有找到,就会从父级执行上下文的变量对象中查找
  • 一直找到全局执行上下文的变量对象,也就是全局对象
  • 这样由多个执行上下文的变量对象构成的链条

js执行上下文栈

执行上下文栈类型
  • 全局上下文
  • 函数上下文 -- 函数被调用时才创建
  • eval函数执行上下文
执行上下文组成
  • 词法环境
  • 变量环境
  • this值

堆栈溢出和内存泄漏

堆栈溢出
  • 是指内存空间已经被申请完,没有足够的内存提供了

程序代码运行都需要一定的计算存储空间--栈,栈遵循先进后出的原则,所以程序从栈底开始运行计算,程序内部函数的调用以及返回会不停的执行进栈和出栈的操作,栈内被所占的资源也在不断的对应变化,但是一旦你的调用即进栈操作过多,返回即出栈不够,这时候就会导致栈满了,再进栈的就会溢出来

内存泄漏
  • 是指申请的内存执行完后没有及时地清理或者销毁,占用空闲内存,内存泄漏过多的话,就会导致后面的程序申请不到内存。因此内存泄露会导致内存溢出
解决办法
  • 标记清除法
    • 在一个变量进入执行环境后就给它添加一个标记:进入环境
    • 进入环境的变量不会被释放,因为只要“执行流”进入响应的环境,就可能用到他们
    • 当变量离开环境后,则将其标记为“离开环境”。
预防方法
  • 减少不必要的全局变量
  • 减少闭包的使用(因为闭包会导致内存泄漏)
  • 避免死循环的发生

节流和防抖

节流&防抖.png

防抖(执行最后一次)

当持续触发事件时,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行

应用
  1. search搜索联想,用户在不断输入值时,用防抖来节约请求资源
  2. window触发resize时,不断地调整浏览器窗口大小会不断触发该事件,用防抖来让其只触发一次
分解
  1. 持续触发不执行
  2. 不触发的一段时间之后再执行
实现
  1. 不触发的一段时间之后再执行

定时器里面调用要执行的函数,将arguments传入

  • 封装一个函数,将目标函数(持续触发的事件)作为回调传进去,等待一段时间过后执行目标函数
function debound(func, delay) {
 return function() {
   setTimeout(() => {
     func.apply(this, arguments)
   }, delay)
 }
}
  1. 持续触发不执行

防抖1.png

function debound(func, delay) {
  let timeout
  return function() {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}
  1. 用法
box.onmousemove = debound(function(e) {
  box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)
节流(执行第一次)

让函数有节制地执行,即在一段时间内,只执行一次

应用
  1. 鼠标不断点击触发,mousedownmousemove
  2. 监听滚动事件,比如是否滑到底部自动加载更多
分解
  1. 持续触发并不会执行多次
  2. 到一定时间再去执行
实现

持续触发,并不会执行,但是到时间了就会执行

  • 关键点: 执行的时机
  1. 要做到控制执行的时机,可以通过一个开关,与定时器setTimeout结合完成
  2. 函数执行的前提是开关打开,持续触发时,持续关闭开关,等到setTimeout到时间了,再把开关打开,函数就会执行了
function throttle(func, delay) {
  let run = true
  return function() {
    if(!run) {
      return
    }
    run = false
    setTimeout(() => {
      func.apply(this, argument)
      run = true
    }, delay)
  }
}
  • 用法
box.onmousemove = throttle(function(e) {
  box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)
  • 节流还能用时间间隔去控制

    如果当前事件与上次执行时间的时间差大于一个值,就执行

节流1.png

函数柯里化currying

什么是柯里化

把函数完全变成「接受一个参数;返回一个值」的固定形式,这样对于讨论和优化会更加方便。

  • 柯里化是一种 将使用多个参数的一个函数转换成一系列使用一个参数的函数 的技术
一个简单的柯里化函数
function sum (a, b) {
    console.log(a + b);
}
sum(1, 2); // 3
// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}
curryingAdd(1)(2) // 3
柯里化的目的:减少代码冗余,以及增加代码的可读性
柯里化的好处
  • 参数复用
  • 提前确认
  • 延迟执行
参数复用
function check(reg, txt) {
  return reg.text(txt)
}
check(/\d+/g, 'test')  // false
check(/[a-z]+/g, 'test') // true

function curryingCheck(reg) {
  return function(txt) {
    return reg.test(txt)
  }
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('1212') // false
提前确认
延迟执行
封装柯里化
function curry (fn, currArgs) {
    return function() {
        let args = [].slice.call(arguments);
        // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
        if (currArgs !== undefined) {
            args = args.concat(currArgs);
        }
        // 递归调用
        if (args.length < fn.length) {
            return curry(fn, args);
        }
        // 递归出口
        return fn.apply(null, args);
    }
}
//这样就可以直接调用curry了
  • currArgs 是调用 curry 时传入的参数列表
  • currArgs !== undefined 的判断,是为了解决递归调用时的参数拼接
测试
function sum(a, b, c) {
    console.log(a + b + c);
}

const fn = curry(sum);

fn(1, 2, 3); // 6
fn(1, 2)(3); // 6
fn(1)(2, 3); // 6
fn(1)(2)(3); // 6
柯里化性能
  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … )fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
编程 -- 实现一个add方法,使计算结果能够满足如下预期
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
console.log(add(1)(2)(3))   // f 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9
Object.prototype.toString() 函数的隐式转换

为什么上面输出的是f 6而不是6?

function add() {
    return 20
}
console.log(add + 10) // function add 20
function add() {
    return 20
}
add.toString = function() {
    return 10
}
console.log(add + 10) // 20

function add() {
    return 20
}
add.valueOf = function() {
    return 5
}
add.toString = function() {
    return 10
}
 
console.log(add + 10) // 15
  • 当我们没有重新定义toStringvalueOf时,函数的隐式转换会调用默认的toString方法,它会将函数的定义内容作为字符串返回。
  • 而当我们主动定义了toString/vauleOf方法时,那么隐式转换的返回结果则由我们自己控制了。
  • 其中valueOf会比toString后执行
总结
  • 函数的柯里化,是 js 中函数式编程的一个重要概念。它返回的是一个函数的函数。其实现方式,需要依赖参数以及递归,通过拆分参数的方式,来调用一个多参数的函数方法,以达到减少代码冗余,增加可读性的目的。