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


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 相关文章推荐
eclipse导入jquery包后报错的解决方法
Feb 17 Javascript
滚动条响应鼠标滑轮事件实现上下滚动的js代码
Jun 30 Javascript
JQuery Tips相关(1)----关于$.Ready()
Aug 14 Javascript
JavaScript学习笔记之取数组中最大值和最小值
Mar 23 Javascript
jQuery实现背景弹性滚动的导航效果
Jun 01 Javascript
引用jquery框架后出错的解决方法
Aug 09 Javascript
手机浏览器 后退按钮强制刷新页面方法总结
Oct 09 Javascript
Bootstrap CSS布局之表单
Dec 17 Javascript
浅谈vue中慎用style的scoped属性
Nov 28 Javascript
7个好用的JavaScript技巧分享(译)
May 07 Javascript
js实现网页版贪吃蛇游戏
Feb 22 Javascript
vue缓存之keep-alive的理解和应用详解
Nov 02 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部分常见问题总结
2006/10/09 PHP
PHPMailer安装方法及简单实例
2008/11/25 PHP
PHP 删除文件与文件夹操作 unlink()与rmdir()这两个函数的使用
2011/07/17 PHP
探寻PHP脚本不报错的原因
2014/06/12 PHP
php常用字符串比较函数实例汇总
2014/11/24 PHP
PHP中的session安全吗?
2016/01/22 PHP
PHP在同一域名下两个不同的项目做独立登录机制详解
2017/09/22 PHP
PHP封装类似thinkphp连贯操作数据库Db类与简单应用示例
2019/05/08 PHP
Package.js  现代化的JavaScript项目make工具
2012/05/23 Javascript
JavaScript高级程序设计 阅读笔记(十四) js继承机制的实现
2012/08/14 Javascript
JavaScript String.replace函数参数实例说明
2013/06/06 Javascript
js图片处理示例代码
2014/05/12 Javascript
node.js中的fs.readlink方法使用说明
2014/12/17 Javascript
javascript+html5实现仿flash滚动播放图片的方法
2015/04/27 Javascript
jQuery插件HighCharts绘制的2D堆柱状图效果示例【附demo源码下载】
2017/03/14 Javascript
vue2.0 axios前后端数据处理实例代码
2017/06/30 Javascript
Angular实现预加载延迟模块的示例
2017/10/12 Javascript
jquery实现动态创建form并提交的方法示例
2019/05/27 jQuery
js实现漂亮的星空背景
2019/11/01 Javascript
解决vue.js提交数组时出现数组下标的问题
2019/11/05 Javascript
深入了解JS之作用域和闭包
2020/06/16 Javascript
js瀑布流布局的实现
2020/06/28 Javascript
在react-antd中弹出层form内容传递给父组件的操作
2020/10/24 Javascript
[31:00]2014 DOTA2华西杯精英邀请赛5 24 NewBee VS iG
2014/05/25 DOTA
Python正则表达式的使用范例详解
2014/08/08 Python
Android模拟器无法启动,报错:Cannot set up guest memory ‘android_arm’ Invalid argument的解决方法
2016/07/01 Python
python实现对指定输入的字符串逆序输出的6种方法
2018/04/26 Python
Python将一个Excel拆分为多个Excel
2018/11/07 Python
用xpath获取指定标签下的所有text的实例
2019/01/02 Python
Python实现计算文件MD5和SHA1的方法示例
2019/06/11 Python
详解python3中用HTMLTestRunner.py报ImportError: No module named 'StringIO'如何解决
2019/08/27 Python
Python3标准库之functools管理函数的工具详解
2020/02/27 Python
路政管理专业推荐信
2013/11/11 职场文书
springboot新建项目pom.xml文件第一行报错的解决
2022/01/18 Java/Android
python 安全地删除列表元素的方法
2022/03/16 Python
三种方式清除vue路由跳转router-link的历史记录
2022/04/10 Vue.js