小引
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。
不过你也可以通过这种委托的关系来模拟经典的面向对象体系:类、继承、多态。但“类”设计模式只是一种可选的设计模式,你可以模拟,也可以不模拟!
现实是 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 绑定的问题,有兴趣自行了解。
小结
-
“类设计模式”的构造函数挂在同名的类里面,类的继承意味着复制,多态意味着复制 + 自定义。
-
“原型设计模式”的构造函数挂在原型上,原型的查找是一种自下而上的委托关系。
-
“类设计模式”的类定义之后就不支持修改。
-
“原型设计模式”讲究的是一种动态性,任何对象的定义都可以修改,这和 JavaScript 作为脚本语言所需的动态十分契合!
你可以用“原型设计模式”来模拟“类设计模式”,但是这大概率是得不偿失的。
最后,如果再被问道:JavaScript 是面向对象语言吗?
如果这篇文章看懂了,就可以围绕:“类设计模式”和“原型设计模式”来吹了。
如果本文没有看懂,就把下面的标答背下来吧......
关注公众号《掘金安东尼》,持续输出ing!!!
最近有点疲于写业务代码?......谁能支支招?
参考文献
- 命名函数表达式探秘
- 函数式和面向对象编程有什么区别?
- tutorials/js.mp
- 你不知道的 JavaScript
- JavaScript 轻量级函数式编程
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!