带JavaScript函数的OOP

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担心或使用类。 希望本文为您提供了关于这种疯狂的动态语言的一些新见解。