闭包

121 阅读4分钟

闭包

定义

闭包(Closure) 通常理解是 外部函数使用了内部函数的私有变量,导致私有变量无法释放

使用场景

1. 科里化函数

将多变量函数拆解为单变量(或部分变量)的多个函数并依次调用

例1 参数复用

function uri_curring(protocol) { 
    return function(hostname, pathname) {
        return `${protocol}${hostname}${pathname}`; 
      } 
  } // 测试一下 
const uri_https = uri_curring('https://'); 
const uri1 = uri_https('www.fedbook.cn', '/frontend-languages/javascript/function-currying/'); 
const uri2 = uri_https('www.fedbook.cn', '/handwritten/javascript/10-实现bind方法/'); 
const uri3 = uri_https('www.wenyuanblog.com', '/');
console.log(uri1); 
console.log(uri2); 
console.log(uri3);

例2 兼容性检测

因为浏览器的发展和各种原因,有些函数和方法是不被部分浏览器支持的,此时需要提前进行判断,从而确定用户的浏览器是否支持相应的方法。

以事件监听为例,IE(IE9 之前) 支持的是 attachEvent 方法,其它主流浏览器支持的是 addEventListener 方法,我们需要创建一个新的函数来进行两者的判断。

const addEvent  = function(element, type, listener, useCapture) {
  if(window.addEventListener) {
    console.log('判断为其它浏览器')
    // 和原生 addEventListener 一样的函数
    // element: 需要添加事件监听的元素
    // type: 为元素添加什么类型的事件
    // listener: 执行的回调函数
    // useCapture: 要进行事件冒泡或者事件捕获的选择
    element.addEventListener(type, function(e) {
      // 为了规避 this 指向问题,用 call 进行 this 的绑定
      listener.call(element, e);
    }, useCapture);
  } else if(window.attachEvent) {
    console.log('判断为 IE9 以下浏览器')
    // 原生的 attachEvent 函数
    // 不需要第四个参数,因为 IE 支持的是事件冒泡
    // 多拼接一个 on,这样就可以使用统一书写形式的事件类型了
    element.attachEvent('on' + type, function(e) {
      listener.call(element, e);
    });
  }
}

// 测试一下
let div = document.querySelector('div');
let p = document.querySelector('p');
let span = document.querySelector('span');

addEvent(div, 'click', (e) => {console.log('点击了 div');}, true);
addEvent(p, 'click', (e) => {console.log('点击了 p');}, true);
addEvent(span, 'click', (e) => {console.log('点击了 span');}, true);

上面这种封装的弊端是:每次写监听事件的时候调用 addEvent 函数,都会进行 if...else... 的兼容性判断。事实上在代码中只需要执行一次兼容性判断就可以了,把根据一次判定之后的结果动态生成新的函数,以后就不必重新计算。

那么怎么用函数柯里化优化这个封装函数?

// 使用立即执行函数,当我们把这个函数放在文件的头部,就可以先进行执行判断
const addEvent  = (function() {
  if(window.addEventListener) {
    console.log('判断为其它浏览器')
    return function(element, type, listener, useCapture) {
      element.addEventListener(type, function(e) {
        listener.call(element, e);
      }, useCapture);
    }
  } else if(window.attachEvent) {
    console.log('判断为 IE9 以下浏览器')
    return function(element, type, handler) {
      element.attachEvent('on'+type, function(e) {
        handler.call(element, e);
      });
    }
  }
}) ();

// 测试一下
let div = document.querySelector('div');
let p = document.querySelector('p');
let span = document.querySelector('span');

addEvent(div, 'click', (e) => {console.log('点击了 div');}, true);
addEvent(p, 'click', (e) => {console.log('点击了 p');}, true);
addEvent(span, 'click', (e) => {console.log('点击了 span');}, true);

上述封装因为立即执行函数的原因,触发多次事件也依旧只会触发一次 if 条件判断。

这里使用了函数柯里化的两个特点:提前返回和延迟执行

例3:实现一个 curry 函数

const add = (a,b,c)=>a+b+c;
let a1 = curry(add,1)
let a2 = a1(2)
let a3 = a2(3)
a3() // 6 也就是一个累加的过程,在面试中比较常见
functon curry(fn,...arg){
let totalArg = arg;
let fnLength = fn.length;
// 这个返回的函数就是 a1 ,a2
    return fnRes = (...newArg)=>{
    // 收集 新的 arguments
        totalArg = [...newArg,...totalArg]
        if(totalArg.length == fnLength){
        // 因为 totalArg 是个数组,需要展开
         return fn(...totalArg)
        }else {
            return fnRes
        }
    }
}

实现私有化变量

由于刚开始没有模块的概念,又担心命名冲突,所以使用闭包来隔绝作用域,现代 webpack 打包结果是 匿名自执行性函数的

function One(i){
    return function Two(){
        console.log(i)
   }
}
let a = One(1)
let b = One(2)
//在 Two 中使用了 One 中传递的变量 i;每次返回的都是一个新函数

匿名自执行函数

实现保护变量的作用,外部无法获取 num 值,保护了 num不会发生命名冲突,和上述的 私有变量 差不多

var funOne = (function One(){
    var num = 0;
    return Two(){
        num++;
        return num
    }
})()
funOne() //1
funOne() //2
funOne() //3

缓存结果

let memoize = (fn) => {
  let cache = {}; 
  return function() {
    let key = JSON.stringify(arguments)
    cache[key] = cache[key] || fn.apply(this, arguments)
    return cache[key]
  }
}
let add = memoize((a,b)=>a+b)
add(1,3)
add(1,3) // 这个就会执行的是 cache

note:记录关于闭包的一些知识,便于以后自己复习,2022.5.15 18:18