JavaScript需要掌握的特性和设计模式,你get了吗?

1,592 阅读2分钟

设计模式0.5: 与众不同的JavaScript

基于对象的JavaScript

JavaScript与很多面向对象的语言不同,它最初并没有class这个概念,ES6新增的class关键字其实也是通过函数实现的。

JavaScript的变量都是基于对象的,为什么这么说呢?

我们来看几个例子:

例1:

1639206159086.png

这里我定义了一个数,也就是一个number类型的变量,我们通过__proto__去查询它的原型。可以看到,他是属于一个Number类的。侧面可以看出,即使一个数,也是一个对象,有自己的方法(从基类继承过来)。

例2:

let a = new Object()  // a = {}
Object.defineProperties(a, { 
    'age': { value:12, writable:false }
})
a.age // 12 
a.age = 1
a.age // 12

在这个例子中,我们使用所有类的基类Object,new了一个变量。此时这个变量里是什么都没有的,但是我们使用defineProperties方法,就可以向变量中定义属性,而且还可以通过writable控制是否可以更改变量值。这其实就是一个对象最基本的创建和定义属性的方法。

例3:

let str = 'abcd'
console.log(str.charAt(1)) // b
String.prototype.charAt = (num) => `你的参数是${num}`
console.log(str.charAt(1)) // 你的参数是1

我们先是定义了一个字符串,然后使用了charAt方法,这显而易见是可以的。接下来,我们更改了String类的原型prototype上的charAt方法,然后重新str.charAt(1)而这一次,返回的东西却发生了变化。这说明了字符串也是一个对象,而且有一个原型链,用来调用基类(String)的方法。

通过new来解析原型和原型链的作用

既然JavaScript是基于对象的,那么我们要想让一个任何一个变量做一些事,那么我们就需要让对象有一些属性,这些属性从哪里来呢?答案是原型上。

1639209250722.png

str的原型上,有很多的方法,当str需要调用的时候,由于自己没有这个属性,就回去原型上找,从而实现使用String类功能的效果。

__proto__的绑定过程,一般是在new一个新的基类对象中完成的。

当我们执行new方法时,经历了如下步骤

a = new A('xx')

  1. 创建一个新对象
  2. 将prototype和新对象的__proto__(这里原型链也会起作用)连接
  3. 执行(构造)函数的A.call(a,'xx'),绑定新对象的this指向
  4. 返回 this (在构造函数没有返回值的情况下)

从而实现了原型的传递,形成了原型链。

JavaScript中的class

calss是在ES6中才正式出现的,class的功能其实都可以使用函数来实现,所以class也可以是说是一个函数。我们使用class定义一个类,再使用new关键字实例化。这样意义是,每次new都会返回一个新对象,使得每次在使用某一个类new时,new出的对象之间都不会互相影响。

此外,由于JavaScript中并没有面向对象中一些特性,所以在JavaScript中,只有两种类,基类和派生类。所有类的基类是Object,当我们使用 class A extends B {}继承时,B作为基类,A为派生类。

设计模式1:全局变量优化

情景1:

当我们需要实现一个验证用户是否是作者的功能时,我们可以用这样几个函数:

const checkId = function() {}
const checkName = function() {}
const checkWorksNum = function() {}

这样看起来没有什么问题。

但是当我们又要实现一个验证是否是读者的功能时,我们也需要定义几个重复的函数:

const checkId = function() {}
const checkName = function() {}

但是由于之前已经定义过这两个函数了,那么这两个函数就会和上面的函数混淆。

情景2:

还是上面的函数,如果我们或者其他人在之后的使用时增加了新的功能,有更改了一些功能。 那么此时由于他是一个全局变量,那么我们之前的函数就会受到后来函数的影响。

以上情景都是变量污染的例子,由于全局变量一旦定义,就可以随处调,但是也随处可以更改并影响全局,可谓是一把双刃剑。那么,对于上面的情况,我们应该怎么处理呢?

对于情景一,我们可以采用用一个对象将函数包裹起来来使用:

const checkAutor = {
	checkId: function() {},
	checkName: function() {},
    checkWorksNum: function() {}
}
...
const checkReader = {
    checkId: function() {},
	checkName: function() {}
}

这样,我们之后调用检查是否是作者的方法,就可以通过调用相应的对象方法来使用了:

checkAutor.checkId()

对于场景二,我们可以采用类来使得每次调用对象的时候都是新的对象,从而实习不会互相干扰的效果。

class CheckAuthor {
    checkId = function() {}
	checkName = function() {}
    checkWorksNum = function() {}
}

let a = new CheckAuthor()

这样我们就实现了每次调用的时候,通过new来新建对象,而不是使用同一个。

设计模式2:js单例模式

(14) js设计模式(一)-单例模式 - SegmentFault 思否

单例模式的优缺点和使用场景 - 晓明的哥哥 - 博客园 (cnblogs.com)

js前端单例模式实现与应用 - 简书 (jianshu.com)

javascript单例模式(懒汉 饿汉) - 奋斗中的小鸟 - 博客园 (cnblogs.com)

构建单例模式的几种方法:

//整个模块定义一个对象


//实例模式的三种方法
//一开始就创建一个实例(饿汉模式)
function One() {
    if(!One.instance) {
        this.fn = () => {
            console.log('>>>>')
        }
        this.balabala = 'QAQ'
        One.instance = this
    }
    return One.instance
}

//调用时才创建一个方法(懒汉模式)
function One() {
    if(One.instance) 
        return One.instance
    else {
        this.fn = () => {
            console.log('>>>>')
        }
        this.balabala = 'QAQ'
        One.instance = this
    }
}

//class创建
class One {
    instance = null
    constructor() {
        this.balabala = 'hello',
        this.fn = () => {
            console.log('>>>>')
        }
    }
    static getInstance = () => {
        if(!this.instance) {
            this.instance = new One()
        }
        return this.instance
    }
}
//三种方法下的输出结果
const a = One.getInstance()
const b = One.getInstance()
b.balabala = 'world'
console.log(a) //One { instance: null, balabala: 'world', fn: [Function (anonymous)] }
console.log(b) //One { instance: null, balabala: 'world', fn: [Function (anonymous)] }
console.log(a === b) //true

设计模式3:工厂模式

简单工厂模式

又叫静态工厂方法,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

又叫静态工厂方法,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

实例:

function factory(type) {
    function Atype() {
        this.value = 'A'
        this.view = 'Atype'
    }
    function Btype() {
        this.value = 'B'
        this.view = 'Btype'
    }
    function Ctype() {
        this.value = 'C'
        this.view = 'Ctype'
    }
    switch(type) {
        case 'A': {
            return new Atype()
            break;
        }
        case 'B': {
            return new Btype()
            break;
        }
        case 'C': {
            return new Ctype()
            break;
        }
    }
}


const a = factory('A') // a = {value:'A',view:'Atype'}

由于上面这种写法,我们每次增加或修改类,都需要在两个地方操作,不是很方便,所以我们可以优化一下

function factoryPro(type) {
    function Type(opt) {
        this.value = opt.value
        this.view = opt.view
    }
    switch(type) {
        case 'A': {
            return new Type({
                value: 'A',
                view: 'Atype'
            })
            break;
        }
        case 'B': {
            return new Type({
                value: 'B',
                view: 'Btype'
            })
            break;
        }
        case 'C': {
            return new Type({
                value: 'C',
                view: 'Ctype'
            })
            break;
        }
    }
}

工厂方法模式

在上述的情况下,工厂是直接生产一个类,但是如果没有这个类,我们则需要有个"兜底"提示。同时,也可以增加一个功能,灵活处理直接使用方法和new方法的两种情况。同时,我们可以不在通过函数直接创建类,而是通过增加一个

funtion factory(type) {
	if(this instanceof factory){
        	if(this[type]) {
                var a = new this[type]()
            	return a
            }
            else {
                //...兜底
            }
    }
    else{
            return new factory(type);
    }
}
factory.prototype = {
    Atype: function() {
        this.value = 'A'
        this.view = 'Atype'
    },
    Btype: function() {
        this.value = 'B'
        this.view = 'Btype'
        this.fn = () => {...}
    }
}

抽象工厂模式

这种工厂方法其中的类是抽象类,通过子类的继承来实现实例,即function agency(subType, superType) subType是子类superType是抽象类。agency方法将抽象类让子类给继承。但是我觉得使用extends关键字也可以完成一样操作,非大型项目,这种模式用处不大。

设计模式5:外观模式(兼容模式)

对一个dom的点击事件来说,如果我们为其绑定了onclick事件,由于onclick方法是一个DOM0级的事件,当其他人又通过这种方式为当前的dom绑定了点击的事件时,相当于重复定义了一个方法,之前的onclick方法就会被覆盖。

此时我们可以采用DOM2级事件处理程序提供的方法,即时使用addEventListener实现,然后IE9之前的浏览器或一些不太常用的浏览器是不支持这种方法的,所以需要用attachEvent,而如果遇到不支持DOM2级事件处理程序兜底浏览器,就只能用onclick方法绑定事件。

为了兼容这些浏览器,可以时使用外观模式:通过定义一个更简单的高级接口,将复杂的底层接口或逻辑判断封装

//外观模式实现click绑定
function addEvent(dom, type, fn) {
    if(dom.addEventListener) {
		dom.addEventListener(type, fn, false) //默认在冒泡阶段执行
    }else id(dom.attachEvent) {
        dom.attachEvent('on' + type, fn)
    }else {
		dom['on' + type] = fn 
    }
}

通过外观模式

相关拓展

关于DOM事件流、DOM0级事件与DOM2级事件 - 云+社区 - 腾讯云 (tencent.com)

设计模式6:代理模式

不直接访问对象,而是设置一个中间对象来通过它间接访问。控制了对带访问对象的访问途径,起到了区分权限和保护对象的作用。

js 设计模式——代理模式 - 妖色调 - 博客园 (cnblogs.com)

设计模式7:装饰者模式

再不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法)使原有对象可以满足用户的更复杂需求。其要点在于如何在保证原对象不变的情况下仍然可以拓展。

例如我们可以通过装饰者模式进行onclick方法拓展:

/**
*	@description dom是需要装饰的对象,fn是需要被拓展的方法
*/
const decorator = (dom, fn) => {
	if(typeof dom.onclick === 'function') {
		const oldFn = dom.onclick
		dom.onclick = () => {
            oldFn()
            fn()
        }
	}
    else {
        dom.onclick = fn
    }
}

设计模式8:观察者模式

观察者模式是这样一种设计模式。一个被称作被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。

在Vue,React上我们都可以看到观察者订阅者的身影。实现观察者订阅者有两种较为实用的方法:

  • Object.defineProperty()

    Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, 'value', {
    value: 12,
    get: () => {
        console.log(obj.value)
    },
    set: (newVal) => {
        console.log(newVal)
    }
})

这样我们就实现了简单的对一个对象查询和设置时的监听

  • Proxy

    作为Vue3代替Object.defineProperty()的对象,它实现了对数组的监听,以及对动态添加的对象的实时监听。Proxy直接劫持对象并返回新对象。性能也会有相应的优化

const handler = {
    get: (obj, prop) => {
        if(obj[prop])
        console.log(obj[prop])
        return obj[prop]
    },
    set: (obj, prop, value) => {
        obj[prop] = value;
        console.log(value)
        // 表示成功
    	return true;
    }	
}
const p = new Proxy({}, handler);

设计模式9:状态模式

当一个对象的内部状态发生变化的时候,会导致行为发生改变,看起来好像是改变了对象。

举例子:当我们处理Ajax的时候,会遇到不同的statusCode,此时我们一般会通过if..else来解决

if(res.statusCode === 200) {
	...
}else if(res.statusCode === 401) {
	...
}else if(res.statusCode === 403) {
	...
}

但是这样不断的分支判断并不是最优写法,我们可以创建一个对象,每一种条件都作为对象内部的一种状态,面对不同的判断结果,就变成了选择对象内的一种状态。

const StateFn =  function() {
    let status = {
        '200': function() {
            console.log('状态码是200,请求成功')
            //...
        },
        '401': function() {
            //...
        },
        '403': function() {
            //...
        }
    }
    const show = (code) => {
        status[`${code}`]() && status[`${code}`]() 
    }
    return { show }
}

const stateFn = StateFn()
stateFn.show(200)

这里我们利用函数来创建对象,ES6后,也可以使用class关键字解决。

设计架构:MVC,MVP,MVVM

这三种都是在软件设计时的常用架构模式。

MVC

MVC模式将架构分为了三部分:

  • 视图(View):用户界面。
  • 控制器(Controller):业务逻辑
  • 模型(Model):数据保存

其各部分间的通讯模式为:视图层传送指令给控制器;控制器执行一定的业务逻辑后,修改好了数据,再要求Model层更新数据保存;Model层将新的数据发送给视图并且得到反馈。

MVP

MVP架构的三部分:

  • 视图(View):用户界面。
  • 控制器(Presenter):业务逻辑
  • 模型(Model):数据保存

为了避免模型与视图之间的耦合,MVP模式中模型不与视图层发生通信,而是模型与视图层全部和控制器进行双向通信

通讯模式:视图层向Presenter请求加载数据,Presenter加载数据给视图层。Model层变得很简单,只负责接收和为Presenter加载数据。

这种模式让V和M解耦,层次清晰。

MVVM

MVVM和MVP模式十分相似,唯一的不同在于,将控制器(Presenter)改变成了ViewModel层,它的作用在于实现与View视图层的双向绑定,有利于数据更新时视图层的快速更新。

MVC,MVP 和 MVVM 的图示 - 阮一峰的网络日志 (ruanyifeng.com)