前言
与大多数其他语言不同,JavaScript 的对象系统基于原型,而不是类。
类继承
定义: 类就像一个蓝图——对要创建的对象的描述。类继承自类并创建子类关系:分层类分类法。
在 JavaScript 中,类继承是在原型继承之上实现的。JavaScript 的类继承使用原型链将子 “Constructor.prototype” 连接到父 “Constructor.prototype” 以进行委派。通常,也会调用super()构造函数。这些步骤形成单祖先父/子层次结构,并创建 OO 设计中可用的最紧密耦合。
原型继承
定义: 原型是一个工作对象实例。对象直接从其他对象继承。
实例可以由许多不同的源对象组成,从而允许轻松的选择性继承和扁平的 [[Prototype]] 委派层次结构。换句话说,类分类法不是原型OO的自动副作用:一个关键的区别。
实例通常通过工厂函数、对象文字或 “Object.create()” 进行实例化。
为什么这很重要?
继承本质上是一种代码重用机制:一种让不同类型的对象共享代码的方式。共享代码的方式很重要,因为如果您弄错了代码,可能会产生很多问题, 特别是:类继承创建父/子对象分类作为副作用。
这些分类法几乎不可能适用于所有新用例,并且基类的广泛使用会导致脆弱的基类问题, 这使得当您出错时很难修复它们。 事实上,类继承在OO设计中会导致许多众所周知的问题:
- 紧密耦合问题(类继承是 oo 设计中可用的最紧密耦合),这导致了下一个......
- 脆弱的基类问题
- 不灵活的层次结构问题(最终,所有不断发展的层次结构对于新用途都是错误的)
- 必要复制问题(由于层次结构不灵活,新用例通常通过复制而不是调整现有代码来硬塞进去)
- 大猩猩/香蕉问题(你想要的是香蕉,但你得到的是一只拿着香蕉的大猩猩,还有整个丛林)
所有的继承都是不好的吗?
这是面向对象设计中的常识,因为类继承有许多缺陷并导致许多问题。人们在谈论类继承时经常省略类这个词,这听起来好像所有的继承都是不好的——但事实并非如此。
实际上有几种不同类型的继承,其中大多数都非常适合从多个组件对象组合复合对象。
三种不同类型的原型 OO
串联继承: 通过复制源对象属性将特征从一个对象直接继承到另一个对象的过程。在 JavaScript 中,源原型通常被称为 mixins。 从 ES6 开始,这个特性在 JavaScript 中有一个方便的实用程序,叫做 'Object.assign()' 。在 ES6 之前,这通常使用 Underscore/Lodash 的 '.extend()' jQuery 的 *'$.extend()' 来完成。
原型委派: 在 JavaScript 中,对象可能具有指向用于委派的原型的链接。如果在对象上找不到属性,则查找将委托给委托原型,该原型可能具有指向其自己的委托原型的链接,依此类推 , 直到您到达根委托“Object.prototype”。 这是当您附加到 “Constructor.prototype”并使用 “new” 实例化时挂接的原型。您还可以使用 “Object.create()” 来实现此目的,甚至可以将此技术与串联混合,以便将多个原型平展为单个委托,或在创建后扩展对象实例。
功能继承: 在 JavaScript 中,任何函数都可以创建一个对象。当该函数不是构造函数(或 “类”) 时,它被称为工厂函数。函数继承的工作原理是从工厂生成对象,并通过直接为其分配属性(使用串联继承)来扩展生成的对象。Douglas Crockford 创造了这个术语,但函数继承在 JavaScript 中已经普遍使用了很长时间。
正如您可能开始意识到的那样,连接继承是在 JavaScript 中启用对象组合的秘密武器,这使得原型委派和函数继承都更加有趣。
当大多数人想到JavaScript中的原型OO时,他们会想到原型委托。 到现在为止,您应该看到他们错过了很多。委托原型不是类继承的绝佳替代方案 - 对象组合才是。
为什么组合不受脆弱基类问题的影响
要理解脆弱的基类问题以及为什么它不适用于组合,首先你必须了解它是如何发生的:
- “A” 是基类
- “B”继承自 “A”
- “C”继承自 “B”
- “D”继承自 “B”
“C”调用“super” ,它在 “B” 中运行代码。“B”调用“super”,它在 “A” 中运行代码。**
“A”和“B”包含“C”和 “D” 所需的不相关特征。** **“D”是一个新的用例,在“A”的初始化代码中需要的行为与 “C” 需要的行为略有不同。因此,新手开发人员去调整 “A” 的初始化代码。“C”中断,因为它取决于现有行为, “D” 开始工作。
我们在这里拥有的是分布在“A”和“B”之间的特征,“C”和 “D ” 需要以各种方式使用。“C”和“D”没有使用“A”和 “B” 的所有特征...... **他们只是想继承一些已经在“A”和 “B” 中定义的东西。但是通过继承和称呼“超级”, 你不会对你继承的东西有选择性。
使用组合
想象一下你有特征而不是类:feat1, feat2, feat3, feat4
“C”需要“feat1”和“feat3”,“D”需要 “ feat1”、“feat2”、“feat4” :
const C = compose(feat1, feat3);
const D = compose(feat1, feat2, feat4);
现在,假设您发现“D”需要与 “feat1” 略有不同的行为。它实际上不需要更改“feat1”,相反,您可以制作 “feat1” 的自定义版本并使用它。您仍然可以继承“feat2”和“feat4” 的现有行为,而无需更改:
const D = compose(custom1, feat2, feat4);
C不受影响。
使用类继承无法做到这一点的原因是,当您使用类继承时,您会获得整个现有的类分类。
如果你想稍微适应一个新的用例,你要么最终复制现有分类法的一部分(必要性重复问题),要么你重构依赖于现有分类法的所有内容,以使分类法适应新的用例,因为脆弱的基类问题。
组合物对两者都免疫。
你认为你知道原型,但是...
如果你被教导构建类或构造函数并从中继承,那么你学到的不是原型继承。你被教导如何使用原型模仿类继承。请参阅“关于 JavaScript 中继承的常见误解”。
在 JavaScript 中,类继承背负在很久以前语言中内置的非常丰富、灵活的原型继承功能之上,但是当你使用类继承时——即使是建立在原型之上的 ES6+ “类” 继承,你也没有使用原型 OO 的全部功能和灵活性。事实上,你把自己画在角落里,选择解决所有的类继承问题。