各位靓仔靓女,大家好!今天咱们不聊风花雪月,也不谈人生理想,就来唠唠嗑,关于JavaScript里一个既重要又有点让人头大的话题:组合优于继承(Composition over Inheritance)。
别看这名字听起来高大上,其实它说白了就是告诉你,别老想着靠“血缘关系”(继承)来解决问题,多想想怎么把不同的“零件”(函数或对象)拼装起来,搭积木一样。
一、继承的诱惑与陷阱
话说回来,继承这玩意儿,一开始确实挺吸引人的。 想象一下,你有一个Animal
(动物)类,它有eat()
(吃)和sleep()
(睡)方法。然后你想创建一个Dog
(狗)类,狗也是动物啊,那直接继承Animal
,省事!
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
sleep() {
console.log(`${this.name} is sleeping.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof!');
}
}
const dog = new Dog('Buddy');
dog.eat(); // Buddy is eating.
dog.bark(); // Woof!
看起来很完美,对不对? 代码简洁,逻辑清晰。 但问题来了,如果有一天,你想创建一个Bird
(鸟)类,它也需要eat()
和sleep()
方法,你又想让鸟会飞fly()
。
你可能会想: “那简单,再继承一次Animal
就好了!”
class Bird extends Animal {
fly() {
console.log(`${this.name} is flying.`);
}
}
const bird = new Bird('Tweety');
bird.eat(); // Tweety is eating.
bird.fly(); // Tweety is flying.
到目前为止,一切还算顺利。 但是,如果有一天,你突然想创建一个FlyingDog
(会飞的狗)类呢? 狗会叫,也会飞,这可咋办?
- 多重继承的痛苦: 某些语言支持多重继承,你可以让
FlyingDog
同时继承Dog
和Flying
类。 但JavaScript不支持多重继承(虽然可以通过一些方式模拟,但会增加复杂度)。 - 继承层次的混乱: 如果你强行使用继承,可能会导致继承链越来越深,代码越来越复杂,难以维护。 想象一下,
FlyingDog
继承Dog
,Dog
继承Animal
,然后Animal
又继承LivingThing
… 这棵继承树会变得异常臃肿。 - 代码复用的局限性: 继承是一种“强耦合”的关系。子类必须完全依赖于父类的实现。如果父类的实现发生改变,可能会影响到所有的子类。
二、组合的魅力
组合的核心思想是: 将对象的功能分解成更小的、独立的单元,然后通过组合这些单元来构建更复杂的功能。 就像搭积木一样,你可以选择不同的积木块,按照不同的方式组装,从而创造出不同的形状。
回到刚才的例子,我们可以把fly()
方法提取出来,作为一个独立的函数(或者对象),然后让Bird
和FlyingDog
都“拥有”这个功能。
// 定义一个飞行行为
const canFly = (state) => ({
fly: () => console.log(`${state.name} is flying.`)
});
const canEat = (state) => ({
eat: () => console.log(`${state.name} is eating.`)
});
const canSleep = (state) => ({
sleep: () => console.log(`${state.name} is sleeping.`)
});
const canBark = (state) => ({
bark: () => console.log('Woof!')
});
// 创建Bird类
const createBird = (name) => {
let state = {
name: name
};
return Object.assign({},
canEat(state),
canSleep(state),
canFly(state)
);
};
// 创建Dog类
const createDog = (name) => {
let state = {
name: name
};
return Object.assign({},
canEat(state),
canSleep(state),
canBark(state)
);
};
// 创建FlyingDog类
const createFlyingDog = (name) => {
let state = {
name: name
};
return Object.assign({},
canEat(state),
canSleep(state),
canBark(state),
canFly(state)
);
};
const bird = createBird('Tweety');
bird.eat(); // Tweety is eating.
bird.fly(); // Tweety is flying.
const dog = createDog('Buddy');
dog.eat(); // Buddy is eating.
dog.bark(); // Woof!
const flyingDog = createFlyingDog('AirBuddy');
flyingDog.eat(); // AirBuddy is eating.
flyingDog.bark(); // Woof!
flyingDog.fly(); // AirBuddy is flying.
在这个例子中,我们使用Object.assign()
方法将不同的功能“混合”到对象中。 canFly
、canEat
、canSleep
和 canBark
这些都是可复用的行为模块。 FlyingDog
可以同时拥有 canBark
和 canFly
的能力,而不需要复杂的继承关系。
组合的优势:
- 灵活性: 可以根据需要选择不同的功能模块进行组合,创建出各种不同的对象。
- 可复用性: 功能模块可以被多个对象复用,避免代码重复。
- 解耦性: 对象之间的耦合度较低,修改一个功能模块不会影响到其他对象。
- 可测试性: 每个功能模块都是独立的,可以单独进行测试。
三、组合的几种常见姿势
组合的方式有很多种,以下是一些常见的姿势:
-
函数组合(Function Composition)
函数组合是指将多个函数组合成一个函数,然后依次执行这些函数。 就像流水线一样,每个函数负责处理一个环节。
const add = (x) => x + 2; const multiply = (x) => x * 3; // 组合函数 const compose = (f, g) => (x) => f(g(x)); // 先乘以3,再加2 const addThenMultiply = compose(multiply, add); console.log(addThenMultiply(5)); // (5 + 2) * 3 = 21
在这个例子中,
compose
函数接受两个函数f
和g
作为参数,返回一个新的函数,该函数先执行g
函数,然后将结果传递给f
函数。你可以使用 Lodash 或 Ramda 等库提供的
compose
或pipe
函数来进行更简洁的函数组合。 例如:import { compose } from 'lodash'; const toUpperCase = (str) => str.toUpperCase(); const exclaim = (str) => str + '!'; const excited = compose(exclaim, toUpperCase); console.log(excited('hello')); // HELLO!
-
混入(Mixins)
混入是指将一个对象的功能“混入”到另一个对象中。 这可以通过
Object.assign()
或...
扩展运算符来实现。 上面创建Bird
、Dog
、FlyingDog
的例子就使用了混入。const barker = { bark: function() { console.log('Woof!'); } }; const driver = { drive: function() { console.log('Vroom!'); } }; const robotDog = Object.assign({}, barker, driver); robotDog.bark(); // Woof! robotDog.drive(); // Vroom!
-
策略模式(Strategy Pattern)
策略模式是指定义一系列的算法,并将每个算法封装成一个独立的类,然后让客户端可以选择使用哪个算法。 这可以提高代码的灵活性和可扩展性。
// 定义策略接口 class PaymentStrategy { pay(amount) { throw new Error('pay() method must be implemented.'); } } // 实现信用卡支付策略 class CreditCardPayment extends PaymentStrategy { constructor(cardNumber, expiryDate, cvv) { super(); this.cardNumber = cardNumber; this.expiryDate = expiryDate; this.cvv = cvv; } pay(amount) { console.log(`Paying ${amount} using Credit Card: ${this.cardNumber}`); } } // 实现 PayPal 支付策略 class PayPalPayment extends PaymentStrategy { constructor(email) { super(); this.email = email; } pay(amount) { console.log(`Paying ${amount} using PayPal: ${this.email}`); } } // 上下文对象 class ShoppingCart { constructor(paymentStrategy) { this.paymentStrategy = paymentStrategy; } setPaymentStrategy(paymentStrategy) { this.paymentStrategy = paymentStrategy; } checkout(amount) { this.paymentStrategy.pay(amount); } } // 使用示例 const cart = new ShoppingCart(new CreditCardPayment('1234-5678-9012-3456', '12/24', '123')); cart.checkout(100); // Paying 100 using Credit Card: 1234-5678-9012-3456 cart.setPaymentStrategy(new PayPalPayment('[email protected]')); cart.checkout(50); // Paying 50 using PayPal: [email protected]
在这个例子中,
PaymentStrategy
是一个抽象类,定义了支付接口。CreditCardPayment
和PayPalPayment
是具体的策略类,分别实现了信用卡支付和PayPal支付。ShoppingCart
是一个上下文对象,它持有一个PaymentStrategy
对象,并根据客户端的选择来执行不同的支付策略。 -
高阶组件(Higher-Order Components – React)
在高阶组件(HOC)中,一个组件接受另一个组件作为参数,并返回一个新的组件。 这是一种常见的在 React 中复用组件逻辑的方式。
import React from 'react'; // 高阶组件:增强组件的功能 const withLoading = (WrappedComponent) => { return class WithLoading extends React.Component { constructor(props) { super(props); this.state = { isLoading: true }; } componentDidMount() { // 模拟数据加载 setTimeout(() => { this.setState({ isLoading: false }); }, 1500); } render() { if (this.state.isLoading) { return <div>Loading...</div>; } return <WrappedComponent {...this.props} />; } }; }; // 原始组件 const MyComponent = (props) => { return <div>Hello, {props.name}!</div>; }; // 使用高阶组件增强后的组件 const EnhancedComponent = withLoading(MyComponent); // 渲染 ReactDOM.render(<EnhancedComponent name="World" />, document.getElementById('root'));
在这个例子中,
withLoading
是一个高阶组件,它接受一个组件WrappedComponent
作为参数,并返回一个新的组件WithLoading
。WithLoading
组件在渲染WrappedComponent
之前,会显示一个加载指示器,直到数据加载完成。 -
Hooks (React)
React Hooks 允许你在不编写 class 的情况下使用 state 以及其他的 React 特性。 自定义 Hooks 是一种组合逻辑的有效方式。
import { useState, useEffect } from 'react'; // 自定义 Hook:获取窗口大小 function useWindowSize() { const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); } window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Empty array ensures that effect is only run on mount return windowSize; } function MyComponent() { const windowSize = useWindowSize(); return ( <div> Window size: {windowSize.width} x {windowSize.height} </div> ); }
在这个例子中,
useWindowSize
是一个自定义 Hook,它返回当前窗口的大小。MyComponent
使用useWindowSize
Hook 来获取窗口大小,并在屏幕上显示。
四、 总结:选择合适的姿势
组合优于继承并不是说继承完全没有用武之地。 在某些情况下,继承仍然是一种有效的解决方案。 关键在于选择合适的工具来解决问题。
特性 | 继承 | 组合 |
---|---|---|
代码复用 | 通过继承父类的属性和方法 | 通过组合独立的函数和对象 |
灵活性 | 较低,子类必须依赖于父类的实现 | 较高,可以根据需要选择不同的功能模块进行组合 |
耦合度 | 较高,子类和父类之间存在强耦合关系 | 较低,对象之间的耦合度较低 |
可维护性 | 随着继承层次的加深,代码会变得越来越复杂,难以维护 | 功能模块独立,易于维护和测试 |
适用场景 | 当存在明显的“is-a”关系,且继承层次结构稳定时 | 当需要灵活地组合不同的功能,且避免继承带来的复杂性时 |
典型应用 | 创建用户界面组件库,其中某些组件具有共同的基类 | 创建游戏引擎,其中不同的游戏对象可以组合不同的行为 |
什么时候选择组合?
- 当你需要灵活地组合不同的功能时。
- 当你希望避免继承带来的复杂性时。
- 当你需要提高代码的可复用性和可维护性时。
什么时候选择继承?
- 当存在明显的“is-a”关系时。
- 当继承层次结构稳定时。
- 当你需要利用继承来实现多态性时。
总而言之,组合优于继承是一种重要的设计思想,它可以帮助你编写更加灵活、可复用、可维护的代码。 在实际开发中,你应该根据具体情况选择合适的组合方式。
希望今天的唠嗑对大家有所帮助! 咱们下次再见!