类和原型的设计模式之复制与委托差异


Posted in Javascript onJuly 07, 2022

小引

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 混淆了“类设计模式”和“原型设计模式&rdquo;。它最大的问题在于,它的语 法有时会让你认为,定义了一个 class 后,它就变成了一个(未来会被实例化的)东西的 静态定义。你会彻底忽略 Class 是一个对象,是一个具体的可以直接交互的东西。当然,它还有其它细节问题,比如属性覆盖方法、super 绑定的问题,有兴趣自行了解。

总地来说,ES6 的 class 想伪装成一种很好的语法问题的解决方案,但是实际上却让问题更难解决而且让 JavaScript 更加难以理解。 —— 《你不知道的 JavaScript》

小结

  • “类设计模式”的构造函数挂在同名的类里面,类的继承意味着复制,多态意味着复制 + 自定义。
  • “原型设计模式”的构造函数挂在原型上,原型的查找是一种自下而上的委托关系。
  • “类设计模式”的类定义之后就不支持修改。
  • “原型设计模式”讲究的是一种动态性,任何对象的定义都可以修改,这和 JavaScript 作为脚本语言所需的动态十分契合!

你可以用“原型设计模式”来模拟“类设计模式”,但是这大概率是得不偿失的。

最后,如果再被问道:JavaScript 是面向对象语言吗?

如果这篇文章看懂了,就可以围绕:“类设计模式”和“原型设计模式”来吹了。

如果本文没有看懂,就把下面的标答背下来吧......

类和原型的设计模式之复制与委托差异

参考

命名函数表达式探秘

函数式和面向对象编程有什么区别?

tutorials/js.mp

JavaScript 轻量级函数式编程

你不知道的JavaScript

以上就是类和原型的设计模式之复制与委托差异的详细内容,更多关于类原型设计模式复制委托差异的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
JavaScript中的small()方法使用详解
Jun 08 Javascript
基于jQuery实现点击列表加载更多效果
May 31 Javascript
原生JS实现层叠轮播图
May 17 Javascript
详解webpack解惑:require的五种用法
Jun 09 Javascript
原生JS实现图片懒加载(lazyload)实例
Jun 13 Javascript
浅谈Node Inspector 代理实现
Oct 19 Javascript
JS扩展String.prototype.format字符串拼接的功能
Mar 09 Javascript
JavaScript函数、闭包、原型、面向对象学习笔记
Sep 06 Javascript
vue 父组件中调用子组件函数的方法
Jun 06 Javascript
layui table单元格事件修改值的方法
Sep 24 Javascript
Vue + Scss 动态切换主题颜色实现换肤的示例代码
Apr 27 Javascript
vue中全局路由守卫中替代this操作(this.$store/this.$vux)
Jul 24 Javascript
JS高级程序设计之class继承重点详解
Jul 07 #Javascript
JS class语法糖的深入剖析
Jul 07 #Javascript
MutationObserver在页面水印实现起到的作用详解
Jul 07 #Javascript
js作用域及作用域链工作引擎
Promise静态四兄弟实现示例详解
Jul 07 #Javascript
Three.js实现雪糕地球的使用示例详解
二维码条形码生成的JavaScript脚本库
Jul 07 #Javascript
You might like
PHP怎样调用MSSQL的存储过程
2006/10/09 PHP
PHP配置文件中最常用四个ini函数
2007/03/19 PHP
Uchome1.2 1.5 代码学习 common.php
2009/04/24 PHP
超级实用的7个PHP代码片段分享
2012/01/05 PHP
如何用phpmyadmin设置mysql数据库用户的权限
2012/01/09 PHP
PHP中strtotime函数使用方法分享
2012/01/10 PHP
8个必备的PHP功能实例代码
2013/10/27 PHP
php版小黄鸡simsimi聊天机器人接口分享
2014/01/26 PHP
PHP扩展开发入门教程
2015/02/26 PHP
php实现window平台的checkdnsrr函数
2015/05/27 PHP
PHP正则验证Email的方法
2015/06/15 PHP
修复ShopNC使用QQ 互联时提示100010 错误
2015/11/08 PHP
php单例模式的简单实现方法
2016/06/10 PHP
用javascript实现兼容IE7的类库 IE7_0_9.zip提供下载
2007/08/08 Javascript
一个用javascript写的select支持上下键、首字母筛选以及回车取值的功能
2009/09/09 Javascript
实测jquery data()如何存值
2013/08/18 Javascript
浅谈JavaScript数据类型及转换
2015/02/28 Javascript
JS中的THIS和WINDOW.EVENT.SRCELEMENT详解
2015/05/25 Javascript
jquery+Jscex打造游戏力度条
2020/09/12 Javascript
docker中编译nodejs并使用nginx启动
2017/06/23 NodeJs
解决Vue不能检测数组或对象变动的问题
2018/02/24 Javascript
JS替换字符串中指定位置的字符(多种方法)
2020/05/28 Javascript
在Python中测试访问同一数据的竞争条件的方法
2015/04/23 Python
Python实现信用卡系统(支持购物、转账、存取钱)
2016/06/24 Python
代码讲解Python对Windows服务进行监控
2018/02/11 Python
Python连接Redis的基本配置方法
2018/09/13 Python
Python WEB应用部署的实现方法
2019/01/02 Python
关于python 的legend图例,参数使用说明
2020/04/17 Python
Python如何用wx模块创建文本编辑器
2020/06/07 Python
跑鞋、网球鞋、网球拍、服装及装备:Holabird Sports
2016/09/19 全球购物
实习生自荐信范文分享
2013/11/27 职场文书
文明青少年标兵事迹材料
2014/01/28 职场文书
和谐家庭事迹材料
2014/12/20 职场文书
2016年公司新年寄语
2015/08/17 职场文书
手写实现JS中的new
2021/11/07 Javascript
CPU不支持Windows11系统怎么办
2021/11/21 数码科技