类和原型的设计模式之复制与委托差异
作者:掘金安东尼
小引
JavaScript 技能持有者一定有问过这个问题:
JavaScript 是面向对象语言吗?
你期望得到的答案应该为:“是” 或 “不是”。
但是可惜,你得不到这样简单的答案!
你大概了解一通之后,你会被告知:
JavaScript 不是纯粹的面向对象语言!
wtf!为什么是不纯粹?能不能纯粹一点?!我们喜欢纯粹,不喜欢混沌!
......
实际上,死扣定义真的没太必要。定义背后的故事才是最重要的!
看完本篇,你就会明白这种“混沌”是什么、来自何处,以及去往何方!!
撰文不易,多多鼓励。点赞再看,养成习惯。👍👍👍
“类”设计模式
妇孺皆知,面向对象三大特性:【封装】、【继承】、【多态】。
- 所谓封装,即把客观事物封装成抽象的类。
- 所谓继承,即子类继承父类的能力。
- 所谓多态,即子类可以用更特殊的行为重写所继承父类的通用行为。
其中,“类”的概念最最关键!【类】描述了一种代码的组织结构形式,它是软件中对真实世界中问题领域的建模方法。
举个例子:
就好比我们现实中修房子,你要修一个“写字楼”、或者一个“居民楼”、或者一个“商场”,你就得分别找到修“写字楼”、“居民楼”、“商场”的【设计蓝图】。
但是设计蓝图只是一个建筑计划,并不是真正的建筑。要想在真实世界实现这个建筑,就得由建筑工人将设计蓝图的各类特性(比如长宽高、功能)【复制】到现实世界来。
这里的【设计蓝图】就是【类】,【复制】的过程就是【实例化】,【实例】就是【对象】。
类的内部通常有一个同名的构造方法,我们设想下,它的伪代码就可能是这样的:
class Mall { // “商场”类 Mall( num ){ // 同名构造方法 garage = num // 地下车库数量 } shop( goods ) { // 买东西 output( "We can buy: ", goods ) } } // 构造函数大多需要用 new 来调,这样语言引擎才知道你想要构造一个新的类实例。 vanke = new Mall(1) // vanke 有 1 个地下车库 vanke.shop("KFC") // "We can buy: KFC"
java 是典型的面向对象语言。基于“类”,我们再通过以下一段 java 代码来看看对继承和多态的理解。
public abstract class Animal{ // 抽象类 abstract void sound(); } public class Chicken extends Animal{ // 继承 public void sound(){ sound("咯咯咯"); } } public class Duck extends Animal{ public void sound(){ sound("嘎嘎嘎"); } } public static void main(String args[]){ Aninal chicken = new Chicken(); Animal duck = new Duck(); chicken.sound(); //咯咯咯 duck.sound(); //嘎嘎嘎 }
鸡和鸭都属于动物分类,都可以发出叫声(继承),但是它们却可以发出不同的叫声(多态),很容易理解。
继承可以使子类获得父类的全部功能; 多态可以使程序有良好的扩展;
回想下:在 JS 中,我们可能会怎样写:
var Duck = function () {}; var Chicken = function () {}; var makeSound = function ( animal ) { if( animal instanceof Duck){ console.log("嘎嘎嘎"); }else if( animal instanceof Chicken){ console.log("咯咯咯"); } }; makeSound(new Duck()); makeSound(new Chicken());
这里既没用到继承,也没用到多态。这样【写判断】是代码“不清爽”的罪魁祸首!
此处留一个疑问,如果不用判断,还可以怎么写?
在 vue2 中,我们可能会这么写:
export default { data() { return { }, mounted(){ this.Chicken() this.Duck() }, methods:{ funtion AnimalSound(sound){ console.log("叫声:" + sound) }, funtion Chicken(){ this.AnimalSound("咯咯咯") }, funtion Duck(){ this.AnimalSound("嘎嘎嘎") } } }
像这种函数嵌套调用是很常见的。没有看到继承,也没有看到多态,甚至都没有看到最根本的“类”?!
(实际上,每个函数都是一个 Function 对象。按照最开始定义所述,对象是类的实例,所以也是能在函数中看到“类”的!)
在 JavaScript 中,函数成了第一等公民! 函数似乎什么都能做!它可以返回一个对象,可以赋值给一个变量,可以作为数组项,可以作为对象的一个属性......
但这明显不是“类的设计模式”吧!
“类的设计模式” 意味着对【设计蓝图】的【复制】,在 JS 各种函数调用的场景下基本看不到它的痕迹。
“原型”设计模式
其实,众所周知,JS 也是能做到【继承】和【多态】的!只不过它不是通过类复制的方式,而是通过原型链委托的方式!
一图看懂原型链?
看不懂?没关系,记住这两句话再来看:
- 一个对象的显示原型的构造函数指向对象本身(很熟悉有没有?在本文哪里见过?)
- 一个对象的隐式原型指向构造这个对象的函数的显示原型。
原来,JS 不是通过在类里面写同名构造函数的方式来进一步实现的实例化,它的构造函数在原型上!这种更加奇特的代码服用机制有异于经典类的代码复用体系。
这里再附一个经典问题?JS new 操作会发生什么?
会是像类那样进行复制吗?
答案是否定的!
JS 访问一个对象的属性或方法的时候,先在对象本身中查找,如果找不到,则到原型中查找,如果还是找不到,则进一步在原型的原型中查找,一直到原型链的最末端。复制不是它所做的,这种查找的方式才是!对象之间的关系更像是一种委托关系,就像找东西,你在我这找不到?就到有委托关系的其它人那里找找看,再找不到,就到委托委托关系的人那里找......直至尽头,最后还找不到,指向 null。
所以:JavaScript 和面向对象的语言不同,它并没有类来作为对象的抽象模式或者设计蓝图。JavaScript 中只有对象,对象直接定义自己的行为。对象之间的关系是委托关系,这是一种极其强大的设计模式。在你的脑海中对象并不是按照父类到子类的关系垂直组织的,而是通过任意方向的委托关联并排组织的!
不过你也可以通过这种委托的关系来模拟经典的面向对象体系:类、继承、多态。但“类”设计模式只是一种可选的设计模式,你可以模拟,也可以不模拟!
现实是 ES6 class 给我们模拟了:
class Widget { constructor(width,height) { this.width = width || 50; this.height = height || 50; this.$elem = null; } render($where){ if (this.$elem) { this.$elem.css( { width: this.width + "px", height: this.height + "px" }).appendTo( $where ); } } } class Button extends Widget { constructor(width,height,label) { super( width, height ); this.label = label || "Default"; this.$elem = $( "<button>" ).text( this.label ); } render($where) { super.render( $where ); this.$elem.click( this.onClick.bind( this ) ); } onClick(evt) { console.log( "Button '" + this.label + "' clicked!" ); } }
看起来,非常不错,很清晰!
没有 .prototype 显示原型复杂的写法,也无需设置 .proto 隐式原型。还似乎用 extends 、super 实现了继承和多态。
然而,这只是语法糖的陷阱!JS 没有类,没有复制,它的机制是“委托”。
class 并不会像传统面向类的语言一样在申明时作静态复制的行为,如果你有意或者无意修改了父类,那子类也会收到影响。
举例:
class C { constructor() { this.num = Math.random(); } rand() { console.log( "Random: " + this.num ); } } var c1 = new C(); c1.rand(); // "Random: 0.4324299..." C.prototype.rand = function() { console.log( "Random: " + Math.round( this.num * 1000 )); }; var c2 = new C(); c2.rand(); // "Random: 867" c1.rand(); // "Random: 432" ——噢!
ES6 class 混淆了“类设计模式”和“原型设计模式”。它最大的问题在于,它的语 法有时会让你认为,定义了一个 class 后,它就变成了一个(未来会被实例化的)东西的 静态定义。你会彻底忽略 Class 是一个对象,是一个具体的可以直接交互的东西。当然,它还有其它细节问题,比如属性覆盖方法、super 绑定的问题,有兴趣自行了解。
总地来说,ES6 的 class 想伪装成一种很好的语法问题的解决方案,但是实际上却让问题更难解决而且让 JavaScript 更加难以理解。 —— 《你不知道的 JavaScript》
小结
- “类设计模式”的构造函数挂在同名的类里面,类的继承意味着复制,多态意味着复制 + 自定义。
- “原型设计模式”的构造函数挂在原型上,原型的查找是一种自下而上的委托关系。
- “类设计模式”的类定义之后就不支持修改。
- “原型设计模式”讲究的是一种动态性,任何对象的定义都可以修改,这和 JavaScript 作为脚本语言所需的动态十分契合!
你可以用“原型设计模式”来模拟“类设计模式”,但是这大概率是得不偿失的。
最后,如果再被问道:JavaScript 是面向对象语言吗?
如果这篇文章看懂了,就可以围绕:“类设计模式”和“原型设计模式”来吹了。
如果本文没有看懂,就把下面的标答背下来吧......
参考
以上就是类和原型的设计模式之复制与委托差异的详细内容,更多关于类原型设计模式复制委托差异的资料请关注脚本之家其它相关文章!