• 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. 最大限度降低被捕获变量的暴露风险。
  • 相关阅读:
    巨额亏损,股价遭受重创,Polestar极星汽车已陷入困境
    xss-labs/level4
    C++设计模式之——组合模式
    v86.01 鸿蒙内核源码分析 (静态分配篇) | 很简单的一位小朋友 | 百篇博客分析 OpenHarmony 源码
    RabbitMQ之集群管理
    基于Vue(提供Vue2/Vue3版本)和.Net Core前后端分离、强大、跨平台的快速开发框架
    【LeetCode】202. 快乐数 - hash表 / 快慢指针
    【Linux基础】3.5 动态监控系统,rpm包,yum
    从内存角度聊聊虚函数
    2023年中职“网络安全“—Linux系统渗透提权②
  • 原文地址:https://blog.csdn.net/frgod/article/details/131087158