• JavaScript 闭包在高阶函数中的一个极其隐蔽的坑


    今天拜读大牛 Micheal Fogus 的神作『Functional JavaScript』,发现 JavaScript 闭包中一个极其隐蔽的坑,特此梳理,也希望能帮到更多后来人。

    言归正传。在第三章讨论闭包的问题时,大牛提到一个对判定条件取反的案例:

    function complement(PRED) {
        return function() {
            return !PRED.apply(null, _.toArray(arguments));
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    剔除 Underscore.js 的干扰,并简化为 ES6 的写法为:

    const complement = PRED => (...args) => !PRED.apply(null, args);
    
    • 1

    如果有一个判定偶数的谓词函数 isEven,并需要结合 complement 得到一个判定奇数的新函数 isOdd,可以写为:

    const complement = PRED => (...args) => !PRED.apply(null, args);
    let isEven = n => (n % 2) === 0;
    const isOdd = complement(isEven);
    isOdd(2); // => false
    isOdd(413); // => true
    
    • 1
    • 2
    • 3
    • 4
    • 5

    问题来了:如果将 isEven 篡改为其他函数,isOdd 会受影响吗?

    isEven = () => false;
    console.assert(isOdd(2) === true, 'isOdd 不受影响');
    // => Assertion failed: isOdd 不受影响
    
    • 1
    • 2
    • 3

    也就是说,isOdd 的闭包,是函数声明时对初始 isEven 的引用;后续的赋值只是修改了 isEven 的引用,并不影响 isOdd,除非 isEven 又被用于生成新的谓词函数(如 isOddNew),那就会受影响了:

    isEven = () => false;
    const isOddNew = complement(isEven);
    console.assert(isOddNew(2) === false, 'isOddNew 受影响');
    // => Assertion failed: isOddNew 受影响
    
    • 1
    • 2
    • 3
    • 4

    实际应用中,遇到的更常见的情况是,isEven 很可能来自一个通用工具模块:

    const myUtils = { isEven: n => (n % 2) === 0 };
    const complement2 = wrapper => 
    	(...args) => !wrapper.isEven.apply(null, args);
    const isOdd2 = complement2(myUtils);
    console.log('before:', isOdd2(2)); // => before: false
    myUtils.isEven = () => false;
    console.log('after:', isOdd2(2)); // => after: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可见,即便传入的对象引用不变,当新函数真正调用的是该引用下的某个属性时,修改这个属性的引用,也会影响到生成的函数。解决这个问题至少有两种思路:

    1. 尽量避免传入目标引用的包裹对象;
    2. 使用解构赋值直接锁定目标引用;

    第一条不用多说,第二条示例如下:

    // 使用解构赋值直接锁定目标引用
    const myUtils = { isEven: n => (n % 2) === 0 };
    const complement3 = ({isEven}) => 
    	(...args) => !isEven.apply(null, args);
    const isOdd3 = complement3(myUtils);
    console.log('before:', isOdd3(2)); // => before: false
    myUtils.isEven = () => false;
    console.log('after:', isOdd3(2)); // => after: false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    除了上述补救措施外,要从根本上杜绝闭包引用被篡改的情况,应该像书中说的那样,最大限度降低被捕获变量的暴露风险(如使用 Revealing Module Pattern 设计模式,或 class 语法糖):

    // myUtils.mjs
    export const isEven = n => (n % 2) === 0;
    // index.mjs
    import { isEven } from './myUtils.mjs';
    const {isOdd: isOdd4} = (() => {
        const complement = PRED => 
        	(...args) => !PRED.apply(null, args);
        const isOdd = complement(isEven); // captured variable
        return { isOdd };
    })();
    console.log(isOdd4(2));
    // => false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    综上所述,利用 JavaScript 闭包特性及高阶函数创建新函数时,需要注意以下三点:

    1. 闭包的引用尽量直奔主题,不要传入含有目标引用的任何包裹对象;
    2. 如果实在避免不了传入包裹对象,最好使用解构赋值锁定目标引用;
    3. 最大限度降低被捕获变量的暴露风险。
  • 相关阅读:
    【EI会议征稿】第四届智慧城市工程与公共交通国际学术会议(SCEPT 2024)
    Java中的异常体系模型
    解决Mapper接口错误: 使用MyBatis Plus时未正确继承BaseMapper接口或添加@Mapper注解导致无法使用相关方法的探索与编程实践
    中秋《乡村振兴战略下传统村落文化旅游设计》许少辉八月新书——2023学生思乡季辉少许
    计算机毕业设计(附源码)python智能旅游电子票务系统
    Linux 中断
    hdfs和yarn的常用命令
    C++容器string的运用和注意
    如何判断一款GameFi游戏是否有发展空间?
    SpringBoot拦截器和动态代理有什么区别?
  • 原文地址:https://blog.csdn.net/frgod/article/details/131087158