今天拜读大牛 Micheal Fogus 的神作『Functional JavaScript』,发现 JavaScript
闭包中一个极其隐蔽的坑,特此梳理,也希望能帮到更多后来人。
言归正传。在第三章讨论闭包的问题时,大牛提到一个对判定条件取反的案例:
function complement(PRED) {
return function() {
return !PRED.apply(null, _.toArray(arguments));
};
}
剔除 Underscore.js
的干扰,并简化为 ES6 的写法为:
const complement = PRED => (...args) => !PRED.apply(null, args);
如果有一个判定偶数的谓词函数 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
问题来了:如果将 isEven
篡改为其他函数,isOdd
会受影响吗?
isEven = () => false;
console.assert(isOdd(2) === true, 'isOdd 不受影响');
// => Assertion failed: isOdd 不受影响
也就是说,isOdd
的闭包,是函数声明时对初始 isEven
的引用;后续的赋值只是修改了 isEven
的引用,并不影响 isOdd
,除非 isEven
又被用于生成新的谓词函数(如 isOddNew
),那就会受影响了:
isEven = () => false;
const isOddNew = complement(isEven);
console.assert(isOddNew(2) === false, 'isOddNew 受影响');
// => Assertion failed: isOddNew 受影响
实际应用中,遇到的更常见的情况是,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
可见,即便传入的对象引用不变,当新函数真正调用的是该引用下的某个属性时,修改这个属性的引用,也会影响到生成的函数。解决这个问题至少有两种思路:
第一条不用多说,第二条示例如下:
// 使用解构赋值直接锁定目标引用
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
除了上述补救措施外,要从根本上杜绝闭包引用被篡改的情况,应该像书中说的那样,最大限度降低被捕获变量的暴露风险(如使用 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
综上所述,利用 JavaScript
闭包特性及高阶函数创建新函数时,需要注意以下三点: