Open su37josephxia opened 2 years ago
闭包的应用场景 闭包就是一种保护机制,应用场景如下:
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
完中间件之后,才会复制成真的
把中间件的函数通过 reduce
在了一起,实际上每个函数都会接受上一个函数的返回值作为参数
实际上中间件函数有一个固定的套路
({ getState, dispatch }) => next => action => next(action)
这里 dispatch
会被惰性赋值为 compose
的结果,next
实际上是 store.dispatch
,那么这么写的好处就是我可以对于 action
有不同的处理,不一定要直接调用 store.dispatch
,而且可以在里边函数拿到 store.dispatch
和 compose
了所有中间件的那个最外边的 dispatch
好久没看 Redux 源码了,原来都重构成 TS 啦。。。 刚刚才温习了下,所以答案可能有错误
柯里化是把接收多个参数的函数变成接收单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数且返回结果的新函数。例如: 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)的形式被调用的转化
函数柯里化是闭包的一种典型使用方法
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 方法, 用于移除对变量的观察
自定义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;
}
任何使用了回调函数的场景,其实都是在使用闭包。
比如:定时器、事件监听器、ajax请求、跨窗口通信、Web Workers或其他任何的异步(或同步)任务中。
var testNum = closePackage(); testNum(); testNum();`
此时输出0,1,说明num变量在外部可以被访问到且num在closePackage()执行完成后没有被销毁或回收。
sum(1,2);
function sum1(x){ return function(y){ console.log(x+y); } }
sum1(1)(2)` 输出结果都是3.
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.
本质上,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。这也是利用了闭包
上一道题目回答了什么是闭包,和闭包的产生原因。本期来简单介绍一下闭包的两个经典使用场景。
场景一 保护数据
如vue2的双向数据绑定中,通过闭包实现定义对象属性的时候,重定义set和get方法,从而达到保护数据的作用。
场景二 缓存数据
在vue源码中,发布订阅模式底层收集依赖是基于闭包实现的,用于缓存数据。
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直接更改,达到所谓隐藏数据的效果
在函数内部需要保存一个状态,该状态可读取,可修改,但是又不想被外部直接访问到。
比如在封装 axios
的时候,需要传入 url
、method
、param
等参数,最后返回一个方法供后续调用。
比如我们有一个函数 foo
,可以将输入的数字保留两位小数。此时我们需要一个函数,可以把输入数字保留两位小数并每隔三位添加一个逗号,这个时候就可以把函数 foo
引入进来,并在之前基础上添加每隔三位添加逗号的功能。
1.保护内部变量,用户只能通过固定函数来访问,例如 对象私有属性,私有作用域变量 2.不污染全局,例如 立即执行函数 3.使将来某个时间调用的函数在使用某些变量时维持这些变量在此函数声明时的值,例如 定时器,延时器,或者异步回调 4.利用内部变量来保存一些值,使这些值始终保存在内存中,例如单例模式,惰性函数,以及缓存记忆
闭包可以用在我们想要保存持久的变量的地方,他们不会在函数调用的时候重新生成赋值。
IIFE:实现私有作用域,避免污染全局作用域。常见的有一些公用库比如Jquery,Lodash等库都会使用闭包来封装。
也可以使用闭包来实现单例模式。
延时计算:比如for循环中的定时器由于异步会造成内部输出问题。而使用闭包传参可以避免这种问题。
闭包的作用
可以读取函数内部的变量。 让这些变量的值始终保持在内存中。 避免变量污染全局 把变量存到独立的作用域,作为私有成员存在 模拟私有方法,模块封装
应用场景一
典型应用是模块封装,在各模块规范出现之前,都是用这样的方法防止变量污染全局。
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]();
在项目中闭包应用的场景,
function a(fn) {
let flag = true
return async function (...arg) {
if (flag) {
flag = false
await fn(...arg)
}
flag = true
}
}
var c = a((a, b) => {
new Promise(rs => {
setTimeout(() => {
rs()
}, 2000)
})
})
在javascript中闭包其实最要用来缓存数据, 所以说有很多的应用场景
私有作用域/循环赋值
一个经典例子是每秒执行1次,分别输出1-10
//每秒执行1次,分别输出1-10
for(var i=1;i<=10;i++){
(function(j){
//j来接收
setTimeout(function(){
console.log(j);
},j*1000);
})(i)//i作为实参传入
}
每次循环都把setTimeout里的函数和外层函数的j保存下来了,等到最后setTimeout里的函数统一执行时,就是拿出循环当时保存的j的值输出。也就是每秒执行1次,分别输出1-10的效果了。
IIFE(自执行函数)
var n = 'song';
(function p(){
console.log(n)
})()
// 输出song
同样也是产生了闭包p()
,存在 window
下的引用 n
。
柯里化(返回函数)
function add (a) {
return function(b) {
return a + b
}
}
add(1)(2)
// 3
一个函数可以访问它被创建时的上下文环境被称为闭包。因此,开发中常用的定时器、回调函数、web Wokers、立即执行函数等都是在使用闭包,还有柯里化函数也是在使用闭包,vue中使用的单例模式创建vue实例也是在使用闭包
定义私有作用域
防抖函数
单例模式 这里由单例模式详细举例,移动端项目中我们经常需要一个全局的弹框,弹框全局可调用,且希望不管调用多少次都只被实例化一次。
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
闭包是处理状态的。所有OOP处理的场景,都是闭包的处理场景。
如果一个过程它是无状态的,那么用纯函数就解决问题了。但是如果说一个过程是有状态的,那么在传统的面向对象编程中,我们是把状态存储在对象中的,而在函数式编程这块儿,我们就把状态存储在闭包中。
所以通过闭包我们就可以做到原来在OOP编程中做到的,比如说像单例模式/缓存/私有属性这种东西。
闭包是处理状态的。所有OOP处理的场景,都是闭包的处理场景。
如果一个过程它是无状态的,那么用纯函数就解决问题了。但是如果说一个过程是有状态的,那么在传统的面向对象编程中,我们是把状态存储在对象中的,而JS在这块儿,我们就可以把状态存储在闭包中。
所以通过闭包我们就可以做到原来在OOP编程中做到的,比如说像单例模式/缓存这种东西。
如果一个函数可以使用另一个函数的内部变量,那么他就是一个闭包; 像单例模式就是利用了闭包的特性,他可以保证一个类只有一个实例; 也可以利用闭包去模拟私有属性; 还有柯里化函数,回调函数等都闭包具体的呈现。
闭包的使用场景
// 模拟私有属性
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
// 单例模式
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'
函数柯里化的实现
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);
}
}
}
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)
}
}
}
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);
};
}
})();
闭包的应用场景最常见的有三个,防抖节流函数的实现,嵌套函数维持外层函数变量的引用,函数柯里化的实现
https://juejin.cn/post/6844904165672484871#heading-10