设计模式(前端)

设计模式

1.环境搭建

1)初始化npm环境

下载node.js

执行npm init命令 (生成package.json)

根目录下,新建src文件夹,src文件夹下新建index.js文件:

alert("Hello World");

2)安装webpack

npm install webpack webpack-cli --save-dev

为了安装的快一点,使用npm.taobao.org淘宝镜像地址:

npm install webpack webpack-cli --save-dev --registry=https://registry.npm.taobao.org

安装完成后package.json变成(devDependencies里有webpack和webpack-cli:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2"
  }
}

根目录下创建webpack.dev.config.js:

module.exports = {
    // 项目入口文件
    entry: './src/index.js',
    // 项目出口文件
    output: {
        path: __dirname,
        file: './release/bundle.js'
    }
}

修改package.json(“scripts"里新增"dev”):

在package.json文件里面,使用scripts字段定义脚本命令,scripts字段是一个对象。它的每一个属性,对应一段脚本。这些定义在package.json里面的脚本,就称为 npm 脚本。命令行下使用npm run命令,就可以执行这段脚本。更加详细的介绍看下面这一篇文章。

http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

Webpack 在执行的时候,除了在命令行传入参数,还可以通过指定的配置文件来执行。默认情况下,会搜索当前目录的 webpack.config.js 文件。 通过webpack的–config选项来指定配置文件。webpack功能强大,有很多独特的功能,但其中一个难点是配置文件。为此,webpack团队改变了这一现状:webpack 4默认不需要配置文件。可以通过mode选项为webpack指定一些默认的配置。mode分为development/production,默认为production。

webpack4 mode 的默认设置的介绍见这篇文章:https://segmentfault.com/a/1190000013712229

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --config ./webpack.dev.config.js --mode development"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2"
  }
}

执行npm run dev,就会生成之前在webpack.dev.config.js配置的出口文件。

3)安装webpack-dev-server

安装webpack-dev-server和html-webpack-plugin。

npm install webpack-dev-server html-webpack-plugin --save-dev

淘宝镜像后加 --registry=https://registry.npm.taobao.org

根目录下创建index.html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>设计模式</title>
</head>
<body>
    <p>设计模式</p>
</body>
</html>

修改webpack.dev.config.js:

// 引入node的path模块
const path = require('path')
// 引入html-webpack-plugin,可以根据你设置的模板,在每次运行后生成对应的模板文件,同时所依赖的CSS/JS也都会被引入,如果CSS/JS中含有hash值,则html-webpack-plugin生成的模板文件也会引入正确版本的CSS/JS文件。
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    // 项目入口文件
    entry: './src/index.js',
    // 项目出口文件
    output: {
        path: __dirname,
        file: './release/bundle.js'
    },
    // 插件列表
    plugins: [
        new HtmlWebpackPlugin({
            // 模板的路径。支持加载器,webpack打包的bundle.js文件会插入到index.html中去执行。
            template:'./index.html',

            // title: 生成的HTML模板的title,如果模板中有设置title的名字,则会忽略这里的设置
            // title:"设计模式",

            // inject: 引入模块的注入位置;取值有true/false/body/head
            // inject: "body",

            // 生成的模板文件的名字
            // filename: "./index.html",

            // favicon:"",
            // favicon: 指定页面图标

            // cache: 是否需要缓存,如果填写true,则文件只有在改变时才会重新生成
            // cache:true,

            // 是否生成hash添加在引入文件地址的末尾,类似于我们常用的时间戳,比如最终引入是:<script type="text/javascript" src="bundle.049424f7d7ea5fa50656.js?049424f7d7ea5fa50656"></script>。这个可以避免缓存带来的麻烦。
            // hash: true,

            // chunks: 引入的模块,这里指定的是entry中设置多个js时,在这里指定引入的js,如果不设置则默认全部引入
            // chunks:"",

            // chunksSortMode: 引入模块的排序方式
            // chunksSortMode:"auto",

            // excludeChunks: 排除的模块
            // excludeChunks:"",
        })
    ],
    // 在开发模式下,DevServer 提供虚拟服务器,提供实时重新加载让我们进行开发和调试,大大减少开发时间。
    devServer: {
        // 提供哪里的内容给虚拟服务器用。这里最好填 绝对路径
        contentBase: path.join(__dirname,'./release'),

        // 如果为 true ,页面出错不会弹出 404 页面。
        historyApiFallback:true,

        // 热模块更新作用。即修改或模块后,保存会自动更新,页面不用刷新呈现最新的效果。
        // hot: true,

        // 运行npm run dev,自动打开浏览器
        open: true,

        // 主机名。默认 localhost。
        // host: 127.0.0.1,

        // 端口号。默认 8080。
        port:9000,

        // 如果为 true ,开启虚拟服务器时,为你的代码进行压缩。
        // compress:true,

        // 如果为 true ,在浏览器上全屏显示编译的errors或warnings。默认 false (关闭)
        overlay:true,
        // 如果你只想看 error ,不想看 warning:
        // overlay:{
        //     errors:true,
        //     warnings:false
        // },

        // true,则终端输出的只有初始启动信息。 webpack 的警告和错误是不输出到终端的。
        quiet:true,

        // 配置了 publicPath后, url = '主机名' + 'publicPath配置的' + '原来的url.path'。这个其实与 output.publicPath 用法大同小异。
        // output.publicPath 是作用于 js, css, img 。
        // 而 devServer.publicPath 则作用于请求路径上的。
        // devServer.publicPath
        // publicPath: "/assets/"
        // 原本路径 --> 变换后的路径
        // http://localhost:8080/app.js --> http://localhost:8080/assets/app.
        
        // 当您有一个单独的API后端开发服务器,并且想要在同一个域上发送API请求时,则代理这些 url 。
        // proxy: {
        //     '/proxy': {
        //         target: 'http://your_api_server.com',
        //         changeOrigin: true,
        //         pathRewrite: {
        //             '^/proxy': ''
        //         }
        // }
        // (1)假设你主机名为 localhost:8080 , 请求 API 的 url 是 http://your_api_server.com/user/list
        // (2)'/proxy':如果点击某个按钮,触发请求 API 事件,这时请求 url 是http://localhost:8080/proxy/user/list 。
        // (3)changeOrigin:如果 true ,那么 http://localhost:8080/proxy/user/list 变为 http://your_api_server.com/proxy/user/list 。但还不是我们要的 url 。
        // (4)pathRewrite:重写路径。匹配 /proxy ,然后变为'' ,那么 url 最终为 http://your_api_server.com/user/list 。

        // 一组自定义的监听模式,用来监听文件是否被改动过。
        // watchOptions: {
        //     // 一旦第一个文件改变,在重建之前添加一个延迟。填以毫秒为单位的数字。
        //     aggregateTimeout: 300,
        //     // 填以毫秒为单位的数字。每隔(你设定的)多少时间查一下有没有文件改动过。不想启用也可以填false。
        //     poll: 1000,
        //     // 观察许多文件系统会导致大量的CPU或内存使用量。可以排除一个巨大的文件夹
        //     ignored: /node_modules/
        // }
    }
}

修改webpack为webpack-dev-server:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --config ./webpack.dev.config.js --mode development"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2"
  }
}

4)安装babel

安装babel-core、babel-loader、babel-polyfill、babel-preset-es2015 和 babel-preset-latest。

npm install babel-core babel-loader babel-polyfill babel-preset-es2015 babel-preset-latest --save-dev

babel-core是作为babel的核心存在,babel的核心api都在这个模块里面,比如:transform。

polyfill这个单词翻译成中文是垫片的意思,详细点解释就是桌子的桌脚有一边矮一点,拿一个东西把桌子垫平。polyfill在代码中的作用主要是用已经存在的语法和api实现一些浏览器还没有实现的api,对浏览器的一些缺陷做一些修补。

理解polyfill的意思之后,再来说说babel为什么存在polyfill。因为babel的转译只是语法层次的转译,例如箭头函数、解构赋值、class,对一些新增api以及全局函数(例如:Promise)无法进行转译,这个时候就需要在代码中引入babel-polyfill,让代码完美支持ES6+环境。

presets,polyfill一个一个配置插件会非常的麻烦,为了方便,babel为我们提供了一个配置项叫做persets(预设)。预设就是一系列插件的集合,就好像修图一样,把上次修图的一些参数保存为一个预设,下次就能直接使用。babel-preset-es2015 是一个babel的插件,用于将部分ES6 语法转换为ES5 语法。

babel-preset-latest。这是一个特殊预设,将包含所有年度预设,因此用户无需单独指定每个预设。

babel-cli是一个通过命令行对js文件进行换码的工具。

.babelrc是babel的全局配置文件,所有的babel操作(包括babel-core、babel-node)基本都会来读取这个配置。后面的后缀rc来自linux中,使用过linux就知道linux中很多rc结尾的文件,比如.bashrc,rc是run command的缩写,翻译成中文就是运行时的命令,表示程序执行时就会来调用这个文件。

babel所有的操作基本都会来读取这个配置文件,除了一些在回调函数中设置options参数的,如果没有这个配置文件,会从package.json文件的babel属性中读取配置。

使用方法:

直接在命令行输出转译后的代码:

babel script.js

指定输出文件:

babel script.js --out-file build.js

或者是:

babel script.js -o build.js

2.面向对象

1)什么是面向对象

// 类
class People {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    eat() {
        alert(`${this.name} eat something`)
    }
    speak() {
        alert(`My name is ${this.name}, age ${this.age}`)
    }
}
// 创建实例
let zhang = new People('zhang', 20)
zhang.eat()
zhang.speak()
// 创建实例
let wang = new People('wang', 21)
wang.eat()
wang.speak()

2)三要素:继承、封装、多态

继承:子类继承父类
封装:数据的权限和保密
多态:同一接口不同实现

继承
// 父类
class People {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    eat() {
        alert(`${this.name} eat something`)
    }
    speak() {
        alert(`My name is ${this.name}, age ${this.age}`)
    }
}
// 子类-继承父类 extends为继承关键字
class Student extends People {
    constructor(name, age, number) {
        // super(name, age),即name和age传给父类的构造函数来执行
        super(name, age)
        this.number = number
    }
    study() {
        alert(`${this.name} study`)
    }
}
// 实例
let xiaoming = new Student('xiaoming', 10, 'A1')
xiaoming.speak()
console.log(xiaoming.number)
// 实例
let xiaohong = new Student('xiaohong', 11, 'A2')
xiaohong.study()

People是父类,公共的,不仅仅服务于Student
继承可将公共方法抽离出来,提高复用,减少冗余

封装

对属性和方法的修饰:
public 完全开放
protected 对子类开放
private 对自己开放

单纯的面向对象语言就是通过上述3个关键字来做封装的,但ES6不支持,这里我们使用typescript来演示。

// 父类
class People {
    // ts中必须先声明属性变量(类似Java语法),后面才可以使用。虽然不灵活,但很规范、标准。如果代码写的有问题,预编译就会报错。
    name // 前面什么都不写,默认就是public
    age
    protected weight // 定义 protected 性质的变量

    constructor(name,age) {
        this.name = name
        this.age = age
        this.weight = 120
    }
    
    eat() {
      alert(`${this.name} eat something`)
    }
    speak() {
      alert(`My name is ${this.name},age ${this.age}`)
    }
}
// 子类 继承父类
class Student extends People {
    // 也要先声明变量
    number
    private girlfriend // 定义 private 属性

    constructor(name,age,number) {
        super(name,age)
        this.number = number
        this.girlfriend = 'xiaoli'
    }
    study() {
        alert(`${this.name} study`)
    }
    getWeight() {
      alert (`${this.weight}`)
    }
}
// 实例
let xiaoming = new Student('xiaoming',10.'A1')
xiaoming.getWeight()
// 私有属性,只能在类内被访问。
// console.log(xiaoming.girlfriend) // 注意,编译时会报错,直接会编译不通过!!!

我们可以在 http://www.typescriptlang.org/play/ 这个网址下我们的可以运行typescript代码和转换为javascript代码。

减少耦合,不改外漏的不外漏
利于数据、接口的权限管理
ES6目前不支持,一般认为 _开头的属性是private。

多态

同一接口,不同表现
js应用极少
需要结合java等语言的接口、重写、重载等功能

// 父类
class People {
    constructor(name) {
        this.name = name
    }
    saySomething() {

    }
}
// 子类A
class A extends People {
    constructor(name) {
        super(name)
    }
    saySomething() {
        alert('I am A')
    }
}
// 子类B
class B extends People {
    constructor(name) {
        super(name)
    }
    saySomething() {
        alert('I am B')
    }
}
// 针对两个实例执行同样的方法,结果是不一样的。定义一个接口,在子类中实现不同的功能。
let a = new A('a')
a.saySomething() // I am A
let b = new B('b')
b.saySomething() // I am B

保持子类的开放性和灵活性:不是所有都由父类控制,既能放在父类中减少代码量和冗余,又能针对特殊性特殊处理。
面向接口编程:有时不用管子类接口下面具体是怎么实现的,只用管父类有什么接口就行。
(js应用极少,了解即可)

3)js应用举例

jQuery可认为是一个class。
任意获取的对象,例如 $(‘p’) 可认为jQuery的一个实例。

class jQuery {
    // 构造函数,里面传一个选择器
    constructor(selector) {
        // 获取数组的slice函数,slice(start,end) 方法可从已有的数组中返回选定区间的元素。
        let slice = Array.prototype.slice
        // 获取dom节点,querySelectorAll() 方法返回文档中匹配指定 CSS 选择器的所有元素,返回 NodeList 对象。然后我们通过slice.call()方法将它变为数组。
        let dom = slice.call(document.querySelectorAll(selector))
        // 获取数组长度
        let len = dom ? dom.length : 0
        // 循环遍历,将dom节点赋值给实例的元素,并赋值长度和选择器
        for (let i = 0; i < len; i++) {
            // 以对应下标为key,value为对应的dom节点,给实例对象赋值
            this[i] = dom[i]
        }
        // 并给对象长度属性
        this.length = len
        // 给对象selector属性
        this.selector = selector || ''
    }
    append(node) {

    }
    addClass(name) {

    }
    html(data) {

    }
    // 此处省略若干 API
}
// window.$赋值为一个函数,返回 jQuery的一个实例。
window.$ = function (selector) {
    // 工厂模式
    return new jQuery(selector)
}
var $p = $('p')
console.log($p)
console.log($p.addClass)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
  <p>设计模式1</p>
  <p>设计模式2</p>
  <p>设计模式3</p>
</body>
</html>

4)面向对象的意义

为何使用面向对象
程序执行: 通过3种方式(顺序、判断、循环)————实现程序运行结构化
面向对象:实现数据结构化(将零零散散的数据结构化)
对于计算机,结构化的才是最简单的。
编程应该 简单&抽象

3.UML类图

Unified Modeling Language 统一建模语言
UML包含很多种图,我们这里只用其中的类图。
关系,主要讲解泛化(继承)和关联(引用)。其它如实现、聚合、组合、依赖等不会太多涉及。
演示,代码和类图结合。

画法:

类图:1列3行

规则:

1. 类名
2. 属性
    + public属性名A:类型
    # protected属性名B:类型
    -  private属性名C:类型
3. 方法
    + public方法名A(参数1,参数2):返回值类型
    # protected方法名B(参数1,参数2):返回值类型
    - private方法名C(参数1):返回值类型

关系:
泛化(继承):空心箭头;
关联(引用):实心箭头;

工具:微软的MS Office visio。

网站:https://www.processon.com/

class People {
    constructor(name,house) {
        this.name = name
        // 引用对象
        this.house = house
    }
    saySomething() {

    }
}
// 子类A
class A extends People {
    constructor(name,house) {
        super(name,house)
    }
    saySomething() {
        alert('I am A')
    }
}
// 子类B
class B extends People {
    constructor(name,house) {
        super(name,house)
    }
    saySomething() {
        alert('I am B')
    }
}

// 引用的类
class House {
    constructor(city) {
        this.city = city
    }
    showCity() {
        alert(`house in ${this.city}`)
    }
}
// 针对两个实例执行同样的方法,结果是不一样的。定义一个接口,在子类中实现不同的功能。
let aHouse = new House('beijing')
let a = new A('a',aHouse)
console.log(a) // a有房子 
let b = new B('b')
console.log(b) // b无房子

4.设计原则

1)何为设计?

按照哪一种思路或者标准来实现功能。

功能相同,可以有不同设计方案来实现。

伴随着需求增加,设计的作用才能体现出来。

《UNIX/LINUX设计哲学》
基本准则

准则1:小即是美
准则2:让每个程序只做好一件事
准则3:快速建立原型
准则4:舍弃高效率而取可移植性
准则5:采用纯文本来存储数据
准则6:充分利用软件的杠杆效应(软件复用)
准则7:使用shell脚本来提高杠杆效应和可移植性
准则8:避免强制性的用户界面
准则9:让每个程序都成为过滤器

小准则

1.允许用户定制环境
2.尽量使操作系统内核小而轻量化
3.使用小写字母并尽量简短
4.沉默是金(举例:例如,要求输入的是数字,当输入的不是数字时,不回应比回应好)
5.各部分之和大于整体
6.寻求90%的解决方案

演示
  1. 让每个程序成为过滤器
ls 列出文件夹下的文件和文件夹

ls | grep *.json    ls中的结果通过竖线过滤到后一个集合中选择出所有格式为json的文件

ls | grep *.json | grep 'package' 然后在继续选有'package'这个关键字的文件。
  1. 沉默是金

意思是当没有我们想要的结果时,什么都不输出。

ls | grep *.json | grep 'package12345'

什么都不输出。

而不是输出"no file"之类的话。

ls | grep *.json | grep 'package12345' | wc -l 查看结果的行数。结果是0。

wc指令我们可以计算文件的Byte数、字数、或是列数。

2)五大设计原则

S-单一职责原则(single)
一个程序只做好一件事。
如果功能过于复杂就拆分开,每个部分保持独立。
O-开放封闭原则(open)
对拓展开放,对修改封闭。
增加需求时,拓展新代码,而非修改已有代码。
这是软件设计的终极目标。
L-里氏置换原则(Liskov)
子类能够覆盖父类。
父类能出现的地方子类就能出现。
JS中使用较少(弱类型 & 继承使用较少)

(这项原则最早是在1988年,Barbara Liskov 提出来的,是美国第一个获得计算机科学博士学位的女性 。芭芭拉·利斯科夫(Barbara Liskov),本名Barbara Jane Huberman。美国计算机科学家,2008年图灵奖得主,2004年约翰·冯诺依曼奖得主。现任麻省理工学院电子电气与计算机科学系教授。Barbara Liskov被授予2008年度图灵奖得主,以表彰她对编程语言和系统设计方面所做出的实践与理论基础,尤其是数据抽象、容错和分布式计算方面的贡献。她 也是第二位获得此奖项的女性科学家。)
I-接口独立原则(interface)
保持接口单一独立,尽量避免出现“胖接口”。
JS中没有接口(typescript例外),使用较少。
类似于单一职责原则,这里更关注接口。
D-依赖导致原则(dependence)
面向接口编程,依赖于抽象而不依赖于具体
使用方只关注接口而不关注具体类的实现
JS中使用较少(没有接口 & 弱类型)

注意,js中S和O体现较多,将详细介绍,而LID体现较少,但要了解其用意。

用Promise来说明SO:

    function loadImg(src) {
        var promise = new Promise(function (resolve,reject) {
            var img = document.createElement('img')
            img.onload = function () {
                resolve(img)
            }
            img.onerror = function () {
                reject('图片加载失败')
            }
            img.src = src
        })
        return promise
    }
    var src = 'https://www.imooc.com/static/img/index/logo_new.png'
    var result = loadImg(src)
    result.then(function (img) {
        // part1
        console.log('img.width',img.width)
        return img
    }).then(function (img) {
        // part2
        console.log('img.height',img.height)
    }).catch(function (ex) {
        // 统一捕获异常
        alert(ex)
    })

这里是如何体现S和O原则的呢?

S:每个then中只做好一件事。

O:如果有新增需求,扩展then。

如果有新的需求进来,第一、二个then可以不用动。我们甚至可以通过模块化将各个逻辑拆分到不同的文件中,就会更加直观和独立。

3)从设计到模式

4)23种设计模式

创建型:
    工厂模式(工厂方法模式,抽象工厂模式,建造者模式)
    单例模式
    原型模式

组合(或结构)型:
    适配器模式
    装饰器模式
    代理模式
    外观模式

    略讲:
        桥接模式
        组合模式
        享元模式

行为型:
    策略模式
    模板方法模式
    观察者模式(重点)
    迭代器模式
    职责链模式
    命令模式
    备忘录模式
    状态模式(重点)
    访问者模式
    中介者模式
    解释器模式

5)学习设计模式的流程:

1.介绍和举例(生活中易理解的示例)
2.画UML类图写demo代码
3.结合经典应用场景,学习该设计模式如何被使用。

明白每个设计的道理和用意
通过经典应用体会它的真正使用场景
自己编码时多思考,尽量模仿

5.真题

题目1

打车时,可以打专车或者快车。任何车都有车牌号和名称。

不同车价格不同,快车每公里1元,专车每公里2元。

行程开始时,显示车辆信息。

行程结束时,显示打车金额(假定行程就5公里)。

要求:

1)画出UML类图。

2)用ES6语法写出该示例。

分析:

设置一个公共的父类。因为任何车都有车牌号和名称。

父类下面又有两个子类'专车"和"快车"。它们有自己各自的价格属性。

行程和"车的父类"有关联关系,而不是和某个车有关联关系。依赖于抽象编程而不依赖于具体编程。

无论什么车,都有车辆信息,都可以计算出金额。因此,我们需要新建一个"行程"的类,并且需要引用车的信息。

从而得出车的信息(车牌号、名称、单价),进而计算出打车金额。所以,金额应该是行程的属性。只有有行程才有金额。

代码:

    class Car {
        constructor(number,name) {
            this.number = number
            this.name = name
        }
    }

    class Kuaiche extends Car {
        constructor(number,name){
            super(number,name)
            this.price = 1
        }
    }

    class Zhuanche extends Car {
        constructor(number,name){
            super(number,name)
            this.price = 2
        }
    }

    class Trip {
        constructor(car) {
            this.car = car
        }
        start() {
            console.log(`行程开始,名称:${this.car.name},车牌号:${this.cat.price}`)
        }
        end() {
            console.log('行程结束,价格:' + (this.car.price * 5))
        }
    }

    let car = new Kuaiche(100,'桑塔纳')
    let trip = new Trip(car)
    trip.start()
    trip.end()

题目2

(1)某停车场,分3层,每层100车位。
(2)每个车位都能监控到车辆的驶入和离开。
(3)车辆进入前,显示每层的空余车位数量。
(4)车辆进入时,摄像头可识别车牌号和时间。
(5)车辆出来时,出口显示器显示车牌号和停车时长。

要求:
1)画出UML类图。
2)用ES6语法写出该示例。

分析:
(1)"某停车场,分3层,每层100车位。"这句话中包含3个类,停车场、层、车位。

(2)还要一个类是车辆。车位类要有驶入和离开的方法。同时要有一个属性,切换状态,监听车辆的驶入和离开。

(3)车辆进入前,停车场类里要有一个方法,要能显示出每层的空余车位数量。具体的显示多少要交给层类,每一层显示自己的空余车位数量。最终由停车场汇总显示各个层的空余车位数量。

(4)还需一个摄像头class,针对一个车,可以识别车牌号以及记录进入时间。摄像头这个类,输入的是车辆,输出的是车牌号和时间。车牌号和时间需要被记录存储在停车场类中的车辆列表属性中。可以以车牌号为key,进入时间为value存起来。

(5)还需要显示器class。根据车牌号取出进入时间,再用当前时间减去进入时间计算出停车时长。

代码:

// 车辆类
class Car {
    constructor(num) {
        this.num = num
    }
}

// 摄像头
class Camera {
    shot(car) {
        return {
            num: car.num,
            inTime: Date.now()
        }
    }
}

// 出口显示屏
class Screen {
    show(car,inTime) {
        console.log('车牌号',car.num)
        console.log('停车时间',Date.now() - inTime())
    }
}

// 停车场
class Park {
    constructor(floors) {
        this.floors = floors || []
        this.camera = new Camera()
        this.screen = new Screen()
        // 存储摄像头拍摄返回的车辆信息
        this.carList = {}
    }
    // 车辆驶入
    in(car) {
        // 通过摄像头获取信息
        const info = this.camera.shot(car)
        // 停到某个停车位(获取0-99某个随机数)
        const i = parseInt(Math.random() * 100 % 100)
        // 这里只停到第一层的某个车位
        const place = this.floors[0].places[i]
        // 停车
        place.in()
        // 把停车位和车辆的对应信息放到info.place中
        info.place = place
        // 记录信息
        this.carList[car.num] = info

    }
    // 车辆离开
    out(car) {
        // 获取信息info中的num、inTime、carList
        const info = this.carList[car.num]
        // 将停车位清空
        const place = info.place
        place.out()
        // 显示时间
        this.screen.show(car,info.inTime)
        // 清空记录
        delete this.carList[car.num]
    }
    // 计算停车场所有空余车位数量
    emptyNum() {
        return this.floors.map(floor => {
            return `${floor.index} 层还有 ${floor.emptyPlaceNum} 个空余车位`
        }).join('\n')
    }
}

// 层
class Floor {
    constructor(index,places) {
        this.index = index
        this.places = places || []
    }
    emptyPlaceNumber() {
        let num = 0
        this.places.forEach(p => {
            if(p.empty){
                num = num + 1
            }
        })
        return num
    }
}

// 车位
class Place {
    constructor() {
        // 默认为空
        this.empty = true
    }
    in() {
        this.empty = false
    }
    out() {
        this.empty = true
    }
}


// ------------测试-------------------
// 初始化3层
const floors = []
for (let i = 0; i <3; i++){
    const places = []
    for (let j = 0; j < 100; j++) {
        places[j] = new Place()
    }
    floors[i] = new Floor(i + 1,places)
}
// 初始化停车场
const park = new Park(floors)

// 初始化车辆
const car1 = new Car(100)
const car2 = new Car(200)
const car3 = new Car(300)

console.log('第一辆车进入')
console.log(park.emptyNum())
park.in(car1)

console.log('第二辆车进入')
console.log(park.emptyNum())
park.in(car2)

console.log('第一辆车离开')
park.out(car1)

console.log('第二辆车离开')
park.out(car2)

console.log('第三辆车进入')
console.log(park.emptyNum())
park.in(car3)

console.log('第三辆车离开')
park.out(car3)

6.工厂模式

1)介绍

将new操作单独封装。

遇到new时,就要考虑是否该使用工厂模式。

2)演示

去购买汉堡,直接点餐、取餐,不会自己亲手做。

餐厅要"封装"做汉堡的工作,做好直接给买者。

class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        alert('init')
    }
    fn1() {
        alert('fn1')
    }
    fn2() {
        alert('fn2')
    }
}

class Creator {
    create(name) {
        return new Product(name)
    }
}

// -------测试-------------
// 生成一个工厂
let creator = new Creator()
// 通过工厂生成一个product实例
let p = creator.create('p1')
p.init()
p.fn1()

3)场景

(1)jQuery $(‘div’)

$(‘div’)和new $(‘div’)有何区别
第一:书写麻烦,jQuery的链式操作将成为噩梦
第二:一旦jQuery名字变化,将是灾难性的

阅读经典lib源码的意义

学习如何实现功能

学习设计思路

强制模拟编写类似代码

(2)React.createElement 即jsx
// 编译前:
var profile = <div>
    <img src="avatar.png" className="profile" />
    <h3>{[user.firstName,user.lastName].join(' ')}</h3>
</div>;

// 编译后:
var profile = React.createElement("div",null,
    React.createElement("img",{ src: "avatat.png", className: "profile" },
    React.createElement("h3",null, [user.firstName,user.lastName].join(" "))
);

// 其实createElement就是工厂模式
class Vnode(tag,attrs,children) {
    // ......
}
React.createElement = function (tag,attrs.children){
    return new Vnode(tag,attrs,children)
}
(3)vue 异步组件
vue.component('async-example',function(resolve,reject){
    serTimeout(function(){
        resolve({
            template:'<div>I am async!</div>'
        })
    },1000)
})

4)设计原则验证

构造函数和创建者分离

符合开放封闭原则

7.单例模式

1)介绍

系统中被唯一使用
一个类只有一个实例

2)演示

登录框

购物车

vuex和redux中的store

单例模式需要用到Java的private特性

ES6中没有(typescript除外)

只能用Java代码来演示UML图的内容

Java中使用单例模式:

public class SingleObject {
    // 注意,私有化构造函数,外部不能new,只能内部new!!!
    private SingleObject(){}
    // 唯一被new出来的对象
    private SingleObject instance = null;
    // 获取对象的唯一接口
    public SingleObject getInstance() {
        if (instance == null) {
            // 只new一次
            instance = new SingleObject();
        }
        return instance;
    }

    // 对象方法
    public void login(username,password){
        System.out.println("login...");
    }
}

// -------测试--------------
public class SingletonPatternDemo {
    public static void main(String[] args){
        // 不合法的构造函数
        // 编译时错误:构造函数 SingleObject() 是不可见的!!!
        // SingleObject object = new SingleObject();

        // 获取唯一可用的对象
        SingleObject object = SingleObject.getInstance();
        object.login();
    }
}

JS中使用单例模式:

class SingleObject {
    login() {
        console.log('login...')
    }
}
SingleObject.getInstance = (function(){
    let instance
    return function () {
        if (!instance) {
            instance = new SingleObject();
        }
        return instance
    }
})()

// -------------测试----------------
// 注意,这里只能使用静态函数getInstance,不能 new SingleObject()!!!
let obj1 = SingleObject.getInstance()
obj1.login()
let obj2 = SingleObject.getInstance()
obj2.login()
// 两者完全相等
console.log(obj1 === obj2) // true

// 如果是new SingleObject(),二者不等
let obj3 = new SingleObject()
obj.login()
console.log(obj1 === obj3) // false

3)场景

(1)jQuery 只有一个$
// jQuery只有一个'$'
if (window.jQuery != null){
    return window.jQuery
} else {
    // 初始化
}
(2)模拟登陆框
class LoginForm {
    constructor() {
        this.state = 'hide'
    }
    show() {
        if (this.state === 'show'){
            alert('已经显示')
            return
        }
        this.state = 'show'
        console.log('登陆框显示成功')
    }
    hide() {
        if (this.state === 'hide'){
            alert('已经隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功')
    }
}

LoginForm.getInstance = (function () {
    let instance
    return function () {
        if (!instance) {
            instance = new LoginForm();
        }
        return instance
    }
})()

// 测试
let login1 = LoginForm.getInstance()
login1.show() // 登录框显示成功
let login2 = LoginForm.getInstance()
login2.show() // 登录框已显示

let login3 = LoginForm.getInstance()
login3.hide() // 登录框隐藏成功
console.log( login1 === login3) // true

4)设计原则验证

符合单一职责原则,只实例化唯一的对象

没法集体体现开放封闭原则,但是绝对不违反开放封闭原则

8.适配器模式

1)介绍

旧接口格式和使用者不兼容

中间加一个适配器转换接口

2)演示

插头转换

代码演示:

// 被适配的目标
class Adaptee {
    specificRequest() {
        return '德国标准插头'
    }
}

// 适配器
class Target {
    constructor() {
        this.adaptee = new Adaptee()
    }
    request() {
        let info = this.adaptee.specificRequest()
        return `${info} - 转换器 - 中国标准插头`
    }
}

// 测试
let target = new Target()
let res = target.request()
console.log(res)

3)场景

(1)封装旧接口
ajax({
    url:'/getData',
    type:'Post'
    dataType:'json',
    data: {
        id:"123"
    }
})
.done(function(){})

// 但因为历史原因,代码中全都是:
// $.ajax({...})

// 做一层适配器
var $ = {
    ajax:function (options){
        return ajax(options);
    }
}
(2)vue computed
<div id="example">
    <p>Original message:"{ message }"</p>
    <p>Computed reversed message:"{ reversedMessage }"</p>
</div>
var vm = new Vue({
    el:'#example'
    data: {
        message: 'Hello'
    },
    computed: {
        // 计算属性的getter
        reversedMessage: function(){
            // 'this' 指向 vm 实例
            return this.message.split('').reverse().join('')
        }
    }
})

4)设计原则验证

将旧接口和使用者进行分离

符合开放封闭原则

9.装饰器模式

1)介绍

为对象添加新功能

不改变其原有的结构和功能

2)演示

class Circle {
    draw() {
        console.log('画一个圆形')
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle
    }
    draw() {
        this.circle.draw()
        this.setRedBorder(circle)
    }
    setRedBorder(circle){
        console.log('设置红色边框')
    }
}

// 测试代码
let circle = new Circle()
circle.draw() // 画一个圆形

let dec = new Decotator(circle)
dec.draw() // 画一个圆形 // 设置红色边框

3)场景

(1)ES7 装饰器

配置环境

安装插件npm install babel-plugin-transform-decorators-legacy --sava-dev

放入.babelrc文件中的"plugins"数组中

接着就可以正常使用装饰器了。

装饰类
无参数:
// @装饰器函数:对class进行装饰
@testDec
class Demo {

}

function testDec(target){
    target.isDec = true
}

alert(Demo.isDec)

装饰器的原理:

@decorator
class A {}

// 等同于
class A {}
A = decorator(A) || A;
有参数:
function testDec(isDec) {
    return function(target) {
        target.isDec = isDec;
    }
}

@testDec(true)
class Demo {
    // ...
}
alert(Demo.isDec) // true
装饰类-mixin示例
function mixins(...list) {
    return function (target) {
        // Object.assign(target, ...sources) 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。浅拷贝。
        Object.assign(target.prototype,...list)
    }
}

const Foo = {
    foo() { alert('foo') }
}

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // 'foo'
装饰方法
例1:
function readonly(target,name,descriptor) {
    // descriptor属性描述对象(Object.defineProperty 中会用到),原来的值如下:
    // {
    //     value: specifiedFunction,
    //     enumerable: false,
    //     configurable: true,
    //     writable: true
    // };
    descriptor.writable = false;
    return descriptor;
}

class Person {
    constructor() {
        this.first = 'A'
        this.last = 'B'
    }

    // 装饰方法
    @readonly
    name() { return `${this.first} ${this.last}`}
}

var p = new Person()
console.log(p.name())
// p.name = function () {} // 这里会报错,因为 name 是只读属性
例2:
function log (target,name,descriptor) {
    var oldValue = descriptor.value;

    descriptor.value = function() {
        // 先打印日志
        console.log(`Calling ${name} with`,arguments);
        // 再执行函数
        return oldValue.apply(this,arguments);
    };
    return descriptor;
}

class Math {
    // 装饰方法
    @log
    add(a,b) {
        return a + b;
    }
}

const math = new Math();
const result = math.add(2,4); // 执行 add 时,会自动打印日志,因为有 @log 装饰器
console.log('result',result);

补充:

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

obj

要在其上定义属性的对象。

prop

要定义或修改的属性的名称。

descriptor

将被定义或修改的属性描述符。

返回值:

被传递给函数的对象。

descriptor:属性描述符。

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。

数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。

存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

1.数据描述符和存取描述符均具有以下可选键值:

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。

enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

2.数据描述符同时具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。

2.存取描述符同时具有以下可选键值:

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。

默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
默认为 undefined。

(2)core-decorators

第三方开源lib

提供常用的装饰器

查阅文档:github.com/jayphelps/core-decorators

// 首先安装:npm i core-decorators --save

// 开始编码:
import { readonly } form 'core-decorators'
class Person {
    @readonly
    name() {
        return 'zzzzz'
    }
}

let p = new Person()
alert(p.name())
// p.name = function() {} // 此处会报错
// 老版本的库将要废弃不用了时使用,可以告诉用户这些API将不用了
import { deprecate } from 'core-decorators';

class Person {
    @deprecate
    facepalm() {}

    @deprecate('We stopped facepalming')
    facepalmHard() {}

    @deprecate('We stopped facepalming',{ url: 'http://knowyourmeme.com/memes/facepalm' })
    facepalmHarder () {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This fucntion will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
// See http://knowyourmeme.com/memes/facepalm from more details.

补充知识1:什么是Decorator

现在什么 AOP 编程在前端领域越来越被大家追捧,所以我也来探究一下如何在javascript中进行AOP编程。 装饰器无疑是对AOP最有力的设计,在es5 时代,可以通过 Object.defineProperty 来对对象属性/方法 进行访问修饰,但用起来需要写一堆东西。现在decorator已经在ES7的提案中了,借助Babel等转码工具,我们现在也能在javascript中使用装饰器语法了!

decorator 也叫装饰器(装潢器)。它可以在不侵入到原有代码内部的情况下而通过标注的方式修改类代码行为,装饰器对代码行为的改变是在编译阶段完成的,而不是在执行阶段。虽然Decorator还处在ES7草案阶段,但是我们可以通过Babel来转换es7代码,所以大家还是可以愉快的使用decorator。

补充知识2:什么是面向切面编程AOP?

这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。 但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。 也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

4)设计原则验证:

将现有对象和装饰器进行分离,两者独立存在
符合开放封闭原则

10.代理模式

1)介绍

使用者无权访问目标对象

中间加代理,通过代理做授权和控制

2)演示

网络代理,科学上网,访问github.com

明星经纪人

代码演示:

// 真实类
class ReadImg {
    constructor(fileName) {
        this.fileName = fileName
        // 初始化,即从硬盘中加载,模拟
        this.loadFromDisk()
    }

    display() {
        console.log('display...' + this.fileName)
    }

    loadFromDisk() {
        console.log('loading...' + this.fileName)
    }
}

// 代理类
class ProxyImg {
    constructor(fileName) {
        this.realImg = new ReadImg(fileName)
    }
    display() {
        this.realImg.display()
    }
}

// 测试
let proxyImg = new ProxyImg('1.png')
proxyImg.display()

3)场景

(1)网页事件代理
    <div id="div1">
        <a href="#">a1</a>
        <a href="#">a2</a>
        <a href="#">a3</a>
        <a href="#">a4</a>
    </div>
    <button>点击增加一个 a 标签</button>
    <script>
        var div1 = document.getElementById('div')
        div1.addEventListener('click',function(e){
            var target = e.target
            if (e.nodeName === 'A') {
                alert(target.innerHTML)
            }
        })
    </script>
(2)jQuery $.proxy
$('#div1').click(function(){
    // this 符合期望
    $(this).addClass('red')
})

$('#div1').click(function(){
    setTimeout(function(){
        // this 不符合期望,this指向window
        $(this).addClass('red')
    },1000);
});
// 可以用如下方式解决
$('#div1').click(function (){
    var _this = this
    setTimeout(function(){
        // _this 符合期望
        $(_this).addClass('red')
    },1000);
});

这样写就不用存this:

$('#div1').click(function(){
    var fn = function () {
        $(this).css('background-color','yellow')
    }
    setTimeout(fn,1000)
})

再进一步:

$('#div1').click(function(){
    var fn = function () {
        $(this).css('background-color','yellow')
    }
    fn = $.proxy(fn,this)
    setTimeout(fn,1000)
})

也即:

// 但推荐使用$.proxy 解决,这样就少定义了一个变量
$('#div1').click(function(){
    setTimeout($.proxy(function () {
        // this 符合期望
        $(this).addClass('red')
    },this),1000);
});
(3)ES6 Proxy
// 明星
let star = {
    name: '张XX',
    age: 25,
    phone: '13910733521'
}

// 经纪人
let agent = new Proxy(star, {
    get: function (target, key) {
        if (key === 'phone') {
            // 返回经纪人自己的手机号
            return '18611112222'
        }
        if (key === 'price') {
            // 明星不报价,经纪人报价
            return 120000
        }
        return target[key]
    },
    set: function (target, key, val) {
        if (key === 'customPrice') {
            if (val < 100000) {
                // 最低 10w
                throw new Error('价格太低')
            } else {
                target[key] = val
                return true
            }
        }
    }
})

// 主办方
console.log(agent.name)
console.log(agent.age)
console.log(agent.phone)
console.log(agent.price)

// 想自己提供报价(砍价,或者高价争抢)
agent.customPrice = 150000
// agent.customPrice = 90000  // 报错:价格太低
console.log('customPrice', agent.customPrice)

4)设计原则验证

代理类和目标类分离,隔离开目标类和使用者

符合开放封闭原则

5)代理模式 VS 适配器模式

适配器模式:提供一个不同的接口(如不同版本的插头)
代理模式:提供一模一样的接口

5)代理模式 VS 装饰器模式

装饰器模式:扩展,原有功能不变且可直接使用
代理模式:显示原有功能,但是经过限制或者阉割之后的

11.外观模式

1)介绍

为子系统中的一组接口提供了一个统一的高层接口
使用者使用这个高层接口

2)场景

function bindEvent(elem,type,selector,fn){
    // 通过高层接口集成,不管是3个参数还是4个参数,都集成在一个接口
    if (fn == null) {
        fn = selector
        selector = null
    }

    // ******
}

// 调用
// 有代理,接受4个参数
bindEvent(elem,'click','#div1',fn)
// 无代理,接受3个参数
bindEvent(elem,'click',fn)

3)设计原则验证:

不符合单一职责原则和开放封闭原则,因此谨慎使用,不可滥用。

12.观察者模式

1)介绍

发布 & 订阅

一对多

2)演示

点咖啡,点好之后坐等被叫

// 主题,保存状态,接收状态变化,变化后触发每个观察者
class Subject {
    constructor() {
        this.state = 0
        // 所有观察者放在一个数组中
        this.observers = []
    }
    // 获取状态
    getState() {
        return this.state
    }
    // 设置状态
    setState(state) {
        this.state = state
        // 触发遍历更新
        this.notifyAllObservers()
    }
    // 添加一个新的observer进来到数组中
    attach(observer) {
        this.observers.push(observer)
    }

    notifyAllObservers() {
        // 遍历所有观察者
        this.observers.forEach(observer => {
            // 每个observer执行自己的update方法
            observer.update()
        })
    }
}

// 观察者,等待被触发
class Observer {
    constructor(name, subject) {
        this.name = name
        // Subject的实例
        this.subject = subject
        // 把传入的观察者添加到obervers数组中
        this.subject.attach(this)
    }
    update() {
        // 获取当前实例的状态
        console.log(`${this.name} update, state: ${this.subject.getState()}`)
    }
}

// 测试代码
// 初始化一个主题
let s = new Subject()

// 初始化3个观察者
let o1 = new Observer('o1', s)
// o1 update,state: 1
// o2 update,state: 1
// o3 update,state: 1

let o2 = new Observer('o2', s)
// o1 update,state: 2
// o2 update,state: 2
// o3 update,state: 2

let o3 = new Observer('o3', s)
// o1 update,state: 3
// o2 update,state: 3
// o3 update,state: 3

s.setState(1)
s.setState(2)
s.setState(3)

3)场景

(1)网页事件绑定
<button id="btn1">btn</button>

<script>
    // 按钮的点击也是一种状态的变化,变化之后触发所有的观察者
    $('#btn1').click(function(){
        console.log(1)
    })
    $('#btn1').click(function(){
        console.log(2)
    })
    $('#btn1').click(function(){
        console.log(3)
    })
</script>
(2)Promise
// 之前看过,如何生成并返回promise
function loadImg(src) {
    var promise = new Promise(function (resolve,reject){
        var img = document.createElement('img')
        img.onload = function () {
            resolve(img)
        }
        img.onerror = function(){
            reject('图片加载失败')
        }
        img.src = src
    })
    return promise
}

var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
// 绑定两个.then()函数,等到前一个promise状态变化去执行后一个then函数
result.then(function(img){
    console.log('width',img.width)
    return img
}).then(function(img){
    console.log('height',img.height)
})
(3)jquery callbacks
var callbacks = $.Callbacks() //注意大小写
callbacks.add(function (info){
    console.log('fn1',info)
})
callbacks.add(function (info){
    console.log('fn2',info)
})
callbacks.add(function (info){
    console.log('fn3',info)
})
callbacks.fire('gogogo')
// fn1 gogogo
// fn2 gogogo
// fn3 gogogo

callbacks.fire('fire')
// fn1 fire
// fn2 fire
// fn3 fire
(4)nodejs 自定义事件
const EnentEmitter = require('events').EventEmitter
const emitter1 = new EventEmitter()
emitter1.on('some',() => {
    // 监听 some 事件
    console.log('some event is occured 1')
})
emitter1.on('some',() => {
    // 监听 some 事件
    console.log('some event is occured 2')
})
// 触发 some 事件
emitter1.emit('some')
const EventEmitter = require('events').EventEmitter
const emitter = new EventEmitter()
emitter.on('sbowName',name => {
    console.log('event occured',name)
})
emitter.emit('sbowName','zhangsan') // emit 时候可以传递参数过去
const EventEmitter = require('events').EventEmitter

// 任何构造函数都可以继承 EventEmitter 的方法 on emit
class Dog extends EventEmitter {
    constructor(name) {
        super()
        this.name = name
    }
}
// 声明一个实例
var simon = new Dog('simon')
// 这个实例就拥有on()方法了
simon.on('bark',function(){
    console.log(this.name,'barked')
})
setInterval(() => {
    // 通过emit()触发这个自定义事件
    simon.emit('bark')
},500)
// Stream 用到了自定义事件
var fs = require('fs')
// 读取文件的Stream
var readStream = fs.createReadStream('./data/file1.txt')

var length = 0
// 监听一段段数据,不一定是整行的
readStream.on('data',function(chunk){
    let len = chunk.toString().length
    console.log('len',len)
    length += len
})
// 监听流结束
readStream.on('end',function(){
    console.log(length)
})
// readline 用到了自定义事件
var readline = require('readline')
var fs = require('fs')

// readline.createInterface(options)
// options <Object>
// input <stream.Readable> 要监听的可读流。该选项是必需的。
// output <stream.Writable> 要写入逐行读取数据的可写流。
// completer <Function> 一个可选的函数,用于 Tab 自动补全。
// terminal <boolean> 如果 input 和 output 应被当作一个 TTY,且要写入 ANSI/VT100 转换的代码,则设为 true。 默认为实例化时在 output 流上检查 isTTY。
// historySize <number> 保留的历史行数的最大数量。 设为 0 可禁用历史记录。 该选项只有当 terminal 被用户或内部 output 设为 true 时才有意义,否则历史缓存机制不会被初始化。 默认为 30。
// prompt - 要使用的提示字符串。默认为 '> '。
// crlfDelay <number> 如果 \r 与 \n 之间的延迟超过 crlfDelay 毫秒,则 \r 和 \n 都会被当作换行分隔符。 crlfDelay 强制设置为不少于 100. 可以设置为 Infinity, 这种情况下, \r 跟着 \n 会被视为单个新行(也许对带有\r\n分隔符的[reading files][]来说是非常合理的)。 默认为 100 毫秒。
// removeHistoryDuplicates <boolean> 如果为 true, 当新输入行与历史列表中的某行相同时, 那么移除旧有的行。 默认为 false。

// readline.Interface类的实例都是使用readline.createInterface()方法构造的。
// 每个实例都关联着一个input可读流和一个output可写流。output流用于用户输入打印显示,output流数据从input流中读取。
var rl = readline.createInterface({
    input: fs.createReadStream('./data/file1.txt')
});

var lineNum = 0
// 监听一行行的数据
rl.on('line',function(line){
    lineNum++
});
// 监听每行结束
rl.on('close',function(){
    console.log('lineNum',lineNum)
})
    

4)其他场景

(1)nodejs中:处理http请求
function serverCallback(req,res){
    var method = req.method.toLowerCase() // 获取请求的方法
    if (method === 'get'){
        // 省略3行,上行代码示例中处理GET请求的代码
    }
    if (method === 'post'){
        // 接收post请求的内容
        var data = ''
        req.on('data',function(chunk){
            // “一点一点”接收内容
            data += chunk.toString()
        })
        req.on('end',function(){
            // 接收完毕,将内容输出
            res.writeHead(200,{'Content-type':'text/html'})
            res.write(data)
            res.end()
        })
    }
}
(2)多进程通讯
// parent.js 父进程
var cp = require('child_process')
// fork用于产生一个Node.js的子进程
var n = cp.fork('./sub.js')
//每个请求都单独生成一个新的子进程,on message来监听信息,send来发布信息。从而实现进程通信。对于子进程亦是如此。
n.on('message',function(m){
    console.log('PARENT got message: ' + m)
})
n.send({hello: 'world'})

// sub.js 子进程
//接受到send传递过来的参数
process.on('message',function(m){
    console.log('CHILD got message: ' + m)
})
process.send({foo:'bar'})
(3)vue和React组件生命周期触发
// 生成组件就相当于构造函数初始化一个实例
class Login extends React.Component {
    constructor(props,context){
        super(props,context);
        this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
        this.state = {
            checking: true
        }
    }
    // 不同生命周期阶段的回调,也类似于事件发布订阅
    render() {
        return (
            <div>
                <Header title="登录" history={this.props.history}/>
            </div>
        )
    }
    componentDidMount() {
        // 判断是否已经登录
        this.doCheck()
    }
}
(4)vue watch
var vm = new Vue({
    el:'#demo',
    data:{
        firstName:'Foo',
        lastName:'Bar',
        fullName:'Foo Bar'
    },
    // 一旦firstName和lastName改变了,就会触发两个观察者
    watch: {
        firstName: function(val) {
            this.fullName = val + ' ' + this.lastName
        },
        lastName: function (val) {
            this.fullName = this.firstName + ' ' + val
        }
    }
})

5)设计原则验证

主题和观察者分离,不是主动触发而是被动监听,两者解耦

符合开放封闭原则

13.迭代器模式

1)介绍

顺序访问一个集合

使用者无需知道集合的内部结构(封装)

2)演示

(1)jQuery里each的迭代器的使用
<p>jquery each</p>
<p>jquery each</p>
<p>jquery each</p>

<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var arr = [1,2,3]
    var nodeList = document.getElementsByTagName('p')
    var $p = $('p')

    // 要对这3个不同数据类型的变量进行遍历,需要写3个遍历方法

    // 第一
    arr.forEach(function(item){
        console.log(item)
    })
    // 第二
    var i,length = nodeList.length
    for(i = 0;i < length; i++){
        console.log(nodeList[i])
    }
    // 第三
    // jquery已封装好了each(key,elem)方法
    $p.each(function(key,p){
        console.log(key,p)
    })

    // 统一的遍历方法
    // 通过迭代器模式,我们就可以写一个尽量简洁的函数,将3种数据结构都遍历出来
    function each(data){
        //生成迭代器
        var $data = $(data) 

        $data.each(function(key,val){
            console.log(key,val)
        })
    }

    each(arr);
    each(nodeList);
    each($a);

</script>
(2)封装迭代器模式
class Iterator {
    constructor(container) {
        this.list = container.list
        this.index = 0
    }

    next() {
        if (this.hasNext()) {
            return this.list[this.index++]
        }
        return null
    }
    // 是否有下一项
    hasNext() {
        if (this.index >= this.list.length) {
            return false
        }
        return true
    }
}

class Container {
    constructor(list) {
        this.list = list
    }
    // 生成遍历器
    getIterator() {
        return new Iterator(this)
    }
}

// 测试代码
var arr = [1,2,3,4,5,6]
let container = new Container(arr);
// 使用 getIterator() 生成遍历器
let iterator = container.getIterator()
// 通过遍历器来遍历
while(iterator.hasNext()){
    console.log(iterator.next())
}

3)场景

(1)jQuery each

见上

(2)ES6 Iterator
ES6 Iterator为何存在?

ES语法中,有序集合的数据类型已经有很多:
Array Map Set String TypedArray arguments NodeList

需要有一个统一的遍历接口来遍历所有数据类型
(注意,object不是有序集合,可以用Map代替)

ES6 Iterator 是什么?

以上数据类型,都有[Symbol.iterator]属性

属性值是函数,执行函数返回一个迭代器

这个迭代器就有next方法可以顺序迭代子元素

可运行Array.prototype[Symbol.iterator]来测试

Array.prototype[Symbol.iterator]
// f values() { [native code]}
Array.prototype[Symbol.iterator]()
// Array Iterator {}
Array.prototype[Symbol.iterator().next()]
// {value: undefined, done:true}
ES6 Iterator 示例
function each(data) {
    // 生成遍历器
    let iterator = data[Symbol.iterator]()

    // console.log(iterator.next()) // 有数据时返回 { value: 1, done: false}
    // console.log(iterator.next())
    // console.log(iterator.next())
    // console.log(iterator.next())
    // console.log(iterator.next()) // 没有数据时返回  value: undefined, done: true}

    let item = {done: false}
    while (!item.done) {
        item = iterator.next()
        if (!item.done) {
            console.log(item.value)
        }
    }
}

// 测试代码
let arr = [1,2,3,4]
let nodeList = document.getElementsByTagName('p')
let m = new Map()
m.set('a',100)
m.set('a',200)

each(arr)
each(nodeList)
each(m)
// 上面的代码可以简写
// `Symbol.iterator`并不是人人都知道
// 也不是每个人都需要一个each方法
// 因此有了`for...of`语法
function each(data) {
    // 必须带有遍历器特性的对象:data[Symbol.iterator]有值
    for (let item of data){
        console.log(item)
    }
}

each(arr)
each(nodeList)
each(m)
ES6 Iterator 与 Generator

Iterator的价值不限于上述几个类型的遍历

还有 Generator 函数的使用

即只要返回的数据符合 Iterator 接口的要求

即可使用 Iterator 语法,这就是迭代器模式

function* helloWorldGenerator() {
    yield 'hello'
    yield 'world'
    yield 'ending'
}
var hw = helloWorldGenerator();
hw[Symbol.iterator]
// f [Symbol.iterator]() { [native code]}
// 可以看到,Generator 函数返回的结果,也实现了 Iterator 接口
function* helloWorldGenerator() {
    yield 'hello'
    yield 'world'
    yield 'ending'
}

var hw = helloWorldGenerator();
hw.next()
hw.next()
hw.next()
hw.next()
function* foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

// 当然,也可以用`for...of`
for (let v of foo()) {
    console.log(v);
}

4)设计原则验证

迭代器对象和目标对象分离

迭代器将使用者和目标对象隔离开

符合开放封闭原则

14.状态模式

1)介绍

一个对象有状态变化

每次状态变化都会触发一个逻辑

不能总是用if…else来控制

2)演示

交通信号灯不同颜色的变化

// 状态(红灯、绿灯、黄灯)
class State {
    constructor(color) {
        this.color = color
    }
    handle(context) {
        console.log(`turn to ${this.color} light`)
        // 设置状态
        context.setState(this)
    }
}

// 主体
class Context {
    constructor() {
        this.state = null
    }
    // 获取状态
    getState() {
        return this.state
    }
    setState(state) {
        this.state = state
    }
}

// test
let context = new Context()

let green = new State('green')
let yellow = new State('yellow')
let red = new State('red')

// 绿灯亮了
green.handle(context)  // turn to green light
console.log(context.getState) // State {color: "green"}

// 黄灯亮了
yellow.handle(context)  // turn to yellow light
console.log(context.getState) // State {color: "yellow"}

// 红灯亮了
red.handle(context)  // turn to red light
console.log(context.getState) // State {color: "red"}

3)场景

(1)有限状态机

有限个状态、以及在这些状态之间的变化
如交通信号灯
使用开源lib: javascript-state-machine
github.com/jakesgordon/javascript-state-machine

有限状态机-“收藏"和"取消”

<button id="btn1"></button>
import StateMachine from 'javascript-state-machine'
// 状态机模型
var fsm = new StateMachine({
    init: '收藏',// 初始状态,带收藏
    // 变化机制
    transitions: [
        {
            name: 'doStroe',
            from: '收藏',
            to: '取消收藏'
        },
        {
            name: 'deleteStore',
            from: '取消收藏',
            to: '收藏'
        }
    ],
    methods: {
        // 执行收藏
        onDoStore: function() {
            alert('收藏成功') // 可以post请求
            updateText()
        },
        // 取消收藏
        onDeleteStore: function() {
            alert('已取消收藏') // 可以post请求
            updateText()
        }
    }
})


// 业务逻辑

var $btn = $('#btn') // let btn = document.getElementById('btn')

// 点击事件
$btn.click(function(){
    if(fsm.is('收藏')) {
        // 与transitions里的name值对应
        fsm.doStore()
    } else {
        // 与transitions里的name值对应
        fsm.deleteStore()
    }
})

// 更新文案
function updateText() {
    $btn.text(fsm.state)
}

// 初始化文案
updateText()
(2)Promise 语法
// 之前看过,如何生成并返回promise
function loadImg(src) {
    var promise = new Promise(function (resolve,reject){
        var img = document.createElement('img')
        img.onload = function () {
            resolve(img)
        }
        img.onerror = function(){
            reject('图片加载失败')
        }
        img.src = src
    })
    return promise
}

var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
// 绑定两个.then()函数,等到前一个promise状态变化去执行后一个then函数
result.then(function(img){
    console.log('width',img.width)
    return img
}).then(function(img){
    console.log('height',img.height)
})

写一个简单的promise

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值