对象经常在JavaScript中使用,而了解如何有效地使用它们将为您的工作效率带来巨大的胜利。实际上,糟糕的OO设计可能会导致项目失败,在最坏的情况下还会导致公司失败。
与大多数其他语言不同,JavaScript的对象系统基于原型,而不是类。不幸的是,大多数JavaScript开发人员都不了解JavaScript的对象系统或如何最好地利用它。其他人的确理解它,但是希望它的行为更像基于类的系统。结果是JavaScript的对象系统具有令人困惑的拆分个性,这意味着JavaScript开发人员需要对原型和类有所了解。
类和原型继承之间有什么区别?
类的继承: 类就像对要创建的对象的描述。类从类继承并创建子类关系。
实例通常通过带有new
关键字的构造函数来实例化。类继承可以使用也可以不使用ES6中的class
关键字。您可能从Java之类的语言中了解到的类在技术上并不存在于JavaScript中。而是使用构造函数。该ES6 class
关键字转换成构造函数:
class Foo {}
typeof Foo // `function`
在JavaScript中,类继承是在原型继承的基础上实现的,但这并不意味着它执行相同的操作:
JavaScript的类继承使用原型链将子项“ Constructor.prototype”连接到父项“ Constructor.prototype”进行代理。通常,也调用super()
构造函数。这些步骤形成了单祖先的父/子层次结构,并创建了OO设计中可用的最紧密的耦合。
“类从类继承并创建子类关系”
原型继承: 原型是工作对象实例。 对象直接从其他对象继承。
实例可以由许多不同的源对象组成,从而可以轻松地进行选择性继承并实现平坦的[[Prototype]]代理层次结构。换句话说,分类法并不是原型OO的自动副作用:一个关键的区别。
实例通常通过工厂函数,对象,或 Object.create()实例化。
“原型是一个工作对象实例。对象直接从其他对象继承。”
为什么如此重要?
继承从根本上讲是一种代码重用机制:一种不同类型的对象共享代码的方式。共享代码的方式很重要,因为如果您弄错了代码,则会造成很多问题,特别是:
类继承将父/子对象分类法作为副作用。
这些分类法实际上不可能适用于所有新的用例,并且基类的广泛使用会导致脆弱的基类问题,这会使它们在错误时难以修复。 实际上,类继承会在OO设计中引起许多众所周知的问题:
- 紧密耦合问题(类继承是oo设计中可用的最紧密耦合),这导致了下一个问题。
- 脆弱的基层问题
- 僵化的层次结构问题(最终,所有正在发展的层次结构对于新用途都是错误的)
- 必然性复制问题(由于层级不灵活,新用例常常因复制而不是改编现有代码而陷入困境)
所有继承都是不好的吗?
这是OO设计中的常识,因为类继承存在许多缺陷并引起许多问题。人们通常在谈论类继承时会忽略类一词,这听起来似乎所有继承都是不好的,但事实并非如此。
实际上,有几种不同的继承,其中大多数对于从多个组件对象组成复合对象非常有用。
三种不同的原型继承
在深入探讨其他类型的继承之前,让我们仔细研究一下类继承的含义:
// Class Inheritance Example
// NOT RECOMMENDED. Use object composition, instead.
// https://gist.github.com/ericelliott/b668ce0ad1ab540df915
// http://codepen.io/ericelliott/pen/pgdPOb?editors=001
class GuitarAmp {
constructor ({ cabinet = `spruce`, distortion = `1`, volume = `0` } = {}) {
Object.assign(this, {
cabinet, distortion, volume
});
}
}
class BassAmp extends GuitarAmp {
constructor (options = {}) {
super(options);
this.lowCut = options.lowCut;
}
}
class ChannelStrip extends BassAmp {
constructor (options = {}) {
super(options);
this.inputLevel = options.inputLevel;
}
}
test(`Class Inheritance`, nest => {
nest.test(`BassAmp`, assert => {
const msg = `instance should inherit props
from GuitarAmp and BassAmp`;
const myAmp = new BassAmp();
const actual = Object.keys(myAmp);
const expected = [`cabinet`, `distortion`, `volume`, `lowCut`];
assert.deepEqual(actual, expected, msg);
assert.end();
});
nest.test(`ChannelStrip`, assert => {
const msg = `instance should inherit from GuitarAmp, BassAmp, and ChannelStrip`;
const myStrip = new ChannelStrip();
const actual = Object.keys(myStrip);
const expected = [`cabinet`, `distortion`, `volume`, `lowCut`, `inputLevel`];
assert.deepEqual(actual, expected, msg);
assert.end();
});
});
“ BassAmp”继承自“ GuitarAmp”,而“ ChannelStrip”继承自“ BassAmp”和“ GuitarAmp”。这是OO设计出错的一个例子。通道条实际上不是吉他放大器的一种,并且实际上根本不需要cabinet
。更好的选择是创建一个新的基类,amps
和channel
都可以继承该基类,但即使这样也有局限性。
最终,新的共享基类策略也崩溃了。
有更好的方法。您可以使用对象组合继承您真正需要的东西:
// Composition Example
// http://codepen.io/ericelliott/pen/XXzadQ?editors=001
// https://gist.github.com/ericelliott/fed0fd7a0d3388b06402
const distortion = { distortion: 1 };
const volume = { volume: 1 };
const cabinet = { cabinet: `maple` };
const lowCut = { lowCut: 1 };
const inputLevel = { inputLevel: 1 };
const GuitarAmp = (options) => {
return Object.assign({}, distortion, volume, cabinet, options);
};
const BassAmp = (options) => {
return Object.assign({}, lowCut, volume, cabinet, options);
};
const ChannelStrip = (options) => {
return Object.assign({}, inputLevel, lowCut, volume, options);
};
test(`GuitarAmp`, assert => {
const msg = `should have distortion, volume, and cabinet`;
const level = 2;
const cabinet = `vintage`;
const actual = GuitarAmp({
distortion: level,
volume: level,
cabinet
});
const expected = {
distortion: level,
volume: level,
cabinet
};
assert.deepEqual(actual, expected, msg);
assert.end();
});
test(`BassAmp`, assert => {
const msg = `should have volume, lowCut, and cabinet`;
const level = 2;
const cabinet = `vintage`;
const actual = BassAmp({
lowCut: level,
volume: level,
cabinet
});
const expected = {
lowCut: level,
volume: level,
cabinet
};
assert.deepEqual(actual, expected, msg);
assert.end();
});
test(`ChannelStrip`, assert => {
const msg = `should have inputLevel, lowCut, and volume`;
const level = 2;
const actual = ChannelStrip({
inputLevel: level,
lowCut: level,
volume: level
});
const expected = {
inputLevel: level,
lowCut: level,
volume: level
};
assert.deepEqual(actual, expected, msg);
assert.end();
});
如果仔细看,您可能会发现我们在确定哪些对象具有哪些属性方面更加具体,因为有了组合,我们就可以做到。它不是类继承的真正选择。从类继承时,即使您不想要任何东西,也可以得到所有东西。
在这一点上,您可能会想自己:“很好,但是原型在哪里?”
要了解这一点,您必须了解有三种不同的原型OO。
-
串联继承:通过复制源对象属性将特征直接从一个对象继承到另一个对象的过程。在JavaScript中,源原型通常称为mixin。从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
。但是通过继承和调用super
, 您就不必对继承的内容有所选择。您继承了所有内容:
面向对象语言的问题在于,它们具有随身携带的所有隐式环境。您想要香蕉,但是得到的是一只大猩猩,里面盛着香蕉和整个丛林。
使用组合Composition
想象一下您可以拥有功能而不是class:
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
不受影响。
类继承不可能实现的原因是,当您使用类继承时,您会购买整个现有的类分类法。
如果您想为新的用例做些改动,要么最终要复制现有分类法的某些部分(必要性重复问题),要么重构所有依赖于现有分类法的内容,以使分类法适应新的用途。由于脆弱的基层问题。
Composition 对两者都没有影响。
您认为您知道原型,但是…
如果您被教导构建类或构造函数并从中继承,那么您所教的不是原型继承。教会您如何使用原型模仿类继承。
在JavaScript中,类继承是基于很久以前内置在该语言中的非常丰富,灵活的原型继承功能之上的,但是当您使用类继承时-即使是在原型之上构建的ES6 + class
继承,您也不是使用原型OO的全部功能和灵活性。
在JavaScript中使用类继承就像将新的Tesla Model S推向经销商并将其换成生锈的2011年的比亚迪F0一样。
参考
Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance?
网友评论