• 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. 最大限度降低被捕获变量的暴露风险。
  • 相关阅读:
    【重识云原生】第六章容器6.3.4节——etcd组件
    Linux下查找当前目录下所有文件中的关键字AAA并将其替换成BBB
    webpack性能优化配置与实战(一)
    jvm调优思路
    项目管理中,如何建立里程碑式管理?
    Authentication for Hadoop(3.3.1) HTTP web-consoles 不是银弹
    剑指offer——JZ68 二叉搜索树的最近公共祖先 解题思路与具体代码【C++】
    springboot项目的打包发布部署,jar和war的区别
    成都理工大学_Python程序设计_第7章
    WPF“x:name”和“name”有什么区别
  • 原文地址:https://blog.csdn.net/frgod/article/details/131087158