
JavaScript社区一直是围绕最佳范例,最时髦的框架和最酷的观点进行激烈讨论的温床。 老实说,我觉得有一个开放而热情的社区真是太好了。 您需要有各种经验水平的人员进入,他们可以立即参与并更重要地做出贡献。 但是这些讨论变成语义论证的次数比我想要的要多得多。
我想从使用函数原语的角度来解构OOP。 这不是严格的函数式编程。 我只想说明OOP的价值可以分段实现,而无需订阅特定于语言的构造,例如Classes,在JavaScript的情况下,它们实际上不足以满足主要需求。 我听说过这种称为面向工厂的编程。 但这实际上只是经典问题的另一种观点。 也许与此旅程可以更好地理解JavaScript。
那么什么是OOP?
去做一个谷歌搜索。 维基百科会告诉您:
面向对象程序设计 ( OOP )是一种基于“对象”概念的编程范例,它可以包含字段形式的数据(通常称为属性)和程序形式的代码(通常称为方法) )。
好的。 它使用对象进行编程。 听起来很简单。 这是OOP吗?
const pet = {
类型:“狗”,
名称:“杰克”,
makeSound(){
alert('Woof woof。');
}
}
很好……这是一个碰巧可以识别为狗的物体,但是如果我们想养多只狗,那会很不方便。 因此,使工厂功能。
函数createDog(name){
返回{
类型:“狗”,
名称,
makeSound(){
alert('Woof woof。');
}
}
}
const pet = createDog('Jake');
const pet2 = createDog('Jackie');
成功。 现在,我们使用工厂函数来创建对象。 我们的狗有名字,可以吠叫。
事实证明,OOP的意义远不止于此。 有一个原因描述OOP并不是一成不变的。 它是内聚系统中几种编程最佳实践的结合。 最著名的是封装 , 抽象 , 继承和多态性 。
封装/抽象
这些是OOP吸引力的很大一部分。 您可以打包和模块化部分代码。 他们可以在定义的边界内保持自己的状态。 您可以向使用者隐藏实现的详细信息,从而使代码拥有拥有该区域的能力,并根据需要更改其实现。 因此,让我们更新示例以使用getter和setter来控制对数据的访问。
函数createDog(name){
让饥饿=假;
返回{
取得type(){返回'dog'; },
获取名称(){返回名称; },
设置名称(n){名称= n; },
get isHungry(){返回饥饿; },
feed(){饿=假; },
makeSound(){
alert(饿了吗?'groan ....':'Woof woof。');
}
}
}
在这种情况下,我们的数据甚至不需要存在于我们的对象中,但是由于闭包可以被函数包装为包含状态。 在这一点上,我们可以抽象出复杂的行为并维持本地状态,而无需使用new关键字或this上下文。
知道这有什么好处吗? 消费者确实无法达到饥饿变量或名称变量。 没有饥饿,没有后门,什么也没有。 只是纯接口和封装的行为。
当然有不利的一面。 我每次都创建一个新的对象和方法,而ES6类或原型构造函数将重用并重新绑定相同的方法。 这意味着更高的内存使用率,但性能差异几乎无法区分。
继承/多态
JavaScript不是严格类型化的语言,因此多态性确实是一件好事,而不会尝试变好或变坏。 底层运行时编译器肯定与类型有关,但是确实让开发人员感到相当开放。 JavaScript有多种执行运行时检查的方式,例如typeof和instanceOf,但是如果您真的担心键入,那么如果您真的担心使用TypeScript或Flow,那么它就有很长的奇怪异常和怪癖的历史。 因此,让我们保留对多态性的认可,尽管它可以工作,但不能仅由JavaScript强制实施。 相反,让我们看一下继承。
JavaScript确实有使用原型进行继承的方法。 但是,组合提供了一种可行的替代方法,在许多情况下,组合被认为是重用行为的更好方法。 它确实具有一个缺点,即即使只是转发类型,派生类型也需要实现方法。
函数createDomesticDog(name,livingIndoors){
const dog = createDog(name);
返回{
获取type(){dog.type; },
取得name(){return dog.name; },
设置名称(n){dog.name = n; },
得到lifesIndoors(){返回lifesIndoors; },
设置liveIndoors(v){ },
get isHungry(){返回dog.hungry; },
feed(){dog.hungry = false; },
makeSound(){
alert(dog.hungry?'groan ....':'Woof woof。');
}
}
}
因此,更常见的是,它用于建立许多较小的行为,而不是用于子类派生的简单基础。 实现此目的的一种方法是函数组合:
函数canBark(Type){
返回选项=> {
options = {...选项,树皮:()=> alert('Woof woof')};
返回类型(选项);
}
}
函数canEat(Type){
返回选项=> {
让饥饿=假;
选项= {
...选项
isHungry(){返回饥饿; },
feed(){饿=假; },
};
返回类型(选项);
}
}
函数hasName(name){
return(Type)=>
选项=> {
选项= {
...选项
name(){返回名称; },
setName(n){name = n; },
};
返回类型(选项);
}
}
//基本类型
函数createAnimal(options){
const {type,... otherOptions} =选项;
返回{
...其他选项,
取得type(){返回动物; }
};
}
//派生类型
函数createDog(name){
const DogType = canBark(canEat(hasName(name)(createAnimal)))));
返回DogType({type:'dog'});
}
这仅是一种组合方法,并且书面方法不能很好地处理名称冲突,但这只是说明性的。 它看起来有点复杂,但是可以通过辅助方法轻松处理以减少功能。
功能管道(... fns){
返回o => fn.reduce((o,fn)=> fn(o),o);
}
函数createDog(name){
const DogType =管道(
createAnimal,
hasName(name),
可以吃,
canBark
);
返回DogType({type:'dog'});
}
这样做的好处是可以支持多重继承并避免钻石问题。
包起来
我绝不建议不要在JavaScript中使用类。 有一些方法可以使用纯功能达到相同的目的,并且它们有一些好处和折衷。 我认为识别模式而不是语法很重要。 一些像React这样的库在其API中采用了这些技术,因此至少要意识到是一个好主意。
功能编程技术(例如工厂)和组合是包含在工具栏中的有用工具,可用于选择满足您需求的某些OOP核心价值。 选择合适的功能使这种方法变得有趣。 您可以封装而无需合成。 您可以编写而不封装。 您可以编写一些简单的包装状态,而不必this担心或使用类。 希望本文为您提供了关于这种疯狂的动态语言的一些新见解。