首页 > 其他分享 >设计模式创建型之原型模式

设计模式创建型之原型模式

时间:2024-02-23 18:14:45浏览次数:26  
标签:__ 设计模式 创建 Object 原型 animal prototype class

实验介绍

本实验主要为大家介绍了前端中原型模式,为了加深大家对原型的了解,实验中花费大量篇幅讲解了原型及原型的概念,并配上了相关的例子以帮助大家学习。随后我们对 class 进行了简单的介绍,它可以被简单的认为是语法糖。最后,为了帮助大家理解原型中的克隆,实验也对浅拷贝与深拷贝进行了介绍。

知识点

  • 原型模式介绍
  • 原型及原型链
  • Class(类)
  • 浅拷贝与深拷贝
  • 实现挑战

原型模式介绍

原型模式定义:是用于创建重复的对象,同时又能保证性能。

JavaScript 的原型模式和其他静态语言的原型模式的表现形式是不太一样的,例如 java 中的原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

简单来讲,就是在 java 的类中,存在一个 clone 方法,通过这个 clone 方法能够快速的复制现有实例从而快速生成一个新实例。

这里为了不偏离本实验的主题,Java 中的原型模式例子可以参考:Java 原型模式 ,有 Java 基础的同学可以选择自行对照学习。

当然了,如果没有 Java 基础也无需担心,只要认真学习下面的内容,仍然能完全掌握前端层面的原型模式。

言归正传,接下来我们需要把全部注意力放在前端层面的原型模式上。为了更好的引出原型模式,在下面的小节中,会首先为大家介绍原型与原型链。这块内容理解起来会比较困难,希望大家能反复的学习,以求真正掌握。

原型及原型链

原型

在 JavaScript 中,我们首先明确两个概念,

  • 对象有一个特殊的隐藏属性 [[Prototype]]
  • 可以为函数添加一个名为 "prototype" 的常规属性,例如 Fn.prototype

现在无需你理解这两点,因在后面的内容中会对它们进行详细的讲解,不断地为你巩固这两个概念。

首先,[[Prototype]] 要么为 null,要么就是对另一个对象( 被称为 “ 原型 ” )的引用,就像这样:

图片描述

现在来看一个具体的例子。

首先我们需要新建一个 index.htmlprototype.js 文件,并引入,就像这样:

图片描述

然后在 prototype.js 中添加如下代码:

// prototype.js

// 动物对象
const animal = {
  eat: true,
};

// 兔子对象
const rabbit = {
  jump: true,
};

// 为 rabbit 添加原型( animal )
rabbit.__proto__ = animal;

console.log(rabbit.eat); // true

在上述代码中,出现了 __proto__ 这个属性。注意 proto 的前后都是两个下划线,这点很容易被忽略。

注:在这里你可以先简单的认为,__proto__ 就是 [[Prototype]]。但是这样说并不完全正确,不过这里你可以先这样理解,后面会专门为此做出解释。这里还是让我们回归上面的代码。

可以看到,我们通过 __proto__rabbit 添加了原型 animal 。随后在打印输出中,未在 rabbit 中定义的 eat 属性却成功的打印了 true 。这实际上就是原型的功劳。

重点在于:当我们读取对象的某个属性时,如果在这个对象中并没有定义该属性,那么它就会主动的去该对象的原型中寻找是否有该属性。

我们可以尝试访问一下 rabbit 和它原型中都没有定义的属性,例如 see ,结果只能是 undefined ,就像这样:

// prototype.js

console.log(rabbit.see); // undefined

如上,rabbitanimal 就构成了一个简单的原型关系。通过上面的例子相信大家对于原型有了一定的了解。

现在请回忆下我们一开始提到的第一个概念:对象有一个特殊的隐藏属性 [[Prototype]] 。我们正是通过它来定义原型关系的: [[Prototype]] 是对另一个对象的引用,而这个对象就被称作 “ 原型 ” 。

原型链

上面提到的第二个概念:可以为函数添加一个名为 "prototype" 的常规属性,例如 Fn.prototype

有这样一句话,存在即是合理。为什么我们需要为函数添加一个 prototype 属性,必然是有意义的。

例如:在 javascript 中,有一个内置函数 Object ,然后它本身具有这个 prototype 属性,我们可以打印出来看一下:( Object.prototype

图片描述

在图片中,可以清晰的看到 prototype 实际上就是一个对象,其中也有一个 __proto__ ,它的值是 null 。这就是我们一开始说的 [[Prototype]]null 的情况。

现在我们通过 Object 函数 new 一个 animal 对象:

// prototype.js

const animal = new Object();

console.log(animal);

打印结果是这样的:

图片描述

展开 animal 对象会发现它仍然有一个 [[Prototype]] 属性,那么通过 new 的方式实例出来的对象,它的原型是指向哪里的呢?我们可以展开它看看:

图片描述

可以看到,通过 new 的方式实例出来的 animal ,它的原型正是:Object.prototype,也就是 animal 的构造函数的 prototype 属性对象。

Object.prototype 本身的原型为 null,表示在 Object 上面就不存在其他原型了。

同样,我们按一开始的方式,为 animal 添加 eat 属性,并把它设为 rabbit 的原型,就像这样:

// prototype.js

const animal = new Object();

animal.eat = true;

const rabbit = {
  jump: true,
};

rabbit.__proto__ = animal;

console.log(rabbit.eat); // true

这一点与我们最上面的例子中并无区别,表示使用 {} 的方式书写对象和使用 new Object 的方式是一样的,前者是一种简写形式。

同学们可以打印一下 rabbit 对象,然后按上面的方式展开 [[Prototype]] 属性,你会发现:rabbit 的原型是 animalanimal 的原型是 Object.prototype,这便构成了一条 “原型链” 。

我们可以通过下面的方式进行验证:

// prototype.js

console.log(rabbit.__proto__ === animal); // true
console.log(animal.__proto__ === Object.prototype); // true

这里需要再次提醒大家几点:

  • 只有对象才有原型 [[Prototype]]
  • 函数可以添加一个 prototype 属性
  • prototype 本身是一个对象,所以它有 [[Prototype]]
  • 原型链可以很长

特别要指出的是:一个对象的原型要么是 null,要么就是对另一个对象的引用。是不可能出现原型是函数的情况。

要清楚,Object 是内置函数,而 Object.prototype 才是对象,因此 animal 的原型只能是 Object.prototype,而不是 Object

注:__proto__[[Prototype]] 的因历史原因而留下来的 getter/setter

我们上面提到可以简单的认为 __proto__ 就是 [[Prototype]] ,但是这样说并不是正确的。

请注意,__proto__ 与内部的 [[Prototype]] 不一样。__proto__[[Prototype]]getter/setter

__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__get/set 原型。

就像上面我们为 rabbit 添加了原型 animal,也可以使用这种方式:

const animal = {
  eat: true,
};

const rabbit = {
  jump: true,
};

Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.eat); // true

根据规范,__proto__ 必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。

一般的情况下,由于 __proto__ 标记在观感上更加明显,所以我们会在示例中使用这种方式。

内建原型

内建原型:Object、Array、Date、Function 等内建函数上的原型。

按照规范,所有的内建原型顶端都是 Object.prototype。这就是为什么有人说 “一切都从对象继承而来” 。

下面给出 3 个内建对象完整的示意图供参考:

图片描述

从图上也可以看到,原型由下向上追溯,直至 Object.prototype 的原型 null

对于原型,大家可以参考这篇文章:继承与原型链

原型模式在前端中的表现

这里我们仍然会使用 ES5 中定义构造函数的方式,为大家展示 JavaScript 中原型的具体表现。

这里新建一个 person.js 文件,引入 index.html 中。

首先,构建一个 Person 的构造函数:

// person.js

function Person(name, age, phone) {
  this.name = name;
  this.age = age;
  this.phone = phone;
}

随后在构造函数的 prototype 上添加一个 doEat 方法:

// person.js

Person.prototype.doEat = function (other, address) {
  console.log(`${this.name}和${other}在${address}吃饭!`);
};

现在就可以通过 Person 创建一个实例:

// person.js

const zhangsan = new Person("张三", 24, "13911111111");
console.log(zhangsan);

那么 person 是什么样的呢?请看下图

图片描述

可以看到,我们在 Person 的原型对象上绑定的 doEat 方法并没有出现在实例 zhangsan 中,但是我们可以通过 zhangsan 来调用 doEat 方法:

// person.js

zhangsan.doEat("罗翔", "朝阳区"); // 张三和罗翔在朝阳区吃饭!

在输出中展开原型,可以看到 doEat 正好挂在了 zhangsan 的原型上,也就是 Person.prototype 上:

图片描述

这里给出整个 zhangsan 的原型链:

图片描述

这就是原型的作用,相信通过这个小例子,大家可以能更好的了解原型。

而从这一点实际上大家可以看到,JS 中的原型不正是帮助我们在创建大量实例对象的时候,提升效率。我们并没有把一些公用方法写在实例中,但是实例化后的对象却可以顺利的调用到这些方法,这一点正好是原型模式在前端中的应用。

在上面我们反复的提到了原型与原型链,而实际上并未特别的对原型模式进行描述,这是因为在前端中,你完全可以将二者等同起来

接下来,我们会用 class 的方式再次实现这个例子,让大家了解下 ES6 中 class 和 原型的关系。

Class(类)

在 ES6 中,有一个高级的“类(class)”构造方式,它有许多非常棒的新功能,这些功能对于面向对象编程很有用。

新建一个 class.js 文件,引入 index.html 中。

基本语法是:

class MyClass {
  // 构造函数
  constructor() { ... }

// class 方法
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

那么 class 到底是什么,它本质上是一个函数:

// class.js

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert(this.name);
  }
}

console.log(typeof Person); // function

class 更确切地说,是 constructor 方法:

// class.js

console.log(Person === Person.prototype.constructor); // true

大部分文章会说,class 是一个语法糖。这样说在某种程度上没错,但是这样并不是完全准确的。

首先要明白什么是语法糖:为了使内容更易阅读和更方便表示,但不引入任何新内容的语法。

class 却并不是这样,它除了让原型继承实现的更加简单直接以外,还和原本的原型方式之间存在着差异。

不过在本实验中,为了不偏离主题,你可以简单的认为 class 就是语法糖,这并不影响下面的示例。

使用 class 定义一个 Person 类:

// class.js

class Person {
  constructor(name, age, phone) {
    this.name = name;
    this.age = age;
    this.phone = phone;
  }

    // 默认为 void,表示无返回值
  public doEat(other, address) {
    console.log(`${this.name}和${other}在${address}吃饭!`);
  }
};

转成 ES5 的语法:

var Person = (function () {
  function Person(name, age, phone) {
    this.name = name;
    this.age = age;
    this.phone = phone;
  }
  Person.prototype.doEat = function (other, address) {
    console.log(`${this.name}和${other}在${address}吃饭!`);
  };
  return Person;
})();

这段代码是对 class 编译后的结果( 删除了一些注释,方便大家阅读 )。

可以看到,我们在 class 中写的方法经过转换后,实际上也是挂在 Person.prototype 上的。从原型继承的角度上来讲,说 class 是原型继承的语法糖是没问题的。

浅拷贝与深拷贝

在 JS 中,拷贝分为深拷贝与浅拷贝,简单来说,如果是基本类型则通过等号可以拷贝出来一个新的值,不过如果是引用类型的值,那么实际上你拷贝的仅仅是地址,而这个地址指向的值才是真正的数据。

文章一开始我们提到,原型模式(Prototype Pattern)是用于创建重复的对象,虽然在 JS 中的原型模式并不完全是这样,但是针对创建重复对象,即 clone 的行为,也可以来看一下在 JS 中实现深拷贝的方式。

这里直接给出一个完整的深拷贝方案:

新建一个 clone.js 文件,引入 index.html 中。

// clone.js

const cloneDeep = (value) => {
  // 非数组和非对象直接返回值即可
  if (value == null || typeof value !== "object") {
    return value;
  }
  // 初始化
  let result = Array.isArray(value) ? [] : {};
  for (let key in value) {
    if (value.hasOwnProperty(key)) {
      result[key] = cloneDeep(value[key]);
    }
  }
  return result;
};

接下来定义一个覆盖比较全面的变量用于检测该方法的克隆能力:

// clone.js

const arr = [
  [1, 2, 3],
  { a: 4, b: 5 },
  null,
  undefined,
  true,
  0,
  function () {
    console.log("function");
  },
  Symbol("Symbol"),
];
const obj = {
  a: [1, 2, 3],
  b: { c: 4, d: 5 },
  e: null,
  f: undefined,
  g: true,
  h: 0,
  i: function () {
    console.log("function");
  },
  j: Symbol("Symbol"),
};

大家可以自行测验,这里也给出几个测试示例:

// clone.js

const newArr = cloneDeep(arr);

// 克隆不再相等
console.log(arr[0] === newArr[0]); // false

// 修改原本的数组,并不会影响到新克隆出来的数组
arr[1].a = 400;
console.log(arr[1].a, newArr[1].a); // 400 4

关于 JS 中的浅拷贝和深拷贝就简单介绍到这。如果想了解更多可以参考该文章:JS 浅拷贝与深拷贝

至此,对原型模式的介绍和相关的一些知识点就基本介绍完了。原型模式和前面提到的单例模式不一样,单例模式无论是在 Java 这类静态语言,还是 JavaScript 这种动态语言中,其实现上的基本思路是一致的。

而对于原型模式则不同,在 JavaScript 中原型的重要性是不可替代的,而原型模式在前端中的表现正是对原型和原型链的应用。而非生硬的去实现一个克隆(拷贝)。

如果大家想了解静态语言中的原型模式,可以参考:Java 原型模式

实现挑战

给定以下对象:

let head = {
  glasses: 1,
};

let table = {
  pen: 3,
};

let bed = {
  sheet: 1,
  pillow: 2,
};

let pockets = {
  money: 2000,
};
  1. 使用 __proto__ 来分配原型,以使得任何属性的查找都遵循以下路径:pockets → bed → table → head 。例如,pockets.pen 应该是 3(在 table 中找到),bed.glasses 应该是 1(在 head 中找到)。
  2. 回答问题:通过 pockets.glasses 或 head.glasses 获取 glasses,哪个更快?必要时需要进行基准测试。

答案代码放在实验最后课程的源码包里,大家可以自行下载。

实验总结

实验中花费了大量的篇幅为大家介绍原型和原型链,这不仅是 JS 的基石,也是我们理解前端中原型模式的一个重要前提。随后我们为大家介绍了一些其他相关的知识点,例如 class 和拷贝,都是为了让大家有一个更深的认识。相信通过本实验的学习,能为大家打好一个基础。

本节实验源码压缩包下载链接:原型模式源码

标签:__,设计模式,创建,Object,原型,animal,prototype,class
From: https://www.cnblogs.com/xzemt/p/18030130

相关文章

  • 设计模式创建型之工厂模式
    基本概念在给出工厂模式的定义之前,不妨先来了解一下工厂的概念。通过百度百科查到的所谓工厂的定义:是一类用以生产货物的大型工业建筑物,即我们为工厂输送原料,经过工厂对原料进行处理加工之后会输出产物。例如下面这样一个例子:张三是一名大学生,毕业后为了上班方便就考虑买一台......
  • 设计模式创建型之单例模式
    实验介绍本实验主要介绍了设计模式中的单例模式,在前端领域中,有很多地方都运用到了单例模式的思维,例如目前的主流前端框架中所用到的Redux和Vuex。实验首先通过一个小例子为大家展示了单例模式的实现原理,随后通过完成一个自定义的Storage存储器来帮助大家加深对单例模式的理......
  • 【Python&GIS】Python线矢量等距离取点/线等分取点&点创建矢量面
    ​        不多说,这是之前项目需求的代码,已经是去年的了一直没来的及发,今天抽出来一丢丢的空挡发一下。主要就是利用线矢量等距离生成点矢量,或者直接将线矢量等分生成点矢量,这个需求其实极限一下就是线转点了(将距离设置小一点)。顺便将点生成矩形面的代码也给出来,这里的......
  • 3分钟看懂设计模式02:观察者模式
    一、什么是观察者模式观察者模式又叫做发布-订阅模式或者源-监视器模式。结合它的各种别名大概就可以明白这种模式是做什么的。其实就是观察与被观察,一个对象(被观察者)的状态改变会被通知到观察者,并根据通知产生各自的不同的行为。以下为《设计模式的艺术》中给出的定义:观察者......
  • CreateHolesInImage说明文档-对于遥感影像的空洞创建多边形矢量数据
    提取遥感影像的空洞地理处理工具箱特点:通用地理处理工具,支持任何遥感影像,包括无人机,卫星遥感,普通图片和gdb,mdb数据库等。速度快,极致效率,效率高,支持对多个文件夹下的任意多数据进行批处理使用简单,全自动话,无人工干预功能:提取空洞提取空洞和非空洞默认临时文件夹,结果文件夹默认临时......
  • 在K8S中,请问harbor的secret创建能否直接创建资源清单?
    答案:当然可以,在Kubernetes(简称K8S)中,为了允许集群中的Pod能够从Harbor私有仓库拉取镜像,您可以直接通过编写资源清单(YAML文件)来创建一个Secret对象。这个Secret将包含访问Harbor所需的认证信息。以下是一个示例:apiVersion:v1kind:Secretmetadata:name:harbor-registry-secr......
  • 02. Create Project 创建项目导入素材
    使用版本我因为需要开发微信小游戏,所以使用的是2022.3.8f1c1版本创建项目首先创建一个3D项目,因为后续我们需要学习如何将一个普通的项目升级为URP通用渲染管线安装URP通用渲染管线在PackageManager中搜索Universal,然后安装UniversalRP。2022版本的Unity不需......
  • [设计模式]创建型模式-抽象工厂模式
    简介抽象工厂模式是一种创建型设计模式,它提供了一种创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式将一组具有共同主题的单个工厂封装起来,它提供接口用于创建相关或依赖对象的家族,而不需要指定具体的类。抽象工厂模式包含以下几个核心角色:抽象工厂(A......
  • 5 - 设备文件创建
    DeviceFileCreation原文链接我的博客创建字符设备设备文件在上一个小节中,我们知道了如何分配主副设备号,但是到此为止,只是创建主副设备号。并未在/dev目录下创建设备文件。设备文件设备文件可以实现用户空间应用与硬件的通讯。它们并不是普通文件,只是从编程视角来看是一......
  • RAID类型介绍、创建、彻底删除
    目录一、RAID(磁盘阵列)    1.1、概念    1.2、RAID0(条带化存储)    1.3、RAID1(镜像存储)    1.4、RAID5     1.5、RAID6       1.6、RAID1+0(先做镜像,再做条带)    1.7、RAID0+1(先做条带,再做镜像......