如何正确实现对象的「can do」能力判定?
这个问题戳中了对象组合模式里的一个常见痛点——单纯靠obj.method != null检查确实太脆弱了,万一有人不小心给对象加了个同名方法,很容易导致逻辑出错。下面分享几个在函数式编程场景下更靠谱的解决方案:
1. 用Symbol作为方法的唯一标识
Symbol是ES6引入的原生类型,每个Symbol值都是唯一的,完全不用担心同名冲突。你可以用它来标记具备特定能力的方法:
// 定义一个唯一的Symbol,作为能力的标识 const canDoSomething = Symbol('canDoSomething'); // 符合要求的对象,会挂载这个Symbol对应的方法 const compliantObject = { [canDoSomething]() { console.log("执行特定能力逻辑"); } }; // 判定函数:检查对象是否拥有这个Symbol对应的方法 function hasCapability(obj) { return typeof obj[canDoSomething] === 'function'; } // 测试 console.log(hasCapability(compliantObject)); // true console.log(hasCapability({ canDoSomething: () => {} })); // false,因为这里的key是字符串,不是我们定义的Symbol
这种方式的优势在于,除非别人特意拿到你定义的那个Symbol,否则根本无法伪造这个能力标识,安全性和可靠性拉满。
2. 给对象添加明确的能力契约标记
如果你的环境还不支持Symbol,可以给符合要求的对象添加一个独一无二的标记属性,比如一个特定的字符串key或者自定义的属性,结合方法检查来判定:
// 定义一个唯一的能力标记 const CAPABILITY_SOMETHING = '__capability_do_something__'; // 创建符合要求的对象 const compliantObject = { doSomething() { console.log("执行逻辑"); }, [CAPABILITY_SOMETHING]: true }; // 判定函数:同时检查标记和方法存在性 function canDoSomething(obj) { return obj[CAPABILITY_SOMETHING] === true && typeof obj.doSomething === 'function'; } // 测试 console.log(canDoSomething(compliantObject)); // true console.log(canDoSomething({ doSomething: () => {} })); // false,缺少标记
这里的标记key最好选一个不太会被意外使用的名字(比如加下划线前缀),或者用一个命名空间风格的字符串,进一步降低冲突概率。
3. 增强版鸭子类型:检查方法的特征
如果必须依赖方法名,你可以给标准方法添加一些独特的特征,比如自定义属性,然后在判定时检查这些特征:
// 定义标准方法,并给它加一个标记属性 const standardDoSomething = () => { // 方法逻辑 }; standardDoSomething.isCapabilityMethod = true; // 符合要求的对象使用这个标准方法 const compliantObject = { doSomething: standardDoSomething }; // 判定函数:检查方法存在性+特征标记 function canDoSomething(obj) { return typeof obj.doSomething === 'function' && obj.doSomething.isCapabilityMethod === true; } // 测试 console.log(canDoSomething(compliantObject)); // true console.log(canDoSomething({ doSomething: () => {} })); // false,自定义方法没有标记
这种方式适合无法使用Symbol的场景,但要注意如果有人刻意给伪造的方法也加上同样的标记,还是会被误判,所以安全性不如前两种方案。
4. 闭包封装能力(函数式风格推荐)
在函数式编程里,我们可以通过闭包来创建具备特定能力的对象,对外暴露方法的同时,内置唯一的能力标识:
// 工厂函数:创建具备特定能力的对象 function createSomethingDoer() { // 内部定义方法 const doSomething = () => { console.log("执行函数式风格的能力逻辑"); }; // 返回带标识的对象,甚至可以用Object.freeze防止篡改 return Object.freeze({ doSomething, // 用Symbol.for创建一个全局可访问的Symbol(如果需要跨模块判定的话) [Symbol.for('canDoSomething')]: true }); } // 创建对象 const myDoer = createSomethingDoer(); // 判定函数 function canDoSomething(obj) { return obj[Symbol.for('canDoSomething')] === true && typeof obj.doSomething === 'function'; } // 测试 console.log(canDoSomething(myDoer)); // true console.log(canDoSomething({ doSomething: () => {} })); // false
用Object.freeze还能防止对象被外部篡改,进一步保证能力标识的可靠性,非常贴合函数式编程的 immutable 理念。
总的来说,Symbol方案是最优解,它从根源上避免了同名方法的冲突问题;如果环境受限,就选择明确的能力标记或者闭包封装的方式。核心思路就是:不要只依赖方法名这种容易被伪造的特征,而是给能力加上唯一、不可轻易复制的标识,这样就能准确实现「can do」的判定了。
内容的提问来源于stack exchange,提问作者Fuzzyma

