一,简单工厂模式
简单工厂模式最大的优点在于实现对象的创建和对象的使用分离.
依据这个思路,简单工厂模式应该遵循如下原则:封装不变的东西,开放变化的东西
1.1,创建一个对象
举个例子,当我们要创建一个动物对象的时候(具备基本的名字,年龄,喜好).我们写出如下代码:
javascript
const pig={
name:'佩奇',
age:18,
like:function(){
console.log(this.name+"喜欢吃东西")
}
}
pig.like()
这时候,如果还要创建一个动物,就得继续写:
javascript
const pig={
name:'佩奇',
age:18,
like:function(){
console.log(this.name+"喜欢吃东西")
}
}
const dog={
name:'旺财',
age:16,
like:function(){
console.log(this.name+"喜欢蹲村口")
}
}
pig.like()
dog.like()
那要是有很多动物对象需要创建呢?我们总不能一直复制新的对象吧?
1.2,使用类或者构造函数创建类
按照上文封装不变、开放变化的原则,可以使用一个 "动物工厂" ,让它来生产动物对象.
js
class Animal {
constructor(name,age,something){
this.name=name,
this.age=age
this.something=something
}
like(){
console.log(this.name+this.something)
}
}
const pig=new Animal('佩奇',18,"喜欢吃东西")
const dog=new Animal('旺财',16,"喜欢蹲村口")
pig.like()
dog.like()
而类或者说构造函数一旦创建,我们最希望的是在项目迭代过程中,不再发生改变(试想,如果每次迭代都要频繁改动构造函数,那不是会影响到所有它创建的实例对象了吗?这显然是不合理的)
1.3,从基类继承扩展新的类
比如说,现在要创建一个新的动物对象,它有一个新的方法,这时候,就可以使用继承的方式,让它不影响最基本的动物基类:
js
class Animal {
constructor(name,age,something){
this.name=name,
this.age=age
this.something=something
}
like(){
console.log(this.name+this.something)
}
}
class Duck extends Animal{
constructor(name,age,something) {
super(name,age,something);
}
do(){
console.log("游泳")
}
}
const pig=new Animal('佩奇',18,"喜欢吃东西")
const dog=new Animal('旺财',16,"喜欢蹲村口")
const duck1=new Duck('唐老鸭',19,"喜欢游泳")
pig.like()
dog.like()
duck1.do()
这样一来Duck类不仅能有自己特有的属性和方法,又不会对旧有的Animal类造成影响.
简单工厂的好处在于,我们可以在不动之前原逻辑的基础上,继承和拓展新的功能,这样我们就可以提高效率,之前大神写好的功能可以复用,而且可以站在巨人的肩膀上,不断拓展。
二,抽象工厂模式
其实上文1.3节从基类继承扩展新的类,就已经有一些抽象工厂的意味了.
抽象工厂是「开放封闭原则」的实践,开放封闭原则的内容:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。
抽象工厂声明一套规则的抽象类,自身没有任何实现,具体工厂通过继承抽象工厂开展逻辑编写.
也就是说基类它只做基本的规则约束,而不做任何实现.当有多个类似行为的工厂函数时,考虑用抽象工厂作为父类,在其中拟定具体工厂的行为,由具体工厂实现.
2.1,创建一个宾利车厂造宾利车
举个例子,我们来建个车厂造车,那么对应的抽象类,就有两个:工厂和产品车
js
//定义抽象工厂-只规定限制,不做具体实现:车厂需要能造车
class Factory {
createCar () {
throw new Error('不能调用抽象方法,需要重写');
}
}
//定义抽象产品类 Car :车具备能够开的方法
class Car {
drive () {
throw new Error('不能调用抽象方法,请重写');
}
}
而具体工厂用具体产品类创建对象时,产品类的共性也可以提取出来到一个抽象产品中
接着,我们创建一个宾利车厂实例:
js
//定义具体工厂实现抽象工厂-重写抽象工厂的方法
class BenzFactory extends Factory {
createCar () {
return new BentleyCar();
}
}
然后创建宾利车:
js
//由抽象产品类定义具体产品类
class BentleyCar extends Car {
drive () {
console.log('启动宾利');
}
}
于是就可以造车了:
js
let bentley = new BenzFactory();//创建一个宾利工厂实例
let bentleyFirst = bentley.createCar();//创建一辆宾利车实例
let bentleyFSecond = bentley.createCar();//创建一辆宾利车实例
bentleyFirst.drive();//启动宾利
bentleyFSecond.drive();//启动宾利
2.2,创建一个特斯拉车厂造特斯拉
如果这时候,又想开始造特斯拉,那么我们只要新建特斯拉工厂类和特斯拉车类即可:
js
//定义具体工厂实现抽象工厂-重写抽象工厂的方法
class TeslaFactory extends Factory {
createCar () {
return new TeslaCar();
}
}
//由抽象产品类定义具体产品类
class TeslaCar extends Car {
drive () {
console.log('启动特斯拉');
}
}
let tesla = new TeslaFactory();//创建一个特斯拉工厂实例
let teslaFirst = tesla.createCar();//创建一辆特斯拉车实例
let teslaSecond = tesla.createCar();//创建一辆特斯拉车实例
teslaFirst.drive();//启动特斯拉
teslaSecond.drive();//启动特斯拉
也就是说,之后有新的需求,只需要实现新的具体工厂和具体产品.
如果特斯拉工厂有自己的特殊方法,特使拉车有自己的特殊属性,也不会影响到工厂基类和汽车基类.
2.4,抽象工厂模式总结
那么,抽象工厂模式和简单工厂模式有啥区别呢?
很明显的一点是:
erlang
简单工厂模式创建的基类,我们有用它来直接创建实例.而抽象工厂模式,它的基类只被继承扩展,以重写它内部的方法.而不直接用它来创建实例对象.
为啥会有这个区别呢?主要还是使用场景导致的.
在简单工厂的使用场景里,主要是共性特别容易抽离,且不易变更的(这样就不会频繁进行修改).
而抽象工厂处理的是类,这些类中不仅有各种类别,同时存在着千变万化的扩展可能性------这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色类:
抽象工厂、具体工厂、抽象产品、具体产品
具体工厂的作用就是产出具体产品.
三,单例模式
在前端开发中,单例模式是一种常用的设计模式,它用于限制一个类只能创建一个对象。这种模式在处理全局状态或需要跨多个实例共享资源的情况下特别有用。
3.1,保证一个类仅有一个实例
那我们要做的事情就是保证一个类仅有一个实例.
那需要如何实现呢?看如下代码:
js
class Config {
constructor() {
if (Config.instance) {
return Config.instance;
}
Config.instance = this;
this.config = { /* 配置信息 */ };
}
}
const instanceFirst=new Config()
const instanceSecond=new Config()
console.log(instanceFirst===instanceSecond)//true
我们第一次创建这个类的实例的时候,就会在instance这个属性上存储这个实例.
下一次再利用这个类创建实例的时候,只要判断instance已经存储了,就直接返回该实例,从而实现该类只拥有一个实例.
3.2,单例模式的应用一:vuex
单例模式在处理全局状态或需要跨多个实例共享资源的情况下特别有用。
例如我们vue项目常常使用的vuex,就是利用的单例模式.不过它的单例有点特殊:它只是限定一个vue应用中的store是同一个.
我们来手写一下简单版的vuex:
js
//./store/store.js
let Vue;
function install(_Vue, storeName = '$store') {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
//当执行new Vue的时候把store添加到vue的原型上,这样vue实例可以使用this.$store来访问
if (this.$options.store) {
Vue.prototype[storeName] = this.$options.store;
}
}
})
}
class Store {
constructor(options) {
let {
state = {}, getters = {}, mutations = {}, actions = {}, modules = {}
} = options;
for (var key in modules) {
state = {
...state,
...modules[key].state
}
mutations = Object.assign(mutations, modules[key].mutations);
getters = {
...getters,
...modules[key].getters
};
actions = Object.assign(actions, modules[key].actions);
}
this.state = new Vue({
data: state
});
this.actions = actions;
this.mutations = mutations;
this.allGetters = getters;
this.observerGettersFunc(getters);
}
// 触发mutations,需要实现commit
commit = (type, arg) => {
const fn = this.mutations[type]
fn(this.state, arg)
}
// 触发action,需要实现dispatch
dispatch = (type, arg) => {
const fn = this.actions[type]
return fn({
commit: this.commit,
state: this.state
}, arg)
}
//数据劫持getters
observerGettersFunc(getters) {
this.getters = {} // store实例上的getters
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => {
console.log(`retrunValue:` + JSON.stringify(this.state.$data));
return getters[key](this.state)
}
})
})
}
}
export default {
Store,
install
}
然后在使用时:
js
//./store/index.js
import Vue from 'vue'
import Vuex from './store'
Vue.use(Vuex)//会执行vuex.install方法
export const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
export const store2 = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
console.log("----++++----",store===store2)//false
export default store
Main.js中使用
js
import Vue from 'vue'
import App from './App.vue'
import store from './store/index.js'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
可以看到,store并不会全等于store2,也就是说,vuex并没有限制store的单例,那一个vue项目中它又是如何限制只有一个实例的呢?
实际上,主要是利用vue.use方法,它的原理如下,就是执行传入对象的install方法,而在执行install之前,会先判断Vue._installedPlugins 中是否存储已经安装的插件信息来确认该插件是否被安装过。
js
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 获取已经安装的插件
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 看看插件是否已经安装,如果安装了直接返回
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// toArray(arguments, 1)实现的功能就是,获取Vue.use(plugin,xx,xx)中的其他参数。
// 比如 Vue.use(plugin,{size:'mini', theme:'black'}),就会回去到plugin意外的参数
const args = toArray(arguments, 1)
// 在参数中第一位插入Vue,从而保证第一个参数是Vue实例
args.unshift(this)
// 插件要么是一个函数,要么是一个对象(对象包含install方法)
if (typeof plugin.install === 'function') {
// 调用插件的install方法,并传入Vue实例
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// 在已经安装的插件数组中,放进去
installedPlugins.push(plugin)
return this
}
}
它的作用就是:
1,检查插件是否安装,如果安装了就不再安装
2,如果没有没有安装,那么调用插件的install方法,并传入Vue实例
这样一来,无论Vue.use(Vuex)执行多少次,因为传入的vuex是同一个(import引入的是同一个),所以只会执行一次install,在vue的原型对象上挂载一个$store.
3.3,单例模式应用二:全局toast
这里我用的是vue3的写法.
第一步,先创建基本的vue文件:
js
./MyToast.vue
<template>
<div class="toast-box" v-if="showLoad">
<div class="toast-value" :style="{ background: background }">
<div class="loading"></div>
<div class="content" v-if="message">{{ message }}</div>
</div>
</div>
</template>
<script setup>
let props = defineProps({
message: {
type: String,
default: ''
},
background: {
type: String,
default: 'rgba(0,0,0,0.7)'
},
showLoad: {
type: Boolean,
default: false
}
});
</script>
<style lang="scss" scoped>
.toast-box {
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
.toast-value {
width: 220px;
height: 220px;
padding: 8px 10px;
border-radius: 20px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
animation: anim 0.5s;
.content {
text-align: center;
margin-top: 30px;
color: rgba(255, 255, 255, 0.7);
font-size: 22px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
@keyframes anim {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.toast-value.reomve {
animation: reomve 0.5s;
}
@keyframes reomve {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.loading {
width: 60px;
height: 60px;
border: 3px solid rgb(253, 252, 252);
border-top-color: transparent;
border-radius: 100%;
text-align: center;
animation: circle infinite 1s linear;
}
// 转转转动画
@keyframes circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>
第二步,创建index.js文件,实现单例模式的toast
js
import { createVNode, render } from 'vue';
import toastTemplate from './MyToast.vue';
let modal = null
class Toast {
constructor(options){
if(!Toast.instance){
modal = document.createElement('div');
const opt = { ...options };
let vm = createVNode(toastTemplate, opt); // 创建vNode,就是把参数传入,创建一个游离的dom
render(vm, modal);//render方法会在指定dom节点下挂虚拟dom
document.body.appendChild(modal); // 添加到body上
Toast.instance = this;
}else{
const opt = { ...options };
let vm = createVNode(toastTemplate, opt);
render(vm, modal);
document.body.appendChild(modal);
return Toast.instance;
}
}
destory(){
if (modal) {
let myDom = document.querySelector('.toast-value');
myDom && myDom.classList.add('reomve');
const t = setTimeout(() => {
// 淡入淡出效果之后删除dom节点
render(null, modal);
document.body.removeChild(modal);
clearTimeout(t);
}, 500);
}else{
return
}
}
}
export default Toast;
第三步:在页面中使用:
js
const toast1=new Toast({
message:'竹杖芒鞋轻胜马',
showLoad:true
})
const toast2=new Toast({
message:'也无风雨也无晴',
showLoad:true
})
setTimeout(()=>{
toast1.destory()
},2000)
console.log(toast1===toast2)//true
可以看到,无论我们创建多少个实例,其实都同一个,也只会在body下添加一个子元素节点.
四,原型模式
原型模式(prototype):是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型链相关知识可以看我之前这篇文章:原型链和原型对象 - 掘金 (juejin.cn)
在最开始的时候,我们通过上文的工厂模式,创建对象是通过如下的方式:
js
function Animal(name, age) {
this.name = name
this.age = age
}
Animal.prototype.info = function() {
console.log(this.name+'今年'+this.age+'岁啦')
}
const animal1 = new Animal('猪八戒', 3)
const animal2 = new Animal('啸天犬', 18)
animal1.info()//猪八戒今年3岁啦
animal2.info()//啸天犬今年18岁啦
可以看到,创建对象,我们需要不断new Animal然后传入参数.实际上,animal1和animal2的基本一摸一样.我们是不是可以考虑直接复制一份.但是为了避免相互影响,需要进行深拷贝一份.
可以使用Object.create方法来创建这样的对象,该方法创建指定的对象,其对象的prototype有指定的对象(也就是该方法传进的第一个参数对象),也可以包含其他可选的指定属性。例如Object.create(proto, [propertiesObject])
于是按照原型模式可以写为:
javascript
function Animal(name, age) {
this.name = name
this.age = age
}
Animal.prototype.info = function() {
console.log(this.name+'今年'+this.age+'岁啦')
}
const animal1 = new Animal('猪八戒', 3)
const animal2 = Object.create(animal1)
animal2.name="沙悟净"
animal1.info()//猪八戒今年3岁啦
animal2.info()//沙悟净今年3岁啦
console.log(animal2.__proto__===animal1)//true
对应的原型链示意图如下:
所以上文中console.log(animal2.proto===animal1)才是true.
五,建造者模式
当我们需要创建复杂对象时,这些对象可能有多个属性,属性之间存在依赖关系,或需要按照特定的骤来创建。在这种情况下,使用建造者模式 (Builder Pattern
)可以提供一种活的方式来构建对象,避免对象构建过程的复杂性。
"分即是合" - 将对象的创建过程与其表示相互分离,但允许我们连续地构建对象,逐步设置其属性,然后获取最终的构建结果。
使用建造者模式,我们可以按照自己的需求构建对象,而不必关心对象的创建过程和内部细节。
js
class Car {
constructor() {
this.brand = null;
this.color = null;
this.engine = null;
}
}
class CarBuilder {
constructor() {
this.car = new Car();
}
// 品牌
setBrand(brand) {
this.car.brand = brand;
return this;
}
// 颜色
setColor(color) {
this.car.color = color;
return this;
}
// 引擎
setEngine(engine) {
this.car.engine = engine;
return this;
}
build() {
return this.car;
}
}
// 创建汽车对象示例
const car = new CarBuilder()
.setBrand("Tesla")
.setColor("Red")
.setEngine("Electric")
.build();
console.log(car);
系列文章
本文是我整理的js基础文章中的一篇,下面是已经完成的文章:
从promise到await - 掘金 (juejin.cn)
从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)