su37josephxia / frontend-interview

前端面试知识点
MIT License
159 stars 45 forks source link

闭包有什么应用场景? #22

Open su37josephxia opened 2 years ago

su37josephxia commented 2 years ago

https://juejin.cn/post/6844904165672484871#heading-10

QbjGKNick commented 2 years ago

闭包的应用场景 闭包就是一种保护机制,应用场景如下:

bianzheCN commented 2 years ago

Redux applyMiddleware

export default function applyMiddleware(
  ...middlewares: Middleware[]
): StoreEnhancer<any> {
  return (createStore: StoreEnhancerStoreCreator) =>
    <S, A extends AnyAction>(
      reducer: Reducer<S, A>,
      preloadedState?: PreloadedState<S>
    ) => {
      const store = createStore(reducer, preloadedState)
      let dispatch: Dispatch = () => {
        throw new Error(
          'Dispatching while constructing your middleware is not allowed. ' +
            'Other middleware would not be applied to this dispatch.'
        )
      }

      const middlewareAPI: MiddlewareAPI = {
        getState: store.getState,
        dispatch: (action, ...args) => dispatch(action, ...args)
      }
      const chain = middlewares.map(middleware => middleware(middlewareAPI))
      dispatch = compose<typeof dispatch>(...chain)(store.dispatch)

      return {
        ...store,
        dispatch
      }
    }
}

这块是为了保证在 applyMiddleWare 的时候不能 dispatch,所以在 API 里一开始是个假的 dispatch 函数,在 compose 完中间件之后,才会复制成真的

Redux compose

把中间件的函数通过 reduce 在了一起,实际上每个函数都会接受上一个函数的返回值作为参数

Redux 中间件函数

实际上中间件函数有一个固定的套路

({ getState, dispatch  }) => next => action => next(action)

这里 dispatch 会被惰性赋值为 compose 的结果,next 实际上是 store.dispatch,那么这么写的好处就是我可以对于 action 有不同的处理,不一定要直接调用 store.dispatch,而且可以在里边函数拿到 store.dispatchcompose 了所有中间件的那个最外边的 dispatch

注⚠️

好久没看 Redux 源码了,原来都重构成 TS 啦。。。 刚刚才温习了下,所以答案可能有错误

8023nana commented 2 years ago

柯里化是把接收多个参数的函数变成接收单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数且返回结果的新函数。例如: var add = function(x) { return function(y) { return x + y; }; }; var increment = add(1); var addTen = add(10); increment(2); // 3 addTen(2); // 12 add(1)(2); // 3 定义一个add函数,它接收一个参数并且返回一个新的函数。调用add函数后,返回的函数就通过闭包的方式记住了第一个参数。 柯里化是将add(a, b, c)已add(a)(b)(c)的形式被调用的转化

rachern commented 2 years ago

函数柯里化是闭包的一种典型使用方法

function currying(fn, args) {
  var _this = this
  var len = fn.length
  var args = args || []

  return function() {
    var _args = Array.prototype.slice.call(arguments)
    Array.prototype.push.apply(args, _args)

    if(_args.length < len) {
      return currying.call(this, fn, _args)
    }

    return fn.apply(this, _args)
  }
}

函数柯里化的作用是能够使得参数复用和函数的延迟执行

闭包的一个落地示例是在 vue 中的 $watch

Vue.prototype.$watch = function (expOrFn, cb, options) {
    const vm = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

用户调用 $watch 方法时,会实例化一个 Watcher 对象并保存在变量 watcher, 然后返回一个 unwatch 函数,在这个函数里面,调用了 watcher 下的 teardown 方法, 用于移除对变量的观察

chunhuigao commented 2 years ago

自定义hooks实现获取屏幕宽高

function useWinSize() {
    const [size, setSize] = useState({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    })

    const onResize = useCallback(() => {
        setSize({
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight
        })
    }, [])
    useEffect(() => {
        window.addEventListener('resize', onResize)
        return () => {
            window.removeEventListener('resize', onResize)
        }
    }, [])

    return size;

}

优点

qytayh commented 2 years ago

结论

任何使用了回调函数的场景,其实都是在使用闭包。

比如:定时器、事件监听器、ajax请求、跨窗口通信、Web Workers或其他任何的异步(或同步)任务中。

wzl624 commented 2 years ago
  1. 代替全变量,防止全局变量会被污染的情况。 `function closePackage(){ var num = 0; return function(){ console.log(num++); } }

var testNum = closePackage(); testNum(); testNum();`

此时输出0,1,说明num变量在外部可以被访问到且num在closePackage()执行完成后没有被销毁或回收。

  1. 函数柯里化===>多参数变单参数。 计算两个数的和 `function sum(x,y){ console.log(x+y); }

sum(1,2);

function sum1(x){ return function(y){ console.log(x+y); } }

sum1(1)(2)` 输出结果都是3.

  1. compose===>函数管道,一个变量需要通过多个方法执行后得出最终结果,变成一个变量经过一个函数即可得到最终结果。 一个数经过+-x/后得到一个结果 `function compute1(x){ return x+=2; } function compute2(x){ return x-=1; } function compute3(x){ return x*=4; } function compute4(x){ return x/=2; }

let res = compute4(compute3(compute2(compute1(0)))); console.log(res);

function compose(...funcs){ return (x)=>{ if (funcs.length === 0) { return x; } if (funcs.length === 1) { return funcs0; } return funcs.reduce((a,b)=>b(a),x) } }

let res1 = compose(compute1,compute2,compute3,compute4); console.log(res1(0));` 执行结果都是2.

zzzz-bang commented 2 years ago

本质上,JavaScript中并没有自己的模块概念,我们只能使用函数/自执行函数来模拟模块 每一个JS模块都可以认为是一个独立的作用域,当代码执行时,该词法作用域创建执行上下文,如果在模块内部,创建了可供外部引用访问的函数时,就为闭包的产生提供了条件,只要该函数在外部执行访问了模块内部的其他变量,闭包就会产生 在react的hook中,当useState在函数中执行时,访问了state中的变量对象,那么闭包就会产生。 根据闭包的特性,state模块中的state变量,会持久存在。因此当函数再次执行时,我们也能获取到上一次函数执行结束时state的值,这就是React Hooks能够让函数组件拥有内部状态的基本原理 在实现useState的源码中可以发现,updateWorkInProgressHook中,hook是包含了memoizedState, baseState, queue, baseUpdate, next属性的一个对象,我们的状态,就缓存在memoizedState这个值里,而memoizedState: currentHook.memoizedState。最终我们的状态,在update时,其实就是存在于currentHook。这也是利用了闭包

attackam commented 2 years ago

上一道题目回答了什么是闭包,和闭包的产生原因。本期来简单介绍一下闭包的两个经典使用场景。

场景一 保护数据

如vue2的双向数据绑定中,通过闭包实现定义对象属性的时候,重定义set和get方法,从而达到保护数据的作用。

场景二 缓存数据

在vue源码中,发布订阅模式底层收集依赖是基于闭包实现的,用于缓存数据。

Colin3191 commented 2 years ago
  1. IIFE:实现私有作用域,避免污染全局作用域。
  2. 柯里化:延迟计算
  3. 实现单例
  4. 计算缓存
674252256 commented 2 years ago

1》隐藏数据

// 闭包隐藏数据,只提供 API ` function createCache() { const data = {} // 闭包中的数据,被隐藏,不被外界访问 return { set: function (key, val) { data[key] = val }, get: function (key) { return data[key] } } }

const c = createCache() c.set('a', 100) console.log( c.get('a') ) ` 用户只能通过get set等api对数据进行查看和更改等操作,没法对data直接更改,达到所谓隐藏数据的效果

zhenyuWang commented 2 years ago

内部状态保存

在函数内部需要保存一个状态,该状态可读取,可修改,但是又不想被外部直接访问到。

回调函数

比如在封装 axios 的时候,需要传入 urlmethodparam 等参数,最后返回一个方法供后续调用。

函数柯里化

比如我们有一个函数 foo,可以将输入的数字保留两位小数。此时我们需要一个函数,可以把输入数字保留两位小数并每隔三位添加一个逗号,这个时候就可以把函数 foo 引入进来,并在之前基础上添加每隔三位添加逗号的功能。

BambooSword commented 2 years ago
  1. 保存自己私有变量,通过提供接口给外部使用,但外部不能直接访问该变量。
  2. 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
yl-code commented 2 years ago

答案写在我的语雀上

yaoqq632319345 commented 2 years ago

1.保护内部变量,用户只能通过固定函数来访问,例如 对象私有属性,私有作用域变量 2.不污染全局,例如 立即执行函数 3.使将来某个时间调用的函数在使用某些变量时维持这些变量在此函数声明时的值,例如 定时器,延时器,或者异步回调 4.利用内部变量来保存一些值,使这些值始终保存在内存中,例如单例模式,惰性函数,以及缓存记忆

zcma11 commented 2 years ago

闭包可以用在我们想要保存持久的变量的地方,他们不会在函数调用的时候重新生成赋值。

  1. 单例,我们可以在不同地方得到同一个实例,然后实现共享这个实例内的属性,并且里面的属性不会重新生成。
  2. iife匿名函数自调用,模块化,我们可以保留在函数作用域内声明的变量,但不会影响全局。
  3. 函数柯里化,通过保存传入参数返回一个新的函数,直到参数数量够了才返回结果,这样的好处是实现参数的复用。我们可以在一个地方,这里有只有他能处理的值,传一部分参数,把函数返回出去,在另外一个地方获得这个函数然后继续调用。可以减少代码的复杂性。
MMmaXingXing commented 2 years ago
  1. IIFE:实现私有作用域,避免污染全局作用域。常见的有一些公用库比如Jquery,Lodash等库都会使用闭包来封装。

  2. 也可以使用闭包来实现单例模式。

  3. 延时计算:比如for循环中的定时器由于异步会造成内部输出问题。而使用闭包传参可以避免这种问题。

partiallove commented 2 years ago

闭包的作用

可以读取函数内部的变量。 让这些变量的值始终保持在内存中。 避免变量污染全局 把变量存到独立的作用域,作为私有成员存在 模拟私有方法,模块封装

应用场景一

典型应用是模块封装,在各模块规范出现之前,都是用这样的方法防止变量污染全局。

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

应用场景二

var data = [];
for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();
yanzefeng commented 2 years ago

在项目中闭包应用的场景,

jiafei-cat commented 2 years ago

在javascript中闭包其实最要用来缓存数据, 所以说有很多的应用场景

jj56313751 commented 2 years ago
792472461 commented 2 years ago
superjunjin commented 2 years ago
alienRidingCat commented 2 years ago

一个函数可以访问它被创建时的上下文环境被称为闭包。因此,开发中常用的定时器、回调函数、web Wokers、立即执行函数等都是在使用闭包,还有柯里化函数也是在使用闭包,vue中使用的单例模式创建vue实例也是在使用闭包

aiuluna commented 2 years ago
JanusJiang1 commented 2 years ago
var Modal = (function () {
    var ModalClass = function () { }; // 创建弹框的构造函数,无法在外部直接调用
    var instance; // 声明一个instance对象
    return function () {
        if (instance) { // 如果已存在 则返回instance
            return instance;
        }
        instance = new ModalClass () // 如果不存在 则new一个
        return instance;
    }
})();
//不管以何种方式调用Modal 函数左后都只会返回唯一的instance实例
var a = Modal ();
var b = new Modal ();
console.log(a===b) // true
var a = ModalClass (); // 报错,ModalClass is not defined
rhythm022 commented 2 years ago

闭包是处理状态的。所有OOP处理的场景,都是闭包的处理场景。

如果一个过程它是无状态的,那么用纯函数就解决问题了。但是如果说一个过程是有状态的,那么在传统的面向对象编程中,我们是把状态存储在对象中的,而在函数式编程这块儿,我们就把状态存储在闭包中。

所以通过闭包我们就可以做到原来在OOP编程中做到的,比如说像单例模式/缓存/私有属性这种东西。

rhythm022 commented 2 years ago

闭包是处理状态的。所有OOP处理的场景,都是闭包的处理场景。

如果一个过程它是无状态的,那么用纯函数就解决问题了。但是如果说一个过程是有状态的,那么在传统的面向对象编程中,我们是把状态存储在对象中的,而JS在这块儿,我们就可以把状态存储在闭包中。

所以通过闭包我们就可以做到原来在OOP编程中做到的,比如说像单例模式/缓存这种东西。

Limeijuan commented 2 years ago

如果一个函数可以使用另一个函数的内部变量,那么他就是一个闭包; 像单例模式就是利用了闭包的特性,他可以保证一个类只有一个实例; 也可以利用闭包去模拟私有属性; 还有柯里化函数,回调函数等都闭包具体的呈现。

crazyyoung1020 commented 2 years ago

闭包的使用场景

  1. 私有作用域 可以在函数内定义参数,封装方法在闭包函数内供对象自己获取,外部无法通过访问key的方式直接访问这些私有属性。
// 模拟私有属性
function getGeneratorFunc () {
  var _name = 'John';
  var _age = 22;

  return function () {
    return {
      getName: function () {return _name;},
      getAge: function() {return _age;}
    };
  };
}

var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); // 22
obj._age; // undefined
  1. 单例模式 单例模式是设计模式的一种,一个类只有一个实例,在创建实例的时候会先判断是否已经有实例,如果有则直接返回,没有的话才创建,优点就是避免重复创建实例,减少内存开销。
// 单例模式
function Singleton(){
  this.data = 'singleton';
}

Singleton.getInstance = (function () {
  var instance;

  return function(){
    if (instance) {
      return instance;
    } else {
      instance = new Singleton();
      return instance;
    }
  }
})();

var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'
  1. 柯里化函数 柯里化函数就是将一个多参数的函数转化成多次调用每次只传一个参数即 f(x,y,z) => f(x)(y)(z)。柯里化函数的原理就是使用闭包,每次调用都会保存这次传入的参数到函数内部,直到最后一次调用结束才会销毁。 柯里化的优点有:
    1. 参数的复用,如果多次调用一个函数,传入的参数大部分都是相同的,可以考虑用柯里化函数,将前面相同参数的部分封装起来,再去接受后面不同的参数,减少函数的调用。
    2. 延迟计算,分段传入参数调用函数,等真正需要返回结果的时候,不传参数直接调用。

函数柯里化的实现

function curry(fn, ...args) {
    return function(...innerArgs) {
        const allArgs = [...args, ...innerArgs];

        if (fn.length <= allArgs.length) {
            // 说明已经接受完所有参数,这个时候可以执行了
            return fn.apply(this, allArgs);
        } else {
            // 继续返回函数,收集参数
            return curry(fn, ...allArgs);
        }
    }
}
  1. compose函数 容易和柯里化函数搞混,compose是将多个回调调用的函数整合成一个接受多个函数为参数的函数,即F(A(B(x)))=>P(F,A,B)(x) compose函数可以让整个js代码可读性更强。他的原理其实也是用到了闭包,实现如下
var compose = function(...args) {
        var len = args.length
        var count = len - 1
        var result
        return function f1(...args1) {
            result = args[count].apply(this, args1)
            if (count <= 0) {
                count = len - 1
                return result
            } else {
                count--
                return f1.call(null, result)
            }
        }
    }
  1. 惰性函数 函数内部的判断只会执行一次,后序再执行都不会再做判断逻辑。他的原理是使用了闭包,在内部做完条件判断后,返回了一个新的函数出来,等于是当前函数已经被改写了。 最经典的用法如下
    const addEvent = (function () {
    if (window.addEventListener) {
        return (elem, type, fn, capture) => {
            elem.addEventListener(type, (e) => fn.call(elem, e), capture);
        };
    } else {
        return (elem, type, fn, capture) => {
            elem.attachEvent('on' + type, (e) => fn.call(elem, e);
        };
    }
    })();
  2. 计算缓存 空间换时间。就是在多次调用计算方法的时候,其实有的值之前可能已经计算过了,我们可以利用闭包将计算结果存在函数内,计算之前先判断缓存内是否有,没有的话就再计算,更新缓存。
guoshukun1994 commented 2 years ago

闭包的应用场景最常见的有三个,防抖节流函数的实现,嵌套函数维持外层函数变量的引用,函数柯里化的实现