Giter Site home page Giter Site logo

yanlele / node-index Goto Github PK

View Code? Open in Web Editor NEW
361.0 361.0 58.0 30.53 MB

学习笔记、博文、简书、工作日常踩坑记录以及一些独立作品的汇总目录

Home Page: https://yanlele.github.io/node-index

JavaScript 0.29% CSS 0.69% TypeScript 82.64% HTML 7.59% Shell 0.61% Dockerfile 0.18% Python 4.32% Makefile 1.11% Batchfile 1.13% Mako 0.06% C 0.01% Java 0.10% Less 0.57% Dart 0.69%

node-index's Introduction

Hey 👋🏽, I'm Yanlele

About me

  • Web Development Engineer
  • Currently working with JavaScript and TypeScript
  • Continuous learning: Desktop, mobile, fronted, backend, devOps, games, designer

Blogs 🌱


Languages and Tools...

Here are some technologies I use at work

Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge


Other Languages I know

Badge Badge Badge


My contributions to open-source:

node-index's People

Contributors

yanlele avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

node-index's Issues

let和const

let和const

1. 不存在变量提升

必须先定义后使用,否则报错

2. 暂时性死区

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。
作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

3. 不允许重复申明/不允许在函数内部重新申明参数(也算重复申明)

// 报错
    let a = 10;
    let a = 1;

// 报错
    let a = 10;
    let a = 1;

function func(arg) {
    let arg; // 报错
}

function func(arg) {
    {
        let arg; // 不报错
    }
}

4. 块级作用域

4.1 SE5的作用域
SE5只有全局作用域和函数作用域,这样做的缺点如下:
1)、内层变量覆盖外层的变量

var tmp = new Date();

function f() {
    console.log(tmp);
    if (false) {
        var tmp = 'hello world';
    }
}
f(); // undefined

原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

2)、用来计数的循环变量会泄露为全局变量

var s = 'hello';

for (var i = 0; i < s.length; i++) {
    console.log(s[i]);
}
console.log(i); // 5

原因是上面用来控制循环的变量i ,在循环之后并没有消失,而是泄露成为了全局变量

4.2、ES6的作用域
SE6的块级作用域的解决方案 : let

function f1(){
    let n=5;
    if(true){
        let n=10;
        console.log(`if 内部块的变量n:${n}`)//结果为10
    }
    console.log(`外部快的变量 n :${n}`)//结果为5
}

4.3、const
4.3.1、const是一个常量,一旦申明,就不能改变。而且在申明的时候必须初始化,不能留到后面赋值。
4.3.2、作用域和let是一样的
const常量储存的是一个地址,这个地址是指向一个对象的,因为对象本身是可变的,所以依然可以为其添加新的属性和方法:

const arr=[];
arr.push('hello');
console.log(arr);		//可执行
console.log(arr.length);	//可执行
arr=['word!'];			//报错

如果想冻结这个对象的话,要使用Object.freeze()方法:

'use strict'
const foo=Object.freeze({});
foo.prop=123;			//报错

彻底冻结一个对象的方式:上面只冻结了对象,要彻底冻结一个函数,就要冻结对象和属性

var constantize=(obj)=>{
    Object.freeze(obj);
    Object.keys(obj).forEach((key,value)=>{
        if(typeof obj[key]==='object'){
            constantize(obj[key])
        }
    })
};
const obj=constantize([]);
obj.push(123);			//报错

4.4、全局对象属性
在ES5里面,为申明的全局变量会自动生为window的属性:没法在编译过程爆出变量为申明的错误,语法上顶层对象有一个实体含义的对象这样肯定不合适。

a=1;
window.a;//结果为1

ES6的改进:
用var定义的依然会升级为顶层对象(全局对象)window的属性;但是let,const申明则不会

var a=1;
window.a;//结果为1

let b=2;
window.b;//结果为undefined

5、补充

下面的代码如果使用var,最后输出的是10。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。
每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。
也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。
你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,
从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

[设计模式] 03-结构型设计模式

第三篇、结构型设计模式

第九章-外观模式

外观模式(Facade): 为一组复杂的子系统提供一个更高级的统一接口,通过这个接口是的对子系统接口的访问更加容易。

一个点击事件的例子

在大多数的事件绑定中,我们要用 DOM2 级事件处理方法addEventListener来实现,但是在IE9一下的浏览器不支持,要用attachEvent,
如果是不支持DOM2事件处理的低版本浏览器,要用onclick绑定事件。

兼容方式

我们可以通过像点套餐一样定义一个统一的接口方法,然后提供一个更加简单的高级接口,简化底层接口不统一的使用需求。

    function addEvent(dom , type, fn) {
        if(dom.addEventListener) {
            dom.addEventListener(type, fn ,false);
        } else if(dom.attachEvent) {
            dom.attachEvent('on' + type, fn);
        } else {
            dom['on' + type] = fn;
        }
    }
    let myButton = document.getElementById('myButton');
    addEvent(myButton, 'click', function() {
        console.log('click my button')
    })

这样我们就不用在考虑任何浏览器兼容性的问题了,就可以放心绑定事件和处理事件。

其他的兼容处理

在低版本IE下面,不兼容e.preventDefault() 和 e.target 。也可以通过外观模式来解决

let getEvent = function(event) {
    return event || window.event;
};
let getTarget = function(event) {
    let myEvent = getEvent(event);
    return myEvent.target || myEvent.srcElement;
};
//阻止默认行为
let preventDefault = function (event) {
    let myEvent = getEvent(event);
    if(myEvent.preventDefault) {
        myEvent.preventDefault();
    }else {
        myEvent.returnValue = false;
    }
};

小型代码库

很多代码库通过外观设计模式来封装多个功能,简化底层操作方法。

let Le = {
    g(id) {
        return document.getElementById(id)
    },
    css(id, key, value) {
        this.g(id).style[key] = value;
    },
    attr(id, key, value) {
        this.g(id)[key] = value;
    },
    html(id, value) {
        this.g(id).innerHeight = value;
    },
    on(id, type, fn) {
        this.g(id)['on' + type] = fn;
    }
};

第十章-适配器模式

适配器模式(Adapter): 将一个类(对象)的接口(方法或者属性)转化为另外一个接口, 使类(对象)之间接口的不兼容问题通过适配器得以解决。
实质上就是为两个代码库缩写的代码兼容运行而书写的额外代码。

jquery适配器

如果有一个A框架,跟jquery特别类似,如果我们希望A框架要兼容jquery,那么很简单的做法就是:
window.A = A = jquery
这样A框架里面,有jquery的所有方法了,如果A中有的方法; jquery中也有,那么jquery会直接覆盖;如果A有的方法,jquery没有,那么直接沿用A的方法。

适配异类框架

比如我们有这样一个类库:

let A = A || {};
A.g = function (id) {
    return document.getElementById(id);
};
A.on = function (id, type, fn) {
    let dom = typeof id === 'string' ? this.g(id) : id;
    if(dom.addEventListener) {
        dom.addEventListener(type, fn ,false);
    } else if(dom.attachEvent) {
        dom.attachEvent('on' + type, fn);
    } else {
        dom['on' + type] = fn;
    }
};

//调用
A.on(window, 'load', function(){
    A.on('myButton', 'click', function () {
        // do something
    })
});

如果我们想用jquery 替换之前这个类库:

/*用jquery来兼容这个类库*/
A.g = function(id) {
    return $(id).get(0);
};
A.on = function(id, type, fn) {
    let dom = typeof id === 'string' ? $('#' + id) :  $(id);
    dom.on(type, fn);
};

由此可见,要适配异类框架,要写很多代码。

参数适配器

比如一个方法需要传递多个参数:
function doSomeThing(name, title, age, color, size, prize)()
记住这些参数顺序是很困难的,我们就直接传递一个对象。但是又不知道传递对象数据是否完整,所以我们需要给定一些默认值,通过是要配齐来适配传递的这个参数对象:

function doSomeThing(obj) {
    let _adapter = {
        name: 'yanle',
        title: 'no game no life',
        age: 26,
        color: 'prink',
        size: 100,
        prize: 50
    };
    for(let i in _adapter) {
        _adapter[i] = obj[i] || _adapter[i]
    }
    
    // do something
}

服务端数据适配器

服务端数据适配实际上就是把服务端拿下的数据,做一些适当的数据重组和检验,然后再使用。

function ajaxAdapter(data) {
    return [data['key1'], data['key2'], data['key3']]
}
$.ajax({
    url: 'xxxxxx.json',
    success: function(data) {
        if(data) {
            doSomeThing(ajaxAdapter(data));
        }
    }
})

第十一章-代理模式

略, 就是类似于相关 跨域的解决方案

第十二章-装饰者模式

装饰者模式(Decorator): 不改变原对象的基础上,通过对其进行包装扩展(添加属性或者方法)让原有对象可以满足用户的更复杂需求。

比如有这么一个需求

用户点击输入框时,如果输入框输入的内容有限制,那么在其后面显示用户输入内容的限制格式的提示文案 ---------------->>>>>>> 现在要改为:
多加一条需求,默认输入框上边显示一行文案,当用户点击输入框的时候,文案消失。
这里是以前的代码:

// 输入框元素
let telInput = document.getElementById('tel_input');
// 输入框提示文案
let telWarnText = document.getElementById('tel_warn_text');
// 点击输入框显示输入框输入格式提示文案
input.onclick = function () {
    telWarnText.style.display = 'inline-block';
};

这里是修改后的代码:

// 输入框元素
let telInput = document.getElementById('tel_input');
// 输入框输入格式提示文案
let telWarnText = document.getElementById('tel_warn_text');
// 输入框提示输入文案
let telDemoText = document.getElementById('tel_demo_text');
// 点击输入框显示输入框输入格式提示文案
input.onclick = function () {
    telWarnText.style.display = 'inline-block';
    telDemoText.style.display = 'none';
};

但是紧接着悲剧就来了,修改了电话输入框,还有姓名、地址输入框等等;

装饰已有的功能对象

原有的功能已经不满足用户的需求了,此时需要做的是对原有的功能添加,设置新的属性和方法来满足新的需求,但是有不影响原来已经有的部分。

let decorator = function (input, fn) {
    let getInput = document.getElementById(input);
    if(typeof getInput.onclick === 'function') {
        let oldClick = getInput.onclick;
        getInput.onclick = function() {
            // 原来的事件回调函数
            oldClick();
            // 新增的事件回调函数
            fn();
        }
    } else {
        getInput.onclick = fn;
    }
    // 其他事件
};

调用:

// 电话输入框功能装饰
decorator('tel_input', function() {
    document.getElementById('tel_demo_text').sytle.display = 'none'
});
// 姓名输入框装饰
decorator('name_input', function() {
    document.getElementById('name_demo_text').sytle.display = 'none'
});
// 地址输入框装饰
decorator('address_input', function() {
    document.getElementById('address_demo_text').sytle.display = 'none'
});

适配器模式是对原有的对象适配, 添加的方法和原有方法功能上大致类似。装饰者提供的方法与原来的方法有一定的区别。
适配器模式使用适配器时我们新增的方法是为了调用原来的方法。装饰者不需要了解原有的功能是什么,并且对原有的方法照样可以原封不动的使用。

第十三章-桥接模式

桥接模式(Bridge): 在系统沿着多个纬度变化的同事,又不添加其复杂度并已达到解耦。

添加事件交互的一个例子

给页面上部用户信息添加鼠标滑过的特效: 用户信息是由很多小部件组成的。用户名,鼠标滑过要改变背景颜色;但是用户等级和用户消息,只改变数字内容。这两种处理逻辑不一样。

let spans = document.getElementsByTagName('span');

// 为用户明绑定特效
spans[0].onmouseover = function() {
    this.style.color = 'red';
    this.style.backgroundColor = '#ddd'
};
spans[0].onmouseout = function () {
    this.style.color = '#333';
    this.style.backgroundColor = '#f5f5f5'
};

// 绑定等级特效
spans[1].onmouseover = function () {
    this.getElementsByTagName('strong')[0].style.color = 'red';
    this.getElementsByTagName('strong')[0].style.backgroundColor = '#ddd';
};
spans[1].onmouseout = function () {
    this.getElementsByTagName('strong')[0].style.color = '#333';
    this.getElementsByTagName('strong')[0].style.backgroundColor = '#f5f5f5';
};

抽取共同点

对于用户信息模块的每一个部分,鼠标滑过和鼠标离开的两个时间的执行函数有很大的一部分是相似的。处理每个部件中的某个元素,他们都是处理钙元素的字体和背景颜色。

//抽象
function changeColor(dom, color, bg) {
    //字体颜色
    dom.style.color = color;
    //背景颜色
    dom.style.backgroundColor = bg;
}

事件与业务逻辑之间的桥梁

我们还需要一个方法来链接事件绑定与设置样式。桥接方法,我们可以用一个匿名函数来代替,将他们耦合在一起。

function changeColor(dom, color, bg) {
    //字体颜色
    dom.style.color = color;
    //背景颜色
    dom.style.backgroundColor = bg;
}

// 耦合
let spans = document.getElementsByTagName('span');
spans[0].onmouseover = function() {
    changeColor(this, 'red', '#ddd')
};
spans[0].onmouseout = function() {
    changeColor(this, '#333', '#f5f5f5')
};

spans[1].onmouseover = function () {
    changeColor(this.getElementsByTagName('strong')[0], 'red', '#ddd');
};
spans[1].onmouseout = function () {
    changeColor(this.getElementsByTagName('strong')[0], '#333', '#f5f5f5');
};

多元化对象

对于多维的变化对象也同样是使用的。比如我们做一个游戏,游戏中有人,小精灵,小球动作单位。

/*多维变量类*/
//运动单元
function Speed(x, y) {
    this.x = x;
    this.y = y;
}
Speed.prototype.run = function () {
    console.log('运动起来')
};

// 着色单元
function Color(cl) {
    this.color = cl;
}
Color.prototype.draw = function () {
    console.log('绘制色彩')
};

// 变形单元
function Shape(sp) {
    this.shape = sp;
}
Shape.prototype.change = function () {
    console.log('改变形状')
};

// 说话单元
function Speak(wd) {
    this.word = wd
}
Speak.prototype.say = function () {
    console.log('说话')
};


/*创建一个球类,可以运动和着色*/
function Ball(x, y , c) {
    // 实现运动单元
    this.speed = new Speed(x,y);
    // 实现作色单元
    this.color = new Color(c);
}
Ball.prototype.init = function () {
    // 实现运动
    this.speed.run();
    //实现上色
    this.color.draw();
};

/*创建一个人物了,可以运动和说话*/
function Person(x,y ,f) {
    this.speed = new Speed(x,y );
    this.font = new Speak(f);
}
Person.prototype.init = function () {
    this.speed.run();
    this.font.say();
};

/*创建一个精灵类,可以运动,着色, 改变形状*/
function Sprite(x,y,c,s) {
    this.speed = new Speed(x,y);
    this.color = new Color(c);
    this.shape = new Shape(s);
}
Sprite.prototype.init = function () {
    this.speed.run();
    this.color.draw();
    this.shape.change();
};

let p = new Person(10,10, 16);
p.init();

第十四章-组合模式

组合模式(Composite): 又被成为 部分-整体模式, 将对象组合成树形结构表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

一个新闻模块的例子

有一个新闻模块,不断来的需求有: 添加文字新闻,添加直播图标的文字新闻,添加已分类的文字新闻,添加图片新闻,图片新闻文字新闻放在一行。。。。。
需求中的新闻,大致可以分为独立的几种类型,对某种类型新闻做修改时, 又不会影响其他的新闻,所以你完全可以将每一类新闻抽象成面向对象程序中的一个类。
只需要针对这类欣慰做相应的修改就可以了。如果有新的需求,对这些新闻类中挑选一些组成需要的模块。

这就有点儿类似于餐厅的套餐业务,点一个套饭,里面有米饭,有菜,有汤。。。

每一个成员都要有祖先

注意一点就是,接口的统一,JS中通过继承一个虚拟类来实现,比如让所有新闻都集成一个新闻抽象父类News:

/*每一个成员都要有祖先*/
let News = function () {
    // 子容器
    this.children = [];
    // 单签组件元素
    this.element = null;
};
News.prototype = {
    init: function () {
        throw new Error('请重写方法的具体实现')
    },
    add: function () {
        throw new Error('请重写方法的具体实现')
    },
    getElement: function () {
        throw new Error('请重写方法的具体实现')
    }
};

在具体实现其子类的时候,需要注意的是组合模式可以是一个多层次的。我们组合后的整体作为一个部分,可以继续组合。
对象的上一层是可以有子成员,但是最底层中的对象是没有子成员的。

let News = require('./01、一个新闻模块的例子');
let inheritPrototype = require('./inheritPrototype');

// 容器类构造函数
let Container = function (id, parent) {
    // 构造函数继承父类
    News.call(this);
    // 模块id
    this.id = id;
    // 模块的父容器
    this.parent = parent;
    // 构建方法
    this.init();
};
// 寄生式继承父类原型方法
inheritPrototype(Container, News);
// 构建方法
Container.prototype.init = function () {
    this.element = document.createElement('ul');
    this.element.id = this.id;
    this.element.className = 'new-container'
};

// 添加子元素
Container.prototype.add = function (child) {
    // 在子元素容器中插入子元素
    this.children.push(child);
    // 插入当前组件元素树中
    this.element.appendChild(child.getElement());
    return this;
};
// 获取子元素的方法
Container.prototype.getElement = function () {
    return this.element;
};

// 显示方法
Container.prototype.show = function () {
    this.parent.appendChild(this.element);
};

/*下一层的行成员集合类以及后面的新闻组合类实现方式与上面类似*/
// 单个成员
let Item = function (className) {
    News.call(this);
    this.className = className || '';
    this.init();
};
inheritPrototype(Item,News);
Item.prototype.init = function () {
    this.element = document.createElement('li');
    this.element.className = this.className;
};
Item.prototype.add = function (child) {
    // 在子元素容器中插入子元素
    this.children.push(child);
    // 插入当前组件元素树种
    this.element.appendChild(child.getElement());
    return this;
};
Item.prototype.getElement = function () {
    return this.element;
};

// 新闻组
let NewsGrout = function (className) {
    News.call(this);
    this.className = className || '';
    this.init();
};
inheritPrototype(NewsGrout, News);
NewsGrout.prototype.init = function () {
    this.element = document.createElement('div');
    this.element.className = this.className;
};
NewsGrout.prototype.add = function (child) {
    this.children.push(child);
    this.element.appendChild(child.getElement());
    return this;
};
NewsGrout.prototype.getElement = function () {
    return this.element;
};

寄生继承inheritPrototype 的代码如下:

// 寄生式继承
function inheritPrototype(subClass, superClass) {
    // 复制一份父类的原型副本保存在变量中
    let p = inheritObject(superClass.prototype);
    //修正因为重写子类原型导致子类的constuctor属性被修改
    p.constructor = subClass;
    // 设置子类的原型
    subClass.prototype = p;
}

// 原型式继承
function inheritObject(o) {
    // 申明一个过渡函数对象
    function F(){}

    // 过渡兑现过的原型继承父类对象
    F.prototype = o;
    // 返回过渡对象的一个示例,该示例的原型继承了父类对象
    return new F();
}

module.exports = inheritPrototype;

创建一个新闻类

上面把所有子类成员都穿件出来了。光有这些新闻容器类是不行的额,我们还需要更多的底层新闻类,但是底层新闻成员类是不能拥有子成员的,他们继承父类。
示例代码太长了,请看这里:03、新闻成员类

把新闻模块创建出来

let {Container, Item, NewsGroup} = require('./02、组合要有容器类');
let {ImageNews, IconNews, EasyNews, TypeNews} = require('./03、新闻成员类');

let news1 = new Container('news', document.body);
news1.add(
    new Item('normal').add(
        new IconNews('梅西不拿金球奖也伟大', '#', 'video')
    )
).add(
    new Item('normal').add(
        new IconNews('bilibili11111', '#', 'live')
    )
).add(
    new Item('normal').add(
        new NewsGroup('has-img').add(
            new ImageNews('img/a.jpg', '#', 'small')
        ).add(
            new EasyNews('123123123121', '#')
        ).add(
            new EasyNews('222222222222', '#')
        )
    )
).add(
    new Item('normal').add(
        new TypeNews('3333333333', '#', 'NBA', 'left')
    )
).add(
    new Item('normal').add(
        new TypeNews('444444444', '#', 'NBA', 'right')
    )
).show();

结果示例图:
14-01

第十五章-享元模式

享元模式(Flyweight): 运用共享技术有效地支持大量的细粒度对象,避免对象间拥有相同内容造成多余的开销。

翻页的需求

一个简单的分页功能,点击下一页隐藏当前页的新闻,然后显示后面五个新闻;存在的问题,在低版本浏览器中会有卡的现象:

let article = [1, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9];    // 里面存放的是新闻对象
let dom = null,                 // 缓存创建的新闻标题元素
    paper = 0,                  // 当前页数
    num = 5,                    // 每一页显示新闻数目
    i = 0,                      // 创建新闻元素时候保存变量
    len = article.length;       // 新闻数据长度
for (; i < len; i++) {
    dom = document.createElement('div');
    dom.innerHTML = article[i];
    if (i >= num) {
        dom.style.display = 'none'
    }
    document.getElementById('container').appendChild(dom)
}
// 下一页绑定事件
document.getElementById('next_page').onclick = function () {
    let div = document.getElementById('container').getElementsByTagName('div'),
        j = k = n = 0,
        n = ++paper % Math.ceil(len / num) * num;
    for (; j < len; j++) {
        div[j].style.display = 'none';
    }
    for (; k < 5; k++) {
        if (div[n + k]) {
            div[n + k].style.display = 'block'
        }
    }
};

存在的问题

上面的这种做法,实际上是把所有的新闻都插入到也页面,通过展示或者不展示来形成一个分页的效果。
所有新闻都是相同的机构,只是内容不同,如果创建几百条新闻,同事插入页面并且操作,会造成多余的开销,导致了影响性能。
享元模式主要是对数据和方法共享分离,它把数据和方分为了: 内部数据和内部方法、外部数据和外部方法。
内部数据内部方法是指相似或者相同的数据和方法,所以讲这一部分提取出来,可以减少开销,提高性能。

享元对象

在上面的例子中,新闻个体有相同的结构,作为内部数据,下一页绑定事件作为外部方法。内部的数据提取出来了,为了使用它们,需要提供一个操作方法。

let Flyweight = function () {
    let created = [];
    function create() {
        let dom = document.createElement('div');
        document.getElementById('container').appendChild(dom);
        created.push(dom);
        return dom;
    }
    return {
        getDiv: function () {
            if(created.length < 5) {
                return create()
            } else {
                // 获取第一个元素,并且插入到最后
                let div = created.shift();
                created.push(div);
                return div;
            }
        }
    }
};

需求的实现

let Flyweight = require('./02、享元对象');
let article = [1, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9];    // 里面存放的是新闻对象

let paper = 0,
    num = 5,
    len = article.length;
// 添加五条新闻
for (let i = 0; i < 5; i++) {
    if(article[i]) {
        Flyweight().getDiv().innerHTML = article[i]
    }
}

//给下一页添加一个事件
document.getElementById('next_page').onclick = function () {
    // 如果新闻内容不满足五条返回
    if(article.length < 5) return;
    let n = ++paper * num % len,                // 获取当前页的第一条新闻索引
        j = 0;
    // 插入五条新闻
    for (; j<5;j++) {
        if (article[n+j]) {
            Flyweight().getDiv().innerHTML = article[n+j];
        } else if(article[n+j-len]) {           
            Flyweight().getDiv().innerHTML = article[n+j-len];
        } else {
            Flyweight().getDiv().innerHTML = ''
        }
    } 
}

这样重构之后,每次就只需要插入五个元素了。

享元动作

在面向对象编程里面的其他用处: 比如我们可以创建人和精灵等角色,他们都会运动,而且实现方式是相同的。我们就可以创建一个通用享元类,让他们可以实现横向和纵向的运动。

let FlyWeight = {
    moveX: function (x) {
        this.x = x
    },
    moveY: function (y) {
        this.y = y
    }
};

// 让人移动
let Player = function (x, y, c) {
    this.x = x;
    this.y = y;
    this.color = c
};
Player.prototype = FlyWeight;
Player.prototype.changeC = function (c) {
    this.color = c;
};

// 让精灵移动
let Spirit = function (x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
};
Spirit.prototype = FlyWeight;
Spirit.prototype.changeR = function (r) {
    this.r = r;
};

/*使用*/
// 创建一个人
let person = new Player(5,6,'red');
console.log(person);
person.moveX(6);
person.moveY(7);
person.changeC('pink');
console.log(person);

这个例子中,人和精灵都通用一个运动方法,那么我们可以吧这个运动方法作为内部方法提取出来。实现公用。

Set和Map数据结构

Set和Map数据结构

1、Set

1.1、ES6 提供了新的数据结构 Set 。它类似于数组,但是成员的值都是唯一的,没有重复的值。

1.2、Set 实例的属性和方法

  • Set 结构的实例有以下属性。

    • Set.prototype.constructor:构造函数,默认就是Set函数。
    • Set.prototype.size:返回Set实例的成员总数。
  • Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

    • add(value):添加某个值,返回 Set 结构本身。
    • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    • has(value):返回一个布尔值,表示该值是否为Set的成员。
    • clear():清除所有成员,没有返回值。

实例1:

    s.add(1).add(2).add(2);
    //  注意 2 被加入了两次
    s.size // 2
    s.has(1) // true
    s.has(2) // true
    s.has(3) // false
    s.delete(2);
    s.has(2) // false

1.3、遍历操作

  • Set 结构的实例有四个遍历方法,可以用于遍历成员。
    • keys():返回键名的遍历器
    • values():返回键值的遍历器
    • entries():返回键值对的遍历器
    • forEach():使用回调函数遍历每个成员

(1)keys() ,values() ,entries()
key方法、value方法、entries方法返回的都是遍历器对象(详见《 Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以key方法和value方法的行为完全一致。

    let set = new Set(['red', 'green', 'blue']);
    for (let item of set.keys()) {
        console.log(item);
    }
    // red
    // green
    // blue
    for (let item of set.values()) {
        console.log(item);
    }
    // red
    // green
    // blue
    for (let item of set.entries()) {
        console.log(item);
    }
    // ["red", "red"]
    // ["green", "green"]
    // ["blue", "blue"]

( 2 )forEach()
Set 结构的实例的forEach方法,用于对每个成员执行某种操作,没有返回值。

    let set = new Set([1, 2, 3]);
    set.forEach((value, key) => console.log(value * 2) )
    // 2
    // 4
    // 6

2、WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先, WeakSet 的成员只能是对象,而不能是其他类型的值。
其次, WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,
那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。这个特点意味着,无法引用 WeakSet 的成员,因此 WeakSet 是不可遍历的。

  • WeakSet 结构有以下三个方法。
    • WeakSet.prototype.add(value) :向 WeakSet 实例添加一个新成员。
    • WeakSet.prototype.delete(value) :清除 WeakSet 实例的指定成员。
    • WeakSet.prototype.has(value) :返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

实例1:基本使用

    var ws = new WeakSet();
    var obj = {};
    var foo = {};
    
    ws.add(window);
    ws.add(obj);
    
    ws.has(window); // true
    ws.has(foo); // false
    
    ws.delete(window);
    ws.has(window); // false

3、Map

3.1、基本使用和概述

JavaScript 的对象( Object ),本质上是键值对的集合( Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题, ES6 提供了 Map 数据结构。
它类似于对象,也是键值对的集合,但是 “ 键 ” 的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
也就是说, Object 结构提供了 “ 字符串 — 值 ” 的对应, Map 结构提供了 “ 值 — 值 ” 的对应,是一种更完善的 Hash 结构实现。
如果你需要 “ 键值对 ” 的数据结构, Map 比 Object 更合适。

实例1:

    var m = new Map();
    var o = {p: 'Hello World'};
    
    m.set(o, 'content')
    m.get(o) // "content"
    
    m.has(o) // true
    m.delete(o) // true
    
    m.has(o) // false

上面代码使用set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键。

实例2:

    //实例2:作为构造函数, Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
    var map = new Map([
        ['name', ' 张三 '],
        ['title', 'Author']
    ]);
    map.size // 2
    map.has('name') // true
    map.get('name') // " 张三 "
    map.has('title') // true
    map.get('title') // "Author"

3.2、实例的属性和操作方法

size 属性:
size属性返回 Map 结构的成员总数。

set(key, value):
set方法设置key所对应的键值,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

get(key):
get方法读取key对应的键值,如果找不到key,返回undefined。

has(key):
has方法返回一个布尔值,表示某个键是否在 Map 数据结构中。

delete(key):
delete方法删除某个键,返回 true 。如果删除失败,返回 false 。

clear():
clear方法清除所有成员,没有返回值。

3.3、 遍历方法

  • Map 原生提供三个遍历器生成函数和一个遍历方法。
    • keys():返回键名的遍历器。
    • values():返回键值的遍历器。
    • entries():返回所有成员的遍历器。
    • forEach():遍历 Map 的所有成员。

实例1:遍历方法

    let map = new Map([
        ['F', 'no'],
        ['T', 'yes'],
    ]);
    
    for (let key of map.keys()) {
        console.log(key);
    }
    // "F"
    // "T"
    
    for (let value of map.values()) {
        console.log(value);
    }
    // "no"
    // "yes"
    
    for (let item of map.entries()) {
        console.log(item[0], item[1]);
    }
    // "F" "no"
    // "T" "yes"
    
    //  或者
    for (let [key, value] of map.entries()) {
        console.log(key, value);
    }
    //  等同于使用 map.entries()
    for (let [key, value] of map) {
        console.log(key, value);
    }

实例2:结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤( Map 本身没有map和filter方法)。

    //map 过滤
    let map0 = new Map()
        .set(1, 'a')
        .set(2, 'b')
        .set(3, 'c');
    let map1 = new Map(
        [...map0].filter(([k, v]) => k < 3)
    );
    //  产生 Map 结构 {1 => 'a', 2 => 'b'}
    let map2 = new Map(
        [...map0].map(([k, v]) => [k * 2, '_' + v])
    );

3.4、与其他数据结构的互相转换

    //实例1: Map 转为数组
    let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
    [...myMap]
    // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
    //实例2:将数组转入 Map 构造函数,就可以转为 Map 。
    new Map([[true, 7], [{foo: 3}, ['abc']]])
    // Map {true => 7, Object {foo: 3} => ['abc']}
    //实例3: Map 转为对象,如果所有 Map 的键都是字符串,它可以转为对象。
    function strMapToObj(strMap) {
        let obj = Object.create(null);
        for (let [k,v] of strMap) {
            obj[k] = v;
        }
        return obj;
    }
    let myMap = new Map().set('yes', true).set('no', false);
    strMapToObj(myMap)
    // { yes: true, no: false }
    //实例4:对象转为 Map
    function objToStrMap(obj) {
        let strMap = new Map();
        for (let k of Object.keys(obj)) {
            strMap.set(k, obj[k]);
        }
        return strMap;
    }
    objToStrMap({yes: true, no: false})
    // [ [ 'yes', true ], [ 'no', false ] ]
    //实例5: Map 转为 JSON  一种情况是, Map 的键名都是字符串,这时可以选择转为对象 JSON 。
    function strMapToJson(strMap) {
        return JSON.stringify(strMapToObj(strMap));
    }
    let myMap = new Map().set('yes', true).set('no', false);
    strMapToJson(myMap)
    // '{"yes":true,"no":false}'
    //实例6: Map 的键名有非字符串,这时可以选择转为数组 JSON 。
    function mapToArrayJson(map) {
        return JSON.stringify([...map]);
    }
    let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
    mapToArrayJson(myMap)
    // '[[true,7],[{"foo":3},["abc"]]]'
    //实例7、JSON 转为 Map  JSON 转为 Map ,正常情况下,所有键名都是字符串。
    function jsonToStrMap(jsonStr) {
        return objToStrMap(JSON.parse(jsonStr));
    }
    jsonToStrMap('{"yes":true,"no":false}')
    // Map {'yes' => true, 'no' => false}

4、WeakMap

WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。

Refs 转发

Refs 转发

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。
对于大多数应用中的组件来说,这通常不是必需的。
但其对某些组件,尤其是可重用的组件库是很有用的。最常见的案例如下所述。

转发 refs 到 DOM 组件

import React, { createRef, FC, forwardRef, RefForwardingComponent, useEffect } from 'react';
import CodeViewContainer from '../../../components/BaseCodeView/CodeViewContainer';

/**
 * React 组件隐藏其实现细节,包括其渲染结果。其他使用 FancyButton 的组件通常不需要获取内部的 DOM 元素 button 的 ref。
 * 这很好,因为这防止组件过度依赖其他组件的 DOM 结构。
 *
 * Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
 * 在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button:
 *
 * 这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。
 *
 * 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
 * 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>。
 * React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
 * 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
 * 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。
 * */
const RefsDemo1: FC = () => {
  const buttonRef = createRef();
  useEffect(() => {
    console.log('buttonRef.current', buttonRef.current);
  }, []);

  return (
    <CodeViewContainer codePath="Refs/RefsDemo1">
      <FancyButton ref={buttonRef} />
    </CodeViewContainer>
  );
};

interface RefForwardProps {}

interface FancyButtonProps {}

const FancyButton: RefForwardingComponent<RefForwardProps, FancyButtonProps> = forwardRef((props, ref) => {
  return <button ref={ref}>{props.children}</button>;
});

export default RefsDemo1;

子组件向父组件转发内容

import React, { FC, forwardRef, RefForwardingComponent, useEffect, useImperativeHandle, useRef } from 'react';
import CodeViewContainer from '../../../components/BaseCodeView/CodeViewContainer';

interface Props {
  parent: string;
}

export interface ChildRef {
  name: string;
  age: number;
}

const ChildComponent: RefForwardingComponent<ChildRef, Props> = forwardRef((props, ref) => {
  console.log(props);

  useImperativeHandle(ref, () => ({
    name: 'yanle',
    age: 27,
  }));

  return <div>my child component</div>;
});

const RefsDemo2: FC = () => {
  const childRef = useRef<ChildRef>(null);

  useEffect(() => {
    console.log(`<${'='.repeat(50)}${'='.repeat(50)}>`);
    console.log('childRef.current', childRef.current);
    console.log(`<${'='.repeat(50)}${'='.repeat(50)}>`);
  }, []);

  return (
    <CodeViewContainer codePath="Refs/RefsDemo2">
      <ChildComponent ref={childRef} parent="name" />
    </CodeViewContainer>
  );
};

// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default RefsDemo2;

[docker] 入门 - 02 Docker容器与镜像

Docker容器与镜像

目录

Docker核心

Docker 的核心组件包括:

  • Docker 客户端 - Client
  • Docker 服务器 - Docker daemon
  • Docker 镜像 - Image
  • Registry
  • Docker 容器 - Container

1

虚拟机的启动可以参看 本目录下的 vagrantfilesetup.sh 两个文件。启动方式参看第一篇文章

Docker镜像

什么是镜像的话,直接看这个文章: 10张图带你深入理解Docker容器和镜像

镜像的获取

方式1、Dockerfile

一个简单的 Dockerfile 示例

FROM Debian:8
LABEL maintainer="yanle <[email protected]>"
RUN apt-get update && apt-get install -y redis-server
EXPORT 6379
ENTRYPOUNT ["/usr/bin/redis-server"]

然后使用docker 打包命令: docker build -t yanlele/redis:latest .

只是一个简单的示范, 后续更多内容接着整理

方式2、pull from Registry

docker Registry 是一个类似于GitHub的一个东西, 我们可以从Registry中拉取我们想要的image,
也可以比我们自己的image 直接push 到 Registry 上去

举一个非常简单的例子, 我们从 docker Registry 中拉去一个 debian : sudo docker pull debian:8
拉去完成之后, 就可以看到本地的 docker image: sudo docker image ls

[vagrant@docker-host ~]$ sudo docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
debian              8                   7cd9fb1ee74f        10 days ago         129MB

我们所有的 docker Registry 的镜像都是放置在 DockerHub:https://hub.docker.com 上的。
所有的镜像分为官方镜像和个人镜像, 其他的以后慢慢研究。

打包自己的一个image

这里先看一个简单的一个官方demo

  • 拉取镜像: docker pull hello-world
  • 查看当前镜像: docker image ls
  • 执行拉取到的helloWorld: docker run hello-world

如何自己实现一个helloWorld

以为想实现一个纯可执行的, 不需要任何依赖的hello word , 所以考虑使用 C 语言的hello world
~/hello-world/hello.c

#include <stdio.h>

int main(){
	printf("hello docker\n");
}

然后需要安装编译工具, 先查看自己 极其上是否有编译工具: yum list|grep gcc / yum list |grep glibc
所需要的是工具是: gcc、glibc-static 如果没有这两个工具, 直接 yum 安装就可以了。

然后用gcc 编译C 语言, 让其输出一个在Linux 环境下的一个可执行文件: gcc -static hello.c -o hello
运行之后, 在当前目录多了一个可执行文件。

创建Dockerfile打包

当前目录创建Dockerfile: vim Dockerfile

FROM scratch
ADD hello /
CMD ["/hello"]

保存退出之后, 直接进行docker 打包: docker build -t yanlele/hello-world .
运行过程如下:

Sending build context to Docker daemon  860.7kB
Step 1/3 : FROM scratch
 ---> 
Step 2/3 : ADD hello /
 ---> 3cf6370e3b3d
Step 3/3 : CMD ["/hello"]
 ---> Running in 0df423cf29e7
Removing intermediate container 0df423cf29e7
 ---> e436b5b7ed18
Successfully built e436b5b7ed18
Successfully tagged yanlele/hello-world:latest

通过 docker image ls 就可以查看自己刚才的那个docker 镜像了

运行我们的 docker image: docker run yanlele/hello-world

查看docker构建分层

通过docker image可以去看docker 分层情况: docker history [docker image id]

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
e436b5b7ed18        7 minutes ago       /bin/sh -c #(nop)  CMD ["/hello"]               0B                  
3cf6370e3b3d        7 minutes ago       /bin/sh -c #(nop) ADD file:0bd91ef318c5fa6bf…   857kB  

注意一点:
在构建 Dockerfile 里面 FROM scratch 表示不base 任何镜像文件, 所以这里不算一层

Docker容器: Container

基础知识看这个文章 10张图带你深入理解Docker容器和镜像

containerimage 的关系

主要从这三方方面理解

  • container 是通过 image 创建的, 实际上就是在 image layer(只读)之上建立了的一个 container layer(可读写)
  • 可以把 containerimage 的关系 类比于 类(image)于实例(container)
  • image负责app的存储和分发 container 负责运行app

如何创建 container

最简单的方式就是直接通过: docker run [docker image name]

列出当前正在运行的容器: docker container ls
值得注意的地方: 上面运行的 hello world 容器, 无论怎么运行, 都不会出现在 docker container ls 里面,
这个是因为 hello world 容器运行之后, 就退出了, 不会常驻内存里面。

列出当前所有的容器(包括正在运行的和已经退出的): docker container ls -a

[vagrant@docker-host hello-word]$ docker container ls -a
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS                         PORTS               NAMES
819f91112ba1        yanlele/hello-world   "/hello"            12 minutes ago      Exited (13) 12 minutes ago                         elastic_mclean
2f3f72f34396        hello-world           "/hello"            About an hour ago   Exited (0) About an hour ago                       condescending_swanson

看一个比较复杂的image:CentOs

启动: docker run debian:8
之后查看容器: docker container ls -a

CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS                         PORTS               NAMES
fd8ac474c124        debian:8              "bash"              9 seconds ago       Exited (0) 8 seconds ago                           infallible_nobel
819f91112ba1        yanlele/hello-world   "/hello"            16 minutes ago      Exited (13) 16 minutes ago                         elastic_mclean
2f3f72f34396        hello-world           "/hello"            About an hour ago   Exited (0) About an hour ago                       condescending_swanson

发现一个问题, debian 也不会常驻内存

交互式运行容器

上面遗留了一个问题: debian 也不会常驻内存, 那么如何让debian常驻内存。
交互式运行镜像创建容器: docker run -it debian:8
这样我们就能直接 debian 容器里面去了

再启动一个terminal窗口, 进入之前的 虚拟机, 运行命令: docker container ls

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
9e26736a2eeb        debian:8            "bash"              56 seconds ago      Up 56 seconds                           upbeat_allen

发现我们的debian 已经常驻内存了

常用命令

命令行 作用
docker container rm [container id] 删除容器
docker ps -a docker container ls -a 的简写版本
docker rm [container id] docker container rm [container id] 的简写版本
docker rm -f [container id] 强制删除(停止容器并且删除)
docker images docker image ls 的简写版本
docker image rm [image id] 删除镜像
docker rmi [image id] 删除镜像
docker container ls -aq 列举所有容器的id
docker image ls -q 列举所有的镜像id
docker rm $(docker container ls -aq) 删除所有的容器
docker contaienr ls -f 'status=exited' 列出所有退出的容器
docker contaienr ls -f 'status=exited' -q 列出所有退出的容器的id
docker contaienr ls -f 'status=exited' -q 列出所有退出的容器的id
docker rm $(docker contaienr ls -f 'status=exited' -q) 删除所有已经退出的容器

构建自己的镜像

创建一个新的镜像的方式

  • 通过 docker container commit 就可以看到命令行语法:
    Usage: docker container commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] [flags] 可以简写为 docker commit
    这个就是通过image 创建好 container , 然后对container 做出改变之后, 再把改变后的container重新打包为image

  • docker image build 可以简写为 docker build
    Usage: docker image build [OPTIONS] PATH | URL | - [flags]
    根据一个已有的image , build 一个新的 image

举一个例子 - 通过容器创建镜像

我们交互式运行一个centos的镜像,并且对镜像做出改变: docker run it centos
然后安装一个vim: sudo yum install -y vim

安装成功之后, 我就有了一个vim , 然后退出容器之后, docker container ls -a 就可以找到我们已经退出来的容器(这个容器安装了vim)

[vagrant@docker-host ~]$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                       PORTS               NAMES
89851b821678        centos              "/bin/bash"         4 minutes ago       Exited (127) 7 seconds ago                       sad_fermi

直接运行 docker commit [contaienr name] [new image name: tag]

[vagrant@docker-host ~]$ docker commit sad_fermi yanlele/centos-vim
sha256:72d96eef3aa846ae2fa7c5e642f87802aa9dc826bc18c11941c41e464496d8d0

成功之后就会有一个新的 docker image: docker image ls

[vagrant@docker-host ~]$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED              SIZE
yanlele/centos-vim    latest              72d96eef3aa8        About a minute ago   340MB
yanlele/hello-world   latest              e436b5b7ed18        23 hours ago         857kB
debian                8                   7cd9fb1ee74f        11 days ago          129MB
centos                latest              9f38484d220f        3 weeks ago          202MB
hello-world           latest              fce289e99eb9        3 months ago         1.84kB

两个镜像的关系

yanlele/centos-vim    latest              72d96eef3aa8        About a minute ago   340MB
centos                latest              9f38484d220f        3 weeks ago          202MB

这两个镜像其实是公用了很多层的, 可以通过 docker history [image id] 查看
比如先看centos 镜像的 层:

[vagrant@docker-host ~]$ docker history 9f38484d220f
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
9f38484d220f        3 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           3 weeks ago         /bin/sh -c #(nop)  LABEL org.label-schema.sc…   0B                  
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:074f2c974463ab38c…   202MB  

再看 yanlele/centos-vim 的层:

[vagrant@docker-host ~]$ docker history 72d96eef3aa8
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
72d96eef3aa8        About an hour ago   /bin/bash                                       139MB               
9f38484d220f        3 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           3 weeks ago         /bin/sh -c #(nop)  LABEL org.label-schema.sc…   0B                  
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:074f2c974463ab38c…   202MB 

注意
非常不推荐使用这种方式, 以为使用者不知道这个image 里面有什么, 这个镜像是不安全的。

举一个例子 - 通过Dockerfile创建image

创建一个文件夹 ~/docker-centos-vim 然后进去之后, 创建Dockerfile文件

FROM centos
RUN yum install -y vim

然后运行命令行 docker build -t yanlele/centos-vim-new .
就可以执行镜像打包了

Dockerfile

FROM

FROM scratch # 制作base image
FROM centos # 使用base image
FROM ubuntu:14.04 # 使用指定版本的 image

注意
尽量使用官方的image 作为 base image

LABEL

LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="This is description"

注意
尽量要加上LABEL, 类似于 注释

RUN

RUN yum update && yum install -y vum \
    python-dev # 反斜杠
RUN apt-get update && apt-get install -y perl \
    pwgen --no-install-recommends && rm -fe \
    /var/lib/apt/lists/*  # 注意清理cache
RUN /bin/bash -c 'source $HOME/.bashrc;echo $HOME'

注意
每次运行RUM 都会生成一层, 所以可以 \ 一个run来执行多个命令行, 只生成一层

WORKDIR

设定当前工作目录

WORKDIR /root

WORKDIR /test  # 如果没有目录, 会自动创建test 目录
WORKDIR demo
RUN pwd  # 输出结果应该是 /test/demo

注意
尽量用 WORKDIR , 不要用 RUN cd! 尽量使用绝对路径

AND/COPY

ADD hello /
ADD test.tar.gz /  # 添加到根目录并解压

WORKDIR /root
ADD hello test/  # /root/test/hello

WORKDIR /root
COPY hello test/

注意
ADD 可以接压缩
大多数情况下 copy 使用优先级高于 add

ENV

ENV MYSQL_VERSION 5.6  # 设置常量
RUN apt-get install -node-hello-worldy mysql-version = "${MYSQL_VERSION}"  # 引用常量
RUN rm -rf /var/lib/apt/lists/* 

要常用ENV

几个重要的执行命令对比

RUN: 执行命令并且创建新的image layer
CMD: 设置容器启动后默认执行的命令和参数
ENTRYPOINT: 设置容器启动时运行的命令

两种命令行格式

shell:
格式

RUM apt-get install -y vim
CMD echo "hello docker"
ENTRYPOINT echo "hello docker"

参数

FROM centos
NEV name Docker
ENTRYPOINT echo "helloo $name"

exec
格式

RUN ["apt-get", "install",  "-y", "vim"]
CMD ["/bin/echo", "hello docker"]
ENTRYPOINT ["/bin/echo", "hello docker"]

参数

FROM centos
ENV name Docker
ENTRYPOINT ["/bin/echo", "hello $name"]

问题来了, 我们发现按照这个方式 是没有办法输出 hello Docker 的

FROM centos
ENV name Docker
ENTRYPOINT ["/bin/echo", "hello $name"]

输出结果: hello $name

修改方式1:

FROM centos
ENV name Docker
ENTRYPOINT ["/bin/bash", "-c", "echo hello $name"]

CMD

  • 容器启动时默认执行的命令
  • 如果docker run 指定了其他命令, CMD 命令被忽略
  • 多个CMD命令, 只执行最后一个
FROM centos
ENV name Docker
CMD echo "hello $name"

运行:
docker run [image] 输出 hello Docker

docker run -it [image] /bin/bash 不会输出 hello Docker

ENTRYPOINT

  • 让容器以程序或者服务的方式去运行
  • 不会被忽略, 一定执行
  • 最佳实践是写一个shell脚本作为ENTRYPOINT去执行
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 27017
CMD ["mongod"]

其他

多余dockerfile 的语法和写法, 可以多多参考这个项目 https://github.com/docker-library
docker 官方文档: https://docs.docker.com/

镜像发布

https://hub.docker.com 注册账号和密码

然后回到虚拟机, 通过 docker login 登录

[vagrant@docker-host helloDocker]$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: yanlele	
Password: 
Login Succeeded

说明就的登录成功了

推送image

docker push: Usage: docker push [OPTIONS] NAME[:TAG] [flags]
例如我们要push hello-world: docker push yanlele/hello-world:latest

就可以等待推送完成了, 想拉取这个镜像, 就就可以这样: docker pull yanlele/hello-world

不推荐

推送Dockerfile

关联github -> github 项目里面放置Dockerfile -> 自动触发build
推荐

搭建私有DockerHub

在docker hub 上面找到 registry 这个这个镜像就是帮助构建私有 docker hub(没有界面) 仓库的
需要的时候在研究

来一个简单的例子

这里用一个打包一个python flask 服务为例子
app.py

from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
    return "hello docker"
if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000)

Dockerfile

FROM python:2.7
LABEL maintainer="Peng Xiao<[email protected]>"
RUN pip install flask
COPY app.py /app/
WORKDIR /app
EXPOSE 5000
CMD ["python", "app.py"]

然后运行命令行 docker build -t yanlele/flask-demo
打包完成之后 docker run -d -p 5000:5000 --name=flask-demo yanlele/flask-demo
运行容器

注意

1、如果希望宿主机器访问, 必须要加上 -p [point]:[point], 这样可以把容器ip 代理到 宿主机器

2、如果容器打包过程中除了问题, 提议动过查看临时容器ID, 进去查看问题原因, 做调试作用: docker run -it [temp id] /bin/bash

3、对于服务的话,如何才能后台运行: dcoker run -d [docker image]

容器操作

命令 说明
docker exec -it [container id] [命令] 对运行中的容器执行命令, 比如可以 执行 /bin/bashpython
docker [container] stop [container id] 停掉运行中的容器
docker [container] start [container id] 启动容器
docker run -d --name=demo [image] 给启动的容器取一个名字(不取名字, 名称随机)
`docker inspect [container id name]`

再看一个小栗子 - 打包一个node程序

server.js

var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(3001, '0.0.0.0');
console.log('Server running at http://127.0.0.1:3001/');

Dockerfile

FROM node:8.9-alpine
LABEL maintainer="yanlele [email protected]"

ADD . /app/
WORKDIR /app
EXPOSE 3001

CMD ["node", "start"]

执行打包: docker build -t yanlele/node-demo
执行docker 容器: docker run -d -p 3001:3001 --name=node-demo yanlele/node-demo

容器资源限制

docker 容器启动是要吃内存的, 如果不做限定, 就会一直吃虚拟机的内存, 没有内存了, 容器就退出了。
docker run [options] [image]

重要限定参数 含义
--memory 内存限制
--memory-swap 也是内存限制, 如果 --memory-swap不做限制, 那么内存大小跟 --memory 是一样的
--vm [int] 启动进程个数
--verbose 日志输出
--cpu-shares [int] cpu 使用权重

如果内存超过宿主, 那么久直接退出

其他补充

我们每次运行 docker 命令行的时候, 就需要用到 sudo, 那么如何取消这个 sudo

  • 添加一个docker group : sudo groupadd docker
  • 添加当前用户到group: sudo gpasswd -a vagrant docker
  • 重启docker服务: sudo service docker restart
  • 如果还是不行, 尝试重新登录虚拟机

查看cpu进程使用情况top

参考文章

[docker] 入门 - 01 docker环境搭建

docker环境搭建

目录

有远程服务器上面的虚拟机,啥都好说, 怎么安装就不多说了。
网上自行找安装文章, 看官网也行, 看这个文章也行: centos7安装docker
docker --version 可以查看docker 版本
docker version 可以查看docker 运行情况
如果都没有问题,就说明是安装好了的。

注意: 千万不要在本地尝试安装docker 作为学习使用(因为环境依赖会产生很多的没有必要的依赖包, 但是这些依赖包删除又是很麻烦的事儿。
如果都安装在虚拟机里面, 删除的时候, 直接删除我们的虚拟机, 就会方便非常多。)

如果没有云服务器

直接在本地计算机开启虚拟机就可以了, 建议使用VirtualBox+vagrant

基本安装

比如说我们要创建一个centos7 的虚拟机, 用vagrant 命令行直接: vagrant init centos/7
这个时候就直接会给我们初始化一个 Vagrantfile 的文件,这个文件就是配置一些列的 vagrant 的配置, more Vagrantfile 可以查看详情
里面有详细的文档, 但是重要的就是这两句话:

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"

表示我们要启动的是一个 centos7 的一个虚拟机

有了 Vagrantfile 之后, 我们可以直接在 Vagrantfile 的目录下面直接启动 vagrant up
这个过程首先要去找 centos7 的一个basebox 如果,本地有的话, 直接就从本地加载过来, 如果本地没有镜像, 就会直接去下载(过程很漫长)。
然后如果安装成功之后, 直接去看我们的 VirtualBox 就会发现一个正在运行的 centos7

当我们直接创建好了之后 我们可以直接运行 vagrant ssh 就可以登录到我们创建好的那个 centos7 系统了(当然要牢记我们进入的文件目录, 以为系统是放在当前指定的文件目录下面的)
当我们需要分享我们的虚拟机的时候,只用分享我们的 Vagrantfile 就可以了。

如果有多个 vagrant 虚拟机, 当登录到具体的某一台虚拟机, 就可以使用 这个命令行: vagrant ssh [vargant name]

基本使用

vagrant box基本命令:

  • vagrant box list 列出本地环境中所有的box
  • vagrant box add box-name(box-url) 添加box到本地vagrant环境
  • vagrant box update box-name 更新本地环境中指定的box
  • vagrant box remove box-name 删除本地环境中指定的box
  • vagrant box repackage box-name 重新打包本地环境中指定的box
  • https://app.vagrantup.com/boxes/search 在线查找需要的box

vagrant基本命令:

  • vagrant init [box-name] 在空文件夹初始化虚拟机
  • vagrant up 在初始化完的文件夹内启动虚拟机
  • vagrant ssh ssh登录启动的虚拟机
  • vagrant suspend 挂起启动的虚拟机
  • vagrant reload 重启虚拟机
  • vagrant halt 关闭虚拟机
  • vagrant status 查找虚拟机的运行状态
  • vagrant destroy 销毁当前虚拟机
  • vagrant global-status 查看到全局的虚拟机状态

这个过程中最重要的是 Vagrantfile, 这个文件的配置, 可以直接去官方网站看就可以了

通过vagrantFile启动虚拟机的时候自动安装docker

  config.vm.provision "shell", inline: <<-SHELL
    sudo yum remove docker  docker-common docker-selinux docker-engine
    sudo yum install -y yum-utils device-mapper-persistent-data lvm2
    sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
    sudo yum install docker-ce-17.12.0.ce
    sudo systemctl start docker
  SHELL

然后重新安装 vagrant up 就可以搞定了

参考文章:

CentOs7安装Docker

简要说一下安装步骤

首先卸载之前的依赖: sudo yum remove docker docker-common docker-selinux docker-engine
安装驱动包: sudo yum install -y yum-utils device-mapper-persistent-data lvm2
设置yum源(官方源奇慢无比): sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
设置ali-docker-ce源: sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
可以查看所有仓库中所有docker版本,并选择特定版本安装: sudo yum list docker-ce --showduplicates | sort -r
安装docker: sudo yum install docker-ce #由于repo中默认只开启stable仓库,故这里安装的是最新稳定版
或者: sudo yum install <FQPN> # 例如:sudo yum install docker-ce-17.12.0.ce
启动并加入开机启动: sudo systemctl start dockersudo systemctl enable docker
验证安装是否成功(有client和service两部分表示docker安装启动都成功了): docker version

[vagrant@localhost yum.repos.d]$ sudo docker version
Client:
 Version:	17.12.0-ce
 API version:	1.35
 Go version:	go1.9.2
 Git commit:	c97c6d6
 Built:	Wed Dec 27 20:10:14 2017
 OS/Arch:	linux/amd64

Server:
 Engine:
  Version:	17.12.0-ce
  API version:	1.35 (minimum version 1.12)
  Go version:	go1.9.2
  Git commit:	c97c6d6
  Built:	Wed Dec 27 20:12:46 2017
  OS/Arch:	linux/amd64
  Experimental:	false
[vagrant@localhost yum.repos.d]$ docker list
docker: 'list' is not a docker command.
See 'docker --help'

可以通过 ps -ef | grep docker 查看docker 当前的进程

官方有个demo hello-world 我们可以下载下来看一看: sudo docker run hello-world
问题来了, 本地是没有 hello-world , 是通过 docker hub 拉去的。

在这过程中可能还需要解决几个问题

参考文章:

前端日志埋点 SDK 设计思路

关键词:前端埋点监控、埋点 SDK 设计

前端日志埋点 SDK 设计思路

既然涉及到了日志和埋点,分析一下需求是啥:

  • 自动化上报 页面 PV、UV。 如果能自动化上报页面性能, 用户点击路径行为,就更好了。
  • 自动上报页面异常。
  • 发送埋点信息的时候, 不影响性能, 不阻碍页面主流程加载和请求发送。
  • 能够自定义日志发送, 日志 scope、key、value。

SDK 设计

sdk 的设计主要围绕以下几个话题来进行:

  • SDK 初始化
  • 数据发送
  • 自定义错误上报
  • 初始化错误监控
  • 自定义日志上报

最基本使用

import StatisticSDK from 'StatisticSDK';
// 全局初始化一次
window.insSDK = new StatisticSDK('uuid-12345');


<button onClick={() => {
  window.insSDK.event('click', 'confirm');
...// 其他业务代码
}}>确认</button>

数据发送

数据发送是一个最基础的api,后面的功能都要基于此进行。这里介绍使用 navigator.sendBeacon 来发送请求;具体原因如下

使用 navigator.sendBeacon() 方法有以下优势:

  1. 异步操作:navigator.sendBeacon() 方法会在后台异步地发送数据,不会阻塞页面的其他操作。这意味着即使页面正在卸载或关闭,该方法也可以继续发送数据,确保数据的可靠性。

  2. 高可靠性:navigator.sendBeacon() 方法会尽可能地保证数据的传输成功。它使用浏览器内部机制进行发送,具有更高的可靠性和稳定性。即使在网络连接不稳定或断开的情况下,该方法也会尝试发送数据,确保数据的完整性。

  3. 自动化处理:navigator.sendBeacon() 方法会自动处理数据的发送细节,无需手动设置请求头、响应处理等。它会将数据封装成 POST 请求,并自动设置请求头和数据编码,使开发者能够更专注于业务逻辑的处理。

  4. 跨域支持:navigator.sendBeacon() 方法支持跨域发送数据。在一些情况下,例如使用第三方统计服务等,可能需要将数据发送到其他域名下的服务器,此时使用 navigator.sendBeacon()
    方法可以避免跨域问题。

需要注意的是,navigator.sendBeacon() 方法发送的数据是以 POST 请求的形式发送到服务器,通常会将数据以表单数据或 JSON 格式进行封装。因此,后端服务器需要正确处理这些数据,并进行相应的解析和处理。

简单介绍一下 navigator.sendBeacon 用法

语法:

navigator.sendBeacon(url);
navigator.sendBeacon(url, data);

参数

  • url

    • url 参数表明 data 将要被发送到的网络地址。
  • data 可选

    • data 参数是将要发送的 ArrayBuffer、ArrayBufferView、Blob、DOMString、FormData 或 URLSearchParams 类型的数据。

发送代码实现如下

class StatisticSDK {
  constructor(productID, baseURL) {
    this.productID = productID;
    this.baseURL = baseURL;
  }

  send(query = {}) {
    query.productID = this.productID;

    let data = new URLSearchParams();
    for (const [key, value] of Object.entries(query)) {
      data.append(key, value);
    }
    navigator.sendBeacon(this.baseURL, data);
  }
}

用户行为与日志上报

用户行为主要涉及到的是事件上报和 pv 曝光, 借助 send 实现即可。

class StatisticSDK {
  constructor(productID, baseURL) {
    this.productID = productID;
    this.baseURL = baseURL;
  }

  send(query = {}) {
    query.productID = this.productID;

    let data = new URLSearchParams();
    for (const [key, value] of Object.entries(query)) {
      data.append(key, value);
    }
    navigator.sendBeacon(this.baseURL, data);
  }

  event(key, value = {}) {
    this.send({ event: key, ...value })
  }

  pv() {
    this.event('pv')
  }
}

性能上报

性能主要涉及的 api 为 performance.timing 里面的时间内容;

class StatisticSDK {
  constructor(productID, baseURL) {
    this.productID = productID;
    this.baseURL = baseURL;
  }

  send(query = {}) {
    query.productID = this.productID;

    let data = new URLSearchParams();
    for (const [key, value] of Object.entries(query)) {
      data.append(key, value);
    }
    navigator.sendBeacon(this.baseURL, data);
  }

  // ....
  initPerformance() {
    this.send({ event: 'performance', ...performance.timing })
  }
}

错误上报

错误上报分两类:

一个是 dom 操作错误与 JS 错误报警, 也是常说的运行时报错。 该类报错直接可以通过 addEventListener('error') 监控即可;

另一个是Promise内部抛出的错误是无法被error捕获到的,这时需要用unhandledrejection事件。

class StatisticSDK {
  constructor(productID, baseURL) {
    this.productID = productID;
    this.baseURL = baseURL;
  }

  send(query = {}) {
    query.productID = this.productID;

    let data = new URLSearchParams();
    for (const [key, value] of Object.entries(query)) {
      data.append(key, value);
    }
    navigator.sendBeacon(this.baseURL, data);
  }

  // ....
  error(err, errInfo = {}) {
    const { message, stack } = err;
    this.send({ event: 'error', message, stack, ...errInfo })
  }

  initErrorListenner() {
    window.addEventListener('error', event => {
      this.error(error);
    })
    window.addEventListener('unhandledrejection', event => {
      this.error(new Error(event.reason), { type: 'unhandledrejection' })
    })
  }
}

React 和 vue 错误边界

错误边界是希望当应用内部发生渲染错误时,不会整个页面崩溃。我们提前给它设置一个兜底组件,并且可以细化粒度,只有发生错误的部分被替换成这个「兜底组件」,不至于整个页面都不能正常工作。

React

可以使用类组件错误边界来进行处理, 涉及到的生命周期为:getDerivedStateFromErrorcomponentDidCatch

// 定义错误边界
class ErrorBoundary extends React.Component {
  state = { error: null }
  static getDerivedStateFromError(error) {
    return { error }
  }
  componentDidCatch(error, errorInfo) {
    // 调用我们实现的SDK实例
    insSDK.error(error, errorInfo)
  }
  render() {
    if (this.state.error) {
      return <h2>Something went wrong.</h2>
    }
    return this.props.children
  }
}
...
<ErrorBoundary>
  <BuggyCounter />
</ErrorBoundary>

Vue

vue也有一个类似的生命周期来做这件事:errorCaptured

Vue.component('ErrorBoundary', {
  data: () => ({ error: null }),
  errorCaptured (err, vm, info) {
    this.error = `${err.stack}\n\nfound in ${info} of component`
    // 调用我们的SDK,上报错误信息
    insSDK.error(err,info)
    return false
  },
  render (h) {
    if (this.error) {
      return h('pre', { style: { color: 'red' }}, this.error)
    }
    return this.$slots.default[0]
  }
})
...
<error-boundary>
  <buggy-counter />
</error-boundary>

参考文档

https://juejin.cn/post/7085679511290773534

[设计模式] 04-行为型设计模式

第四篇、行为型设计模式

第十六章-模板方法模式

模板方法模式(Template Method): 父类中定义一组操作算法骨架,将一些实现步骤延迟到子类中,是的子类可以不改变弗雷算法结构的同事,可以重新定义算法中实现步骤。

提示框归一化

这个例子中需要报所有的提示框弄成统一样式。
将多个模型抽象化归一,从中抽象提取出来一个最基本的模板,这个模板可以最为实体对象也可以做为抽象对象。

就好比我们要做一个蛋糕,蛋糕的模型是一样的,外面奶油涂层不一样,就做成了不同的蛋糕了。

创建基本提示框

首先要创建一个基本的提示框父类,其他的提示框只需要继承这个父类,扩展自己所需要的即可。我们以后再改动我们的鸡肋,就可以实现所有提示框样式统一变化的效果了。

let Alert = function (data) {
    if (!data) return;
    // 设置内容
    this.content = data.content;
    // 创建提示面板
    this.panel = document.createElement('div');
    // 创建提示内容组件
    this.contentNode = document.createElement('p');
    // 创建确认按钮
    this.confirmBtn = document.createElement('span');
    // 创建关闭按钮
    this.closeBtn = document.createElement('b');
    // 为提示面板添加类
    this.panel.className = 'alert';
    // 未关闭按钮添加类
    this.closeBtn.className = 'a-close';
    // 未确认按钮添加类
    this.confirmBtn.className = 'a-confirm';
    // 为确认按钮添加文案
    this.confirmBtn.innerHTML = data.confirm || '确认';
    // 为提示按钮添加文案
    this.contentNode.innerHTML = this.content;
    // 点击确认按钮执行的方法
    this.success = data.success || function(){};
    // 点击关闭按钮执行方法
    this.fail = data.fail || function(){};
};

// 模板原型方法,包含了模板的基本行为方法
Alert.prototype = {
    // 创建方法
    init: function () {
        // 生成提示框
        this.panel.appendChild(this.closeBtn);
        this.panel.appendChild(this.contentNode);
        this.panel.appendChild(this.confirmBtn);
        // 插入到页面中去
        document.body.appendChild(this.panel);
        // 绑定事件
        this.bindEven();
        // 现实提示框
        this.show()
    },
    // 绑定事件方法
    bindEvent: function () {
        let me = this;
        // 关闭按钮事件
        this.closeBtn.onclick = function () {
            // 执行关闭取消方法
            me.fail();
            // 隐藏弹出层
            me.hide();
        };
        // 点击确认事件
        this.confirmBtn.onclick = function () {
            // 执行关闭确认方法
            me.success();
            // 隐藏弹出层
            me.hide();
        }
    },
    // 隐藏弹出层方法
    hide: function () {
        this.panel.style.display = 'none';
    },
    // 现实弹出框的方法
    show: function () {
        this.panel.style.display = 'block';
    }
};

根据模板创建类

const Alert = require('./01、创建一个基础提示模板');

// 右侧按钮提示框
let RightAlert = function () {
    // 集成基本提示框构造函数
    Alert.call(this, data);
    // 确认按钮添加right类设置位置居右
    this.confirmBtn.className = this.confirmBtn.className + ' right';
};
// 继承基本提示框方法
RightAlert.prototype = new Alert();

// 标题提示框
let TitleAlert = function (data) {
    Alert.call(this);
    this.title = data.title;
    // 创建标题
    this.titleNode = document.createElement('h3');
    this.titleNode.innerHTML = this.title;
};
// 继承基本提示框方法
TitleAlert.prototype = new Alert();
// 对基本提示框方法的扩展
TitleAlert.prototype.init = function () {
    // 插入标题
    this.panel.insertBefore(this.titleNode, this.panel.firstChild);
    // 继承基本提示框的init
    Alert.prototype.init.call(this);
};

继承类也可以作为模板类

在此基础上,如果希望创建带有取消按钮的标题提示框,只需要在构造函数中创建一个取消按钮。然后原型方法实例化方法init中加入取消按钮,绑定事件就可以了。
因为上面已经创建了提示框了, 所以我们可以以上一个TitleAlert作为模板类。

// 标题提示框
let TitleAlert = function (data) {
    Alert.call(this);
    this.title = data.title;
    // 创建标题
    this.titleNode = document.createElement('h3');
    this.titleNode.innerHTML = this.title;
};
// 继承基本提示框方法
TitleAlert.prototype = new Alert();
// 对基本提示框方法的扩展
TitleAlert.prototype.init = function () {
    // 插入标题
    this.panel.insertBefore(this.titleNode, this.panel.firstChild);
    // 继承基本提示框的init
    Alert.prototype.init.call(this);
};

/*模板类还可以作为模板类,继续被继承*/
let CancelAlert = function (data) {
    TitleAlert.call(this);
    // 取消按钮文案
    this.cancel = data.cancel;
    this.cancelBtn = document.createElement('span');
    this.cancelBtn.className = 'cancel';
    this.cancelBtn.innerHTML = this.cancel || '取消'
};
CancelAlert.prototype =new Alert();
CancelAlert.prototype.init = function () {
    // 继承标题提示框创建方法
    TitleAlert.prototype.init.call(this);
    this.panel.appendChild(this.cancelBtn);
};
CancelAlert.prototype.bindEvent = function () {
    let me = this;
    TitleAlert.prototype.bindEvent.call(me);
    // 取消按钮绑定事件
    this.cancelBtn.onclick = function () {
        me.fail();
        me.hide();
    }
};

创建一个提示框

/*创建一个提示框*/
new CancelAlert({
    title: '提示框标题',
    content: '提示框内容',
    success: function () {
        console.log('ok');
    },
    fail: function () {
        console.log('cancel');
    }
}).init();

创建多类导航

这种模板设计模式在创建页面的时候也很常用。比如创建三级导航,第一类是基础,第二类多了消息提示功能,第三类在第二类的基础上多了显示网址的功能。

function formateString(str, data) {
    return str.replace(/\{(\w+)}/g, function (match, key) {
        return typeof data[key] === "undefined" ? '' : data[key];
    })
}

// 基础导航类
exports.loader = function (path) {
    if (/.css$/.test(path)) return `<link rel="stylesheet" type="text/css" href="${config.staticURI}${path}">`;
    else return `<script type="text/javascript" src="${config.staticURI}${path}"></script>`;
};
let Nav = function (data) {
    this.item = `<a href="${href}" title="${title}">${name}</a>  `;
    this.html = '';
    for (let i = 0, len = data.length; i < len; i++) {
        this.html += formateString(this.item, data[i]);
    }
    return this.html
};

// 带有消息提示的导航类
let NumNav = function (data) {
    // 创建信息模板
    let tep = `<b>${num}</b>`;
    // 装饰数据
    for (let i = data.length - 1; i >= 0; i--) {
        data[i].name += data[i].name + formateString(tep, data[i]);
    }
    return Nav.call(this, data);
};

//带有链接的导航地址
let LinkNav = function (data) {
    let tpl = `<span>${link}</span>`;
    for (let i = data.length - 1; i >= 0; i--) {
        data[i].name += data[i].name + formateString(tpl, data[i]);
    }
    return Nav.call(this, data);
};

第十七章-观察者模式

观察者模式(Observer): 又被成
为发布者-订阅者或者消息定制,定义一种依赖关系,解决主体对象与观察者之间的功能耦合;

场景是各个模块需要通信

当用户发表评论的时候,会在评论区展示新的评论,同事用户的信息模块消息数量递增。删除模块的时候,用户消息模块数量也会递减。整个功能是由三个不同开发做的。
不想跟别人的代码强耦合,又希望别的模块接收到自己的推送信息。
作为一个观察者对象,那应该具有两个功能,一个功能是接受被观察者发送过来的信息,第二个功能是想订阅者推送一个被观察者发送过过来的信息。
还需要一个注销订阅者身份的一个方法。还需要一个保存信息的容器。

创建一个订阅者

总结一下: 我们需要把观察者对象创建出来,他有消息容器,有三个方法,分别是订阅信息方法,取消订阅方法,发送订阅信息方法。

注册信息方法: 将订阅者注册的信息推送到信息队列中。接受两个参数,一个是动作类型,相应的信息。而且需要保证多个模块注册同一个信息能够顺利执行。
发布者信息方法: 当观察者发布一个信息时, 将所有订阅者订阅的信息依次执行,接受两个参数,消息类型和动作执行是传递的参数。然后遍历消息只想方法队列,并且一次执行。然后将信息类别以及传递的参数打包一次传入信息执行方法中。
注销方法: 需要两个参数,消息类型和执行的某一个动作。

// 把观察者放在闭包中,页面加载就执行
class Observer {
    // 消息容器
    constructor() {
        this.__message = {};
    }
    // 注册
    regist(type, fn) {
        // 如果消息不存在,那么创建一个消息
        if (typeof this.__message[type] === 'undefined') {
            // 动作推送到消息对应的动作执行队列中
            this.__message[type] = [fn];
        } else {    // 消息已经存在
            this.__message[type].push(fn);
        }
    }
    // 发布
    fire(type, args) {
        // 如果该消息没有被注册,就直接返回
        if (!this.__message[type]) return;
        // 定义消息消息
        let events = {
                type,                           // 消息类型
                args: args || {}                // 消息携带的数据
            },
            i = 0,                          // 消息动作循环变量
            len = this.__message[type].length;   // 消息动作长度
        for (; i < len; i++) {
            // 依次执行注册信息对应的动作序列
            this.__message[type][i].call(this, events);
        }
    }
    remove(type, fn) {
        // 如果消息队列存在
        if (this.__message[type] instanceof Array) {
            // 从最后一个动作反向遍历
            let i = this.__message[type].length - 1;
            for (; i >= 0; i--) {
                // 如果存在就移除相对应的动作
                this.__message[type][i] === fn && this.__message[type].splice(i, 1);
            }
        }
    }
}
let observer = new Observer();

// 订阅一个信息
observer.regist('test', function (e) {
    console.log(e.type, e.args.message)
});

// 发布一个信息
observer.fire('test', {
    message: '传递的参数'
});

各组件需要通信的思考

因为不同的工程师吧自己的代码卸载了不同的必报模块中导致无法互相调用。我们使用观察者模式来解决问题。首先要分析什么模块应该注册消息,那些模块应该发布消息。
发布留言和删除留言是观察者发布信息、追加评论和用户信息的增减是被动出发所以是订阅者需要去注册信息。
用户模块是发布信也是信息的接受者,提交模块是信息的发送者,浏览模块是信息的接受者。

具体实现如下:

let Observer = require('./01、创建一个订阅者');
let observer = new Observer();

// 外观模式 简化获取元素
function $(id) {
    return document.getElementById(id);
}
//工程师 A
(function() {
    // 追加一则信息
    function addMsgItem(e) {
        let text = e.args.text,                     // 获取信息中用户添加的文本内容
            ul = $('msg'),                          // 留言容器元素
            li = document.createElement('li'),      // 创建内容容器元素
            span = document.createElement('span');  // 删除按钮
        li.innerHTML = text;

        // 关闭按钮
        span.onclick = function () {
            ul.removeChild(li);
            // 发布删除留言信息
            observer.fire('removeCommentMessage', {
                num: -1
            });
        };
        // 添加删除按钮
        li.appendChild(span);
        // 添加留言节点
        ul.appendChild(li);
    }
    // 注册添加评论信息
    observer.regist('addCommentMessage', addMsgItem);
})();

// 工程师B
(function () {
    function changeMsgNum(e) {
        // 获取需要增加的用户信息数目
        let num = e.args.num;
        // 增加用户消息数目并写入页面
        $('msg_num').innerHTML = parseInt($('msg_num').innerHTML) + num;
    }
    // 注册添加评论信息
    observer.regist('addCommentMessage', changeMsgNum);
    observer.regist('removeCommentMessage', changeMsgNum);
})();

// 工程师C 提交信息
(function () {
    $('user_submit').onclick = function () {
        // 获取用户输入
        let text = $('user_input');
        if(text.value === '') return;
        // 发布一则评论信息
        observer.fire('addCommentMessage', {
            text: text.value,
            num: 1
        });
        text.value = ''
    }
})();

解决对象间的耦合

以课堂老师提问学生的例子来说明问题: 创建学生类,学生是被提问对象,所以他们是订阅者。同时学生也要对问题进行回答的动作。

let Observer = require('./01、创建一个订阅者');
let observer = new Observer();

// 学生类
let Student = function (result) {
    let that = this;
    that.result = result;
    // 回答问题动作
    that.say = function () {
        console.log(that.result);
    }
};
// 所有学生都可以回答问题,他们回答问题的方法answer
Student.prototype.answer = function (question) {
    // 注册问题
    observer.regist(question, this.say)
};
// 如果学生睡着了,就没有办法回答问题了
Student.prototype.sleep = function (question) {
    console.log(this.result + question + ' 已经注销');
    // 取消对老师的监听
    observer.remove(question, this.say)
};

// 教师类,是一个发布者,他需要一个提问方法
let Teacher = function(){};
Teacher.prototype.ask = function (question) {
    console.log(`问题是 ${question}`);
    // 发布问题
    observer.fire(question);
};

/*测试*/
// 创建三个学生对象
let student1 = new Student('学生1 回答问题');
let student2 = new Student('学生2 回答问题');
let student3 = new Student('学生3 回答问题');

// 这三个同学监听老师的提问
student1.answer('什么是设计模式');
student1.answer('简述观察者模式');
student2.answer('什么是设计模式');
student3.answer('什么是设计模式');
student3.answer('简述观察者模式');
// 3同学睡着了,注销监听
student3.sleep('简述观察者模式');

// 教师类
let teacher = new Teacher();
teacher.ask('什么是设计模式');
teacher.ask('简述观察者模式');

第十八章-状态模式

状态模式(State): 当一个对象内部状态发生变化时,会导致其行为发生改变,状态改变了对下对象。

用最美图片写一个例子

需要选出本月最美图片,根据网友的投票,每张图片有一下几个结果。

// 展示结果
function showResult(result) {
    if(result === 0) {
        // 处理结果 0
    } else if (result === 1) {
        // 处理结果1
    } else if(result === 2) {
        // 处理结果2
    } else if(result === 3) {
        // 处理结果3
    }
}

如果某一天项目经理心血来潮,想增删结果,那就悲剧了。用状态模式,每一种条件作为对象内部的一种状态,面对不同判断结果,它其实就只是选择对象内的一种状态而已。
一个最简单的例子,我们可以将不同的判断结果封装在对象内,然后返回一个可以被调用的接口。

// 投票结果状态对象
class ResultState {
    // 判断结果保存在内部状态中
    constructor() {
        this.states = {
            state0: function () {
                console.log('这里是第一种结果状态')
            },
            state1: function () {
                console.log('这里是第二种状态结果')
            },
            state2: function () {
                console.log('这里是第三种状态结果')
            },
            state3: function () {
                console.log('这里是第四种状态结果')
            }
        }
    }
    show(result) {
        this.states['state' + result] && this.states['state' + result]()
    }
}
let resultState = new ResultState();

/*测试*/
resultState.show(3);

上面这个例子基本上有了状态模式的基本雏形了。
对于状态模式,主要是讲条件判断的不同结果转换为状态对象的内部状态。一般作为状态对象内部的私有变量。提供一个可以调用的对象内部状态的接口方法,做增删改用。

另外一个例子,超级玛丽

在超级玛丽游戏中,跳跃,开枪,蹲下,奔跑等都是一个一个的状态。很多时候再游戏中需要好几个状态同时触发的。
如果用普通的if else 的方式来做判断,会出现下面的结果:

// 单动作条件判断 每增加一个动作就需要添加一个判断
let lastAction = '';
function changeMarry(action) {
    if(action === 'jump') {
        // 跳跃
    } else if(cation === 'move') {
        // 移动动作
    } else {
        // 默认情况
    }
}

// 复合动作的判断 开销是要翻倍的
let lastAction1 = '';
let lastAction2 = '';
function changeMarry(action1, action2) {
    if(action1 === 'shoot') {
        // 射击
    } else if(action1 === 'jump') {
        // 跳跃
    } else if(action1 === 'move' && action2 === 'shoot') {
        // 移动射击
    } else if(action1 === 'jump' && action2 === 'shoot') {
        // 跳跃射击
    }
    //保留上一个动作
    lastAction1 = action1 || '';
    lastAction2 = action2 || '';
}

状态的优化

上面虽然实现的需求,但是可维护性非常糟糕。用状态模式优化: 首先创建一个状态对象,内部保存状态变量,然后内部封装好每一种动作对应的状态,最后状态返回一个借口对象。

class Action {
    constructor() {
        // 内部私有变量
        this._currentState = {};
        // 动作与状态方法的映射
        this.states = {
            jump() {
                console.log('跳跃')
            },
            move() {
                console.log('移动')
            },
            shoot() {
                console.log('移动')
            },
            squat() {
                console.log('下蹲')
            }
        }
    }
}

class MarryState extends Action {
    constructor() {
        super();
    }

    //改变状态方法
    changeState() {
        let arg = arguments;
        // 重置内部状态
        this._currentState = {};
        if(arg.length) {
            for(let i  = 0, len = arg.length; i< len; i++) {
                // 向内部添加动作
                this._currentState[arg[i]] = true;
            }
        }
        return this;
    }
    // 执行动作
    gose() {
        console.log('触发一次动作');
        for(let i in this._currentState) {
            // 如果该动作在就执行
            this.states[i] && this.states[i]();
        }
        return this;
    }
}

let marry  = new MarryState();
marry.changeState('jump', 'shoot').gose().gose().changeState('shoot').gose();

第十九章-策略模式

策略模式(Strategy): 将定义的一组算法封装起来,使其相互之间可以替换。封装的算法具有一定的独立性,不会碎客户端变化而变化。

商品促销的例子

在圣诞节,一部分商品五折出售,一部分八折出售,一部分九折出售。到了元旦节,普通用户满100返30, vip满100返50。
状态模式可以处理这种多分支的情况。但是这里有圣诞节和元旦节两种情况。对于一种商品的促销策略只有一种情况,而不需要其他促销策略。因此采用策略模式。

从结构上看,他和状态模式很像。内部封装一个对象,通过返回的接口对象实现内部对象的调用。不同的是策略模式不需要状态管理,状态之间没有依赖关系,策略之间可以互换,在策略对象内部保存的是相互独立的一些算法。
首先要讲这些算法封装在一个策略对象内,然后对每一种商品的策略调用时,直接对策略对象中的算法调用就可以了。而策略算法又独立地封装在策略对象内。

策略对象的实现

// 价格策略对象
class PriceStrategy {
    constructor() {
        // 内部算法对象
        this.stragtegy = {
            // 100返30
            return30(price) {
                return +price + parseInt( price / 100) * 30;
            },
            // 100 返 50
            return50(price) {
                return +price + parseInt(price/ 100) * 50;
            },
            // 9 折
            percent90(price) {
                return price * 100 * 90 / 10000
            },
            percent80(price) {
                return price * 100 * 80 / 10000
            },
            percent50(price) {
                return price * 100 * 50 / 10000
            }
        }
    }
    // 策略算法调用接口
    getPrice(algorithm, price) {
        return this.stragtegy[algorithm] && this.stragtegy[algorithm](price);
    }
}
let priceStrategy = new PriceStrategy();
let price = priceStrategy.getPrice('return50', 314.67);
console.log(price);

策略模式我们外部看不到算法的具体实现,我们也只关心算法实现的记过,不关注过程。

jquery中的缓冲函数

让一个div动起来,通过对jquery的animate动画传入不同运动算法就可以实现不同的运动曲线了。

$('div').animate({width:'200px'}, 1000, 'linear');
$('div').animate({width:'200px'}, 1000, 'swing');

这个就是用策略模式实现的,提供了linear、swing两种曲线就是策略算法。

表单验证

class InputStrategy {
    constructor() {
        this.strategy = {
            // 是否为空
            notNull(value) {
                return /\s+/.test(value) ? '请输入内容' : '';
            },
            // 是否是一个数字
            number(value) {
                return /^[0-9]+(\.[0-9]?$)/.test(value) ? '' : '请输入数字';
            },
            phone(value) {
                return /(\d{3}-|\d{4}-)?(\d{8}|\d{7})?/.test(value) ? '' : '正输入正确的电话号码格式, 如: 010-12345678 或者 0234-1234567'
            }
        }
    }
    check(type, value) {
        // 祛除空格
        value = value.replace(/^\s|\s+$/g, '');
        return this.strategy[type] ? this.strategy[type](value) : '没有改类型检测方法'
    }
    // 添加策略
    addStrategy(type, fn) {
        this.strategy[type] = fn;
    }
}
let inputStrategy = new InputStrategy();
// 比如说我们需要添加一个扩展
inputStrategy.addStrategy('email', function (value) {
    return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value) ? '' : '请输入真确的email'
});

/*算法的调用*/
// 外观模式
function $tag(tag, context) {
    context  = context || document;
    return context.getElementsByTagName(tag);
}
// 提价按钮
$tag('input')[1].onclick = function () {
    // 获取输入框的内容
    let value = $tag('input')[0].value;
    // 获取日期格式检验结果
    $tag('span')[0].innerHTML = inputStrategy.check('email', value);
};

第二十章-责任链模式

责任链模式(Chain of Responsibility): 解决请求的发送者与请求的接受者之间的耦合。通过责任链上的多个对象分解请求流程。实现请求在多个对象之间传递,知道最后一个对象完成请求处理。

半成品的需求

有一个半成品的需求,首先要在表单输入框中添事件,做输入提示和输入验证处理。完成功能需要向服务端发送请求,还要在原有的页面中创建其他的组件,但是具体输入框有哪些不确定。
分析这个需求: 有的输入框需要绑定keyup事件,有的输入框需要绑定change事件,绑定事件是第一部分。第二部分创建XHR进行一步请求。第三部分是适配相应数据,处理数据格式。最后一部分是向组件创建器传入数据生成组件。

// 异步请求对象
let sendData = function (data, dealType, dom) {
    let xhr = new XMLHttpRequest(),
        url = 'getData.json?mod=userInfo';
    // 请求返回事件
    xhr.onload = function () {
        if((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
            dealData(xhr.responseText, dealType, dom);
        } else {
            // 请求失败
        }
        // 拼接请求字符串
        for (let i in data) {
            url += '&' + i + '=' + data[i];
        }
        // 发送请求
        xhr.open('get', url, true);
        xhr.send(null);
    }
};

// 响应数据适配模块
let dealData = function (data, dealType, dom) {
    let dataType = Object.prototype.toString.call(data);
    switch (dealType) {
        // 输入框提示
        case 'sug':
            // 如果是数组对象
            if(dataType === '[object Array]') {
                // 创建提示框组件
                return createSug(data, dom);
            }
            // 将相应的对象数据转化为数组
            if(dataType === '[object Object]') {
                let newData = [];
                for (let i in data) {
                    newData.push(data[i]);
                }
                return createSug(newData, dom);
            }
            return createSug([data], dom);
        case 'validate':
            // 校验组件
            return createValidateResult(data, dom);
    }
};

// 创建提示框组件
let createSug = function (data, dom) {
    let i = 0,
        len = data.length,
        html = '';
    // 拼接每一条语句
    for(; i < len; i++) {
        html += `<li> ${data[i]} </li>`
    }
    dom.parentNode.getElementsByTagName('ul')[0].innerHTML = html;
};

// 创建校验组件
let createValidateResult = function (data, dom) {
    dom.parentNode.getElementsByTagName('span')[0].innerHTML = data;
};

站点测试-单元测试

/*站点的测试*/
dealData('用户名不对', 'validate', input[0]);
dealData(123, 'sug', input[1]);
dealData(['爱奇艺', '阿里巴巴'], 'sug', input[1]);
dealData({
    'iqy': '爱奇艺',
    'albb': '阿里巴巴'
}, 'sug', input[1]);
// 这样测试会直接调用到了, createSug 和 createValidateResult , 可以先暂时简化他们, 模拟一下测试方法
let createSug = function (data, dom) {
    console.log(data, dom, 'createSug');
};
let createValidateResult = function (data, dom) {
    console.log(data, dom, 'createValidateResult')
};
// 然后就可以执行了

最后等方案确定之后,直接把我们需要的东西灌进去就可以了。

总结

责任链模式,其实就是把一个较大的或者不确定的需求,拆分成一个一个细小的模块。各自做好各自模块的功能,把做好的事儿,交给下一步做。

第二十一章-命令模式

命令模式(Command): 将请求和实现解耦并封装成为独立的对象,从而使不同的请求对客户端的实现参数化。

自由化创建视图的例子

在莫夸里面创建一个图片,有时候又想创建多个图片。这种场景就可以使用命令模式。
命令模式就是讲请求和实现解耦。将创建模块的逻辑封装在一个对象里面,然后对外提供一个参数化请求接口,通过调用这个接口传递一些参数实现调用命令对象内部的一些方法。

具体实现

// 命令对象
let viewCommand = function() {
    let me = this;
    let tpl = {
        // 展示图片结构模块
        product: [
            `<div>
                <img src="${me.src}" alt="">
                <p>${me.text}</p>
            </div>`
        ].join(''),
        // 展示标题结构模板
        title: [
            `<div class="title">
                <div class="main">
                    <h2>${me.title}</h2>
                    <p>${me.text}</p>
                </div>
            </div>`
        ].join('')
    };
    // 格式化字符串缓存字符串
    let html = '';

    // 方法集合
    let Action = {
        // 创建方法
        create: function(data, view){
            if(data.length) {
                me = Object.assign(me, data);
                for (let i = 0,len = data.length;i < len;i++) {
                    // 将格式化之后的字符串缓存到html中
                    html += tpl[view];
                }
            } else {
                me = Object.assign(me, data);
                html +=tpl[view]
            }
        },
        // 展示方法
        display: function(container, data, view){
            if(data) {
                this.create(data, view);
            }
            document.getElementById(container).innerHTML = html;
            html = '';
        },
    };
    // 命令接口
    return function excute(msg){
        msg.param = Object.prototype.toString.call(msg.param) === '[Object Array]' ? msg.param : [msg.param];
        Action[msg.command].apply(Action, msg.param);
    }
}

测试上面的代码

// 测试数据
let productData = [
        {
            src: 'command/01.jpg',
            text: '图片1'
        },
        {
            src: 'command/02.jpg',
            text: '图片2'
        },
        {
            src: 'command/03.jpg',
            text: '图片3'
        }
    ],
    titleData = {
        title: '我是title',
        tips: 'bibibibibibibibibib'
    };

// 调用
viewCommand({
    command: 'create',
    //
    param: ['title', {
        src: 'command/01.jpg',
        text: '图片1'
    }, 'product']
});

// 创建多个图片
viewCommand({
    command: 'display',
    param: ['product', productData, 'product']
})

另外的需求,绘图命令

在使用canvas的时候,经常调用内置方法,需要不停的使用canvas元素的上下文。这样开发中耦合度很高,如果被人串改了canvas的上下文,整个项目就跑不起来。所以有解耦的必要,可以考虑使用命令模式。

let CanvasCommand = (function () {
    let canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');
    // 内置方法
    let Action = {
        // 填充颜色
        fillStyle(c) {
            ctx.fillStyle = c;
        },
        // 填充矩形
        fillRect(x, y, width, height) {
            ctx.fillRect(x, y, width, height);
        },
        // 描边色彩
        strokeStyle(c) {
            ctx.strokeStyle = c;
        },
        // 描边矩形
        strokeRect(x, y, width, height) {
            ctx.strokeRect(x, y, width, height);
        },
        // 填充文字
        fillText(text, x, y) {
            ctx.fillText(text, x, y);
        },
        // 开启路径
        beginPath() {
            ctx.beginPath();
        },
        // 移动画笔
        moveTo(x, y) {
            ctx.moveTo(x, y);
        },
        // 画笔连线
        lineTo(x, y) {
            ctx.lineTo(x, y);
        },
        // 绘制弧线
        arc(x, y, r, begin, end, dir) {
            ctx.arc(x, y, r, begin, end, dir);
        },
        // 填充
        fill() {
            ctx.fill();
        },
        // 描边
        stroke() {
            ctx.stroke();
        }
    };

    return {
        // 命令接口
        excute(msg) {
            if(! msg) return;
            if(msg.length) {
                for(let i = 0,len = msg.length;i<len;i++) {
                    arguments.callee(msg[i]);
                }
            } else {
                msg.param = Object.prototype.toString.call(msg.param) === '[object Array]' ? msg.param: [msg.param];
                Action[msg.command].apply(Action, msg.param);
            }
        }
    }
})();

// 填充给一个矩形
CanvasCommand.excute([
    {
        command: 'fillStyle',
        param: 'red'
    },
    {
        command: 'fillRect',
        param: [20,20,100,100]
    }
])

第二十二章-访问者模式

定义个绑定事件, 但是在低版本浏览器中会报错。

let bindEvent = function (dom, type, fn) {
    if(dom.addEventListener) {
        dom.addEventListener(type, fn, false);
    } else if(dom.attachEvent) {
        dom.attachEvent('on'+ type, fn);
    } else {
        dom['on'+ type] = fn;
    }
};


/**
 * 下面的在IE低版本浏览器运行会有问题
 * 这个地方运行就有问题了,因为this.style 中的this 在低版本IE中,指向的是window对象
 * @type {HTMLElement | null}
 */
let demo = document.getElementById('dome');
bindEvent(demo, 'click', function () {
    this.style.background = 'red';
});

对象访问器的一个示例

/**
 * create by yanle
 * connect me [email protected]
 * create time 2018-12-31 18:10
 */

let Visitor = {
    // 截取方法
    splice: function () {
        // splice 方法参数, 从原来的参数的第二个参数开始计算
        let args = Array.prototype.splice.call(arguments, 1);
        // 对第一个参数对象执行splice 方法
        return Array.prototype.splice.apply(arguments[0], args);
    },

    push: function () {
        let len = arguments[0].length || 0;
        let args = this.splice(arguments, 1);
        arguments[0].length = len + arguments.length - 1;
        return Array.prototype.push.apply(arguments[0], args);
    },

    pop: function () {
        return Array.prototype.pop.apply(arguments[0]);
    }
};


// 这样就可以操作类数组的方式操作对象了
let a = {};
console.log(a.length);          // undefined
Visitor.push(a, 1,2,3,4);
console.log(a.length);          // 4
Visitor.push(a, 4,5,6);
console.log(a);                 // { '0': 1, '1': 2, '2': 3, '3': 4, '4': 4, '5': 5, '6': 6, length: 7 }
console.log(a.length);          // 7
Visitor.pop(a);
console.log(a);                 // { '0': 1, '1': 2, '2': 3, '3': 4, '4': 4, '5': 5, length: 6 }
console.log(a.length);          // 6
Visitor.splice(a, 2);
console.log(a);                 // { '0': 1, '1': 2, length: 2 }

第二十三章-中介者模式

通过中介者对象封装一些列对象之间的交互,是对象之间不再相互引用,降低耦合度。有的时候也可以改变对象之间的交互。

跟观察者模式的区别:
首先他们都是通过消息收发机制实现的,不过在观察者模式中,一个对象既可以是消费者的发送者,也可以是消息的接受者,他们之间的信息交流依托于消息系统之间的解耦。
中介者模式中消息的发送方只有一个,就是中介者对象,而且中介者对象不能订阅消息, 只能那些活跃对象(订阅者)才能订阅中介者的消息。

代码示例如下: 01、创建中介者对象

/**
 * create by yanle
 * create time 2019/1/2 下午4:46
 */

class Mediator {
    constructor() {
        // 消息对象
        this._msg = {};
    }

    /**
     * 订阅消息方法
     * @param type  消息名称
     * @param action    消息回到函数
     */
    register(type, action) {
        // 如果消息存在
        if(this._msg[type]) {
            // 存入
            this._msg[type].push(action)
        } else {
            // 消息不存在, 创建容器
            this._msg[type] = [];
            this._msg[type].push(action)
        }
    }

    /**
     * 发布消息的方法
     * @param type  发布消息的名称
     */
    send(type) {
        if(this._msg[type]) {
            for (let actionKey in this._msg[type]) {
                // 执行回调函数
                this._msg[type] && this._msg[type][actionKey]();
            }
        }
    }
}

module.exports = Mediator;

let mediator = new Mediator();
mediator.register('demo', function () {
    console.log('first');
});
mediator.register('demo', function () {
    console.log('second')
});

mediator.send('demo');  // 分别输出first, second

一个实际场景的使用:02、一个完整的使用场景

/**
 * create by yanle
 * create time 2019/1/2 下午5:11
 */

const Mediator = require('./01、创建中介者对象');

/**
 * 隐藏导航方法
 * @param mod   模块
 * @param tag   标签
 * @param showOrHide    是否隐藏(show/hide)
 */
let showHideNavWidget = function (mod, tag, showOrHide) {
    // 获取导航
    let dom = document.getElementById(mod);
    // 过去tag
    let tags = dom.getElementsByTagName(tag);
    let isShowOrHide = (!showOrHide || showOrHide === 'hide') ? 'hidden' : 'visible';
    tags.forEach(function (item) {
        item.style.visibility = isShowOrHide;
    })
};


const mediator = new Mediator();

// 订阅隐藏用户收藏导航消息提示信息
mediator.register('hideAllNavNum', function () {
    showHideNavWidget('collection_nav', 'b', false);
});
// 订阅现实用户收藏导航消息提示信息
mediator.register('showAllNavNum', function () {
    showHideNavWidget('collection_nav', 'b', true);
});
// 订阅隐藏用户收藏导航网址信息
mediator.register('hideAllNavUrl', function () {
    showHideNavWidget('collection_nav', 'span', false);
});
// 订阅现实用户收藏导航网址信息
mediator.register('showAllNavUrl', function () {
    showHideNavWidget('collection_nav', 'span', true);
});

// 发布消息
let hideNum = document.getElementById('hide_num'),
    hideUrl = document.getElementById('hide_url');
// 消息提示选框事件
hideNum.onchange = function () {
    if(hideNum.checked) {
        mediator.send('hideAllNavNum');
    } else {
        mediator.send('showAllNavNum');
    }
};

// 网址选框事件
hideUrl.onchange = function () {
    if(hideUrl.checked) {
        mediator.send('hideAllNavUrl');
    } else {
        mediator.send('showAllNavUrl');
    }
};

第二十四章-备忘录模式

描述:
在不破坏对象的封装性的前提下, 在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象回复到以前的某个状态。

实际场景:
有这么一个场景, 就是在分页中, 用户点击下一页的时候, 就去去请求数据, 但是又点回上一页的时候, 大多数的操作还是请求上一页数据。
这样的操作就会导致多余的请求。为了避免这种多余的请求, 我们就可以做缓存数据。

/**
 * create by yanle
 * create time 2019/1/2 下午5:56
 */

class Page {
    constructor() {
        this.cache = {};
    }

    init(page, fn) {
        // 判定是否有缓存
        if(this.cache[page]) {
            // 恢复到该页面的状态 , 现实该页面的内容
            this.showPage(page, this.cache[page]);
            // 执行成功的回调
            fn && fn();
        } else {
            // 没有cache数据
            $.post('/data/getNewsData.php', {
                page: page
            },  (res) => {
                // 请求成功
                if(res.errno === 0) {
                    // 显示页面数据
                    this.showPage(page, res.data);
                    this.cache[page] = res.data;
                    fn && fn();
                } else {
                    console.log('异常处理');
                }
            })
        }
    }

    showPage(page, data) {
        // 处理页面逻辑
        console.log('处理页面逻辑', page, data)
    }
}

示例: 01、新闻缓存器

第二十五章-迭代器模式

描述:
在不暴露对象内部结构的同时, 可以顺序的访问聚合对象的元素。

/**
 * create by yanle
 * create time 2019/1/2 下午6:26
 */

class Iterator {
    constructor(items, container) {
        // 父容器, 若 container 参数存在, 并且可以获取该元素则获取, 否则获取document
        this.container = container && document.getElementById(container) || document;
        // 获取元素
        this.items = this.container.getElementsByTagName(items);
        // 元素长度
        this.length = this.items.length;

        // 当前索引值, 默认: 0
        this.index = 0;
        // 缓存源数组splice方法
        this.splice = [].splice();
    }

    first() {
        this.index = 0;
        return this.items[this.index];
    }

    last() {
        this.index = this.length - 1;
        return this.items[this.index];
    }

    pre() {
        if (--this.index > 0) {
            return this.items[this.index];
        } else {
            this.index = 0;
            return null;
        }
    }

    next() {
        if (++this.index < this.length) {
            return this.items[this.index]
        } else {
            this.index = length - 1;
            return null;
        }
    }

    get(num) {
        this.index = num >= 0 ? num % this.length : num % this.length + this.length;
        return this.items[this.index];
    }

    // 对于每一个元素执行某一个方法
    dealEach(fn) {
        // 第二个参数作为回调函数参数
        let args = this.splice.call(arguments, 1);
        for (let item of this.items) {
            fn.apply(item, args);
        }
    }

    // 对某一个元素执行某一个方法
    dealItem(num, fn) {
        fn.apply(this.get(num), this.splice.call(arguments, 2))
    }

    // 排他方式处理某一个元素
    exclusive(nums, allfn, numfn) {
        // 对所有元素执行回调函数
        this.dealEach(allfn);
        // 如果是num类型的数组
        if(Object.prototype.toString.call(nums) === "[object Array]") {
            nums.forEach((num) => {
                this.dealItem(num, numfn)
            })
        } else {
            this.dealItem(nums, numfn)
        }
    }
}


/*
* 比如获取页面中id 为 container 的ul元素中的4个li元素
* */
let demo  = new Iterator('li', 'container');
console.log(demo.first());          // <li>1</li>
console.log(demo.pre());            // null
console.log(demo.next());           // <li>2</li>
console.log(demo.get(2000));        // <li>1</li>

// 处理所有元素
demo.dealEach(function (text, color) {
    this.innerHTML = text;
    this.style.background = color;
}, 'test', 'pink');

// 排他**处理3,4元素
demo.exclusive([2,3], function () {
    this.innerHTML = '被排除了';
    this.style.background = 'green';
}, function () {
    this.innerHTML = '选中的';
    this.style.background = 'red';
});

代码示例:
01、迭代器的实现

第二十六章-解释器

描述:
用一些描述性的语句, 几次功能的提取抽象, 形成一套语法规则, 这就是解释器要处理的事情。

/**
 * create by yanle
 * create time 2019/1/3 下午7:14
 */

//  解释器
class Interpreter {
    // 获取兄弟元素名称
    static getSiblingName(node) {
        // 存在兄弟节点
        if(node.previousSibling) {
            let name = '',
                count = 1,
                nodeName = node.nodeName,
                sibling = node.previousSibling;

            // 如果存在前一个兄弟元素
            while(sibling) {
                // 如果节点为元素, 并且节点类型与前一个兄弟元素类型相同, 并且前一个兄弟元素名称存在
                if(sibling.nodeType === 1 && sibling.nodeType === node.nodeType && sibling.nodeName) {
                    // 如果节点名称和前一个兄弟元素名称相同
                    if(nodeName === sibling.nodeName) {
                        // 节点名称后面添加计数
                        name += ++count;
                    } else {
                        count = 1;
                        name += '|' + sibling.nodeName.toUpperCase();
                    }
                }
                sibling = sibling.previousSibling;
            }
            return name;
        } else {
            return ''
        }
    }

    // XPath 解释器
    static main(node, wrap = document) {
        let path = [];      // 路径数组
        if(node === wrap) {
            // 容器节点为元素
            if(wrap.nodeType === 1) {
                path.push(wrap.nodeName.toUpperCase());
            }
            return path;
        }

        // 当前节点的父节点不等于容器节点
        if(node.parentNode !== wrap) {
            // 对当前节点的父节点执行遍历操作
            path = arguments.callee(node.parentNode, wrap);
        } else {
            // 容器节点为元素
            if(wrap.nodeType === 1) {
                path.push(wrap.nodeName.toUpperCase());
            }
        }

        // 获取元素的兄弟元素名称统计
        let siblingsNames = this.getSiblingName(node);
        // 如果节点为元素
        if(node.nodeType === 1) {
            path.push(node.nodeName.toUpperCase() + siblingsNames);
        }
        return path;
    }
}

// 使用方式
let path = Interpreter.main(document.getElementById('span7'));
console.log(path);          // HTML>BODY|HEAD>DEV2>DEV2>DEV>UL>LI2>SPAN

代码示例:
01、Interpreter

React Context 使用

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

基础使用

如果在使用 Context 的时候, 传递值需要这样:

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用 Context 之后可以这样:

import React, { Component, createContext } from 'react';
import { Button } from 'antd';
import CodeViewContainer from '../../../components/BaseCodeView/CodeViewContainer';

const ThemeContext = createContext('light');

class ContextDemo1 extends Component {
  render() {
    return (
      <CodeViewContainer codePath="Context/ContextDemo1">
        <ThemeContext.Provider value={'dark'}>
          <ToolBar />
        </ThemeContext.Provider>
      </CodeViewContainer>
    );
  }
}

const ToolBar = () => {
  return (
    <div>
      <ThemeButton />
    </div>
  );
};

class ThemeButton extends Component {
  static contextType = ThemeContext;

  render() {
    const { context } = this;
    return <Button>{context}</Button>;
  }
}

export default ContextDemo1;

使用场景问题

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。

这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。

重要API

React.createContext

const MyContext = React.createContext(defaultValue);

组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

Context.Provider

<MyContext.Provider value={/* 某个值 */}>

  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。
  • Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 基于 MyContext 组件的值进行渲染 */
  }
}
MyClass.contextType = MyContext;
  • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。
  • 这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
  • 如果你正在使用实验性的 public class fields 语法,你可以使用 static 这个类属性来初始化你的 contextType。
class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* 基于这个值进行渲染工作 */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
  • 这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。

Context.displayName

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
  • context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

使用场景

动态更新Context

import React, { Component, createContext, FC } from 'react';
import CodeViewContainer from '../../../components/BaseCodeView/CodeViewContainer';

/*
 * 动态更新 Context
 * */

/* ==============================  const - Start ============================== */
const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

const ThemeContext = createContext(themes.dark);
/* ==============================  const - End   ============================== */

/* ==============================  ThemedButton - Start ============================== */
class ThemedButton extends Component<{ onClick: () => void }> {
  render() {
    const { context } = this;
    return (
      <button {...this.props} style={{ backgroundColor: context.background }}>
        {this.props.children}
      </button>
    );
  }
}

ThemedButton.contextType = ThemeContext;
/* ==============================  ThemedButton - End   ============================== */

/* ==============================  ToolBar:ThemedButton 的一个中间件 - Start ============================== */
const ToolBar: FC<{ changeTheme: () => void }> = props => {
  return <ThemedButton onClick={props.changeTheme}>Change Theme</ThemedButton>;
};

/* ==============================  ToolBar:ThemedButton 的一个中间件 - End   ============================== */

/* ==============================  ContextDemo2 - Start ============================== */
interface ContextDemo2State {
  theme: {
    foreground: string;
    background: string;
  };
}

class ContextDemo2 extends Component<any, ContextDemo2State> {
  state = {
    theme: themes.light,
  };

  toggleTheme = () => {
    this.setState(state => ({
      theme: state.theme === themes.dark ? themes.light : themes.dark,
    }));
  };

  render() {
    return (
      <CodeViewContainer codePath="Context/ContextDemo2">
        <ThemeContext.Provider value={this.state.theme}>
          <ToolBar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
      </CodeViewContainer>
    );
  }
}
/* ==============================  ContextDemo2 - End   ============================== */

export default ContextDemo2;

在嵌套组件中更新 Context

import React, { Component, createContext } from 'react';

/*
 * 在嵌套组件中更新 Context
 *
 * 从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。
 * 在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context:
 * */

/* ==============================  const - Start ============================== */
const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

const ThemeContext = createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

const { Provider, Consumer } = ThemeContext;
/* ==============================  const - End   ============================== */

/* ==============================  ThemeToggleButton - Start ============================== */
const ThemeToggleButton = () => {
  return (
    <Consumer>
      {({ theme, toggleTheme }) => (
        <button onClick={toggleTheme} style={{ backgroundColor: theme.background }}>
          Toggle Theme
        </button>
      )}
    </Consumer>
  );
};
/* ==============================  ThemeToggleButton - End   ============================== */

/* ==============================  ContentComponent - Start ============================== */
const Content = () => (
  <div>
    <ThemeToggleButton />
  </div>
);
/* ==============================  ContentComponent - End   ============================== */

/* ==============================  ContextDemo3 - Start ============================== */
interface ContextDemo3State {
  theme: {
    foreground: string;
    background: string;
  };
}

class ContextDemo3 extends Component<any, ContextDemo3State> {
  state = {
    theme: themes.light,
  };

  toggleTheme = () => {
    this.setState(state => ({
      theme: state.theme === themes.dark ? themes.light : themes.dark,
    }));
  };

  render() {
    return (
      <Provider value={Object.assign({}, this.state, { toggleTheme: this.toggleTheme })}>
        <Content />
      </Provider>
    );
  }
}
export default ContextDemo3;
/* ==============================  ContextDemo3 - End   ============================== */

消费多个 Context

为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。

// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');

// 用户登录 context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // 提供初始 context 值的 App 组件
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// 一个组件可能会消费多个 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

注意

当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。
举个例子,当每一次 Provider 重渲染时,
以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

class App extends React.Component {
  render() {
    return (
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
  }
}

为了防止这种情况,将 value 状态提升到父节点的 state 里:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

[vim] 入门基础

基础vim

vim起步

命令 左右
i 进入编辑模式
I 行首插入
a 在光标后面开始输入文字
A 行尾插入
o 新起一行输入文字
O 在上一行插入
v 批量操作
V 整行的选中
control + v 块状操作

快速纠错

命令 描述
ctrl + h 删除上一个字符
ctrl + w 删除上一个单词
ctrl + u 删除当前行
ctrl + a 行首
ctrl + b 行尾
gi 快速回到最后一次编辑的地方并且插入

vim快速移动

hjkl

命令 描述
w/W 移动到下一个 word/WORD 开头
e/E 移动到下一个 word/WORD 尾
b/B 回到上一个 word/WORD 开头

word指的是非空白分隔符的单词, WORD 以空白分隔符的单词

行间搜索移动

使用 f{char}可以移动到char字符上, t移动到char 的前一个字符
如果一次有搜索到, 可以用分号 ; 或者 , 继续搜索改行下一个/上一个

水平移动

命令 描述
0 移动到行首
$ 移动到行尾
^ 移动到第一个非空白字符
g_ 移动到行尾非空白字符

页面移动

命令 描述
gg 文件开头
G 文件结尾
H/M/L 快速跳转到屏幕的开头、中间和结尾
ctrl + u / crtl + f 上下翻页
zz 把屏幕设置为中间

快速正删改查

增加字符: i/a/o

快速删除: 如何删除一个字符和单词呢?

  • normal 模式下 x 快速删除一个字符
  • normal 模式下 d 快速删除一个单词
  • normal 模式下 daw 删除一个单词
  • normal 模式下面 dd 可以删除一个行
  • normal 模式下 dt* 表示删除某段内容, 直到 * 为止

其中 x 和 d 都是可以搭配数字一起是用。
比如 2d 表示删除两行 。 4x 表示删除四个服

修改: 删除之后改为我们希望的文本内容

r(replace)/c(change)/s(substitute): r可以替换一个字符, s 替换并且插入模式, c 配合文本对象快速修改内容
R: 可以不断的替换当前字符

最常用的就是 c 的操作

查询操作:
/ 或者 ? | 前向或者反向查询
n 或者 N | 跳转到下一个或者上一个匹配
* 或者 # | 单词的前向或者后巷匹配

搜索替换

命令::[range]s[ubstitute]/{pattern}/{string}/[flags]

  • range 表示范围, 比如: 10, 20 表示10-20行, % 表示全部
  • pattern 表示要替换的欧式
  • string 表示替换后的文本
  • flags
    • g(global)表示全局范围内执行
    • c(confirm)表示缺人,可以缺人或者拒绝修改
    • n(number)报告匹配到的次数而不替换, 可以用来查询匹配次数

例如需要把文本中的self替换为this: :% s/self/this/g
例如我们需要精准替换: :% s/\<quack\>/main/g

撤销与反撤销

命令 描述
u 撤销操作
ctrl + r 撤销 上一步的撤销操作

多文件操作

有几个相关概念: Buffer、窗口、Tab
作为了解作用 ...... 如果以后需要使用再了解就OK

text-Object

语法命令: [number]<command>[text object]
number 表示次数、command 是命令, d(delete)、c(change)、y(yank)
text object 是要操作的文本对象, 比如单词w, 句子s, 段落p

示例:
viw - 选中单词
vaw - 选中单词也会选中空格
vi" - 就可以快速选中双引号内的内容
ci" - 快速删除双引号中的内容, 并且进入插入模式

记住四个常用命令
d - 删除
c - 修改并且插入
v - 选中
y - 复制

复制、剪切、粘贴

命令 描述
y 复制
yy 复制一行
d 剪切
p 粘贴

宏macro

可以看做一些列命令的结合
使用 q 来录制命令, 再次使用 q 结束录制
使用 q{register} 选择需要保存的寄存器
使用 @{register} 回放命令

例如要给所有的url 加上双引号的需求:
q a --> I --> 插入行首输入引号 --> A --> 移动行尾, 输入引号 --> q

选中所有行, 输入进入 normal 模式, normal @a 回车就可以了

补全

ctrl + n / ctrl + p - 补全单词
ctrl + x / ctrl + f - 补全文件名
ctrl + x / ctrl + o - 补全代码(需要开启文件类型检测, 安装插件)

其他命令

命令 描述
:syntax on 打开高亮
:set nu 打开行数统计
:set hls 搜索结果高亮
:set incsearch 可以一边搜索一边高亮
:ctrl + t 可以回放上一个命令

断言库chai

chai.js断言库

目录

前端自动化测试之chai.js断言库

chai.js简介

chai.js 是一套TDD(测试驱动开发)/BDD(行为驱动开发)的断言库。包含有3个断言库支持BDD风格的expect/should和TDD风格的assert,这里主要说明expect/should库,
BDD风格说简单的就是你的测试代码更加的语义化,让你的断言可读性更好,expect/should库都支持链式调用可以在node和浏览器环境运行,可以高效的和任何js测试框架搭配使用。

**说明:**BDD,行为驱动开发(注重测试逻辑),TDD是测试驱动开发(注重输出结果)。

三大断言库的使用:

var chai = require('chai');  
var assert = chai.assert;    // Using Assert style
var expect = chai.expect;    // Using Expect style
var should = chai.should();  // Using Should style

在es6中的使用:

import { assert } from 'chai';  // Using Assert style
import { expect } from 'chai';  // Using Expect style
import { should } from 'chai';  // Using Should style
should();  // Modifies `Object.prototype`

语言链修饰符

语言链修饰符是单纯作为语言链提供以期提高断言的可读性。除非被插件改写否则它们一般不提供测试功能。主要包括如下相关的修饰符:

to
be
been
is
that
which
and
has
have
with
at
of
same

例如:可以采用任何组合修饰符的方式来编写测试用例

expect(foo).to.is.has.which.equal('bar');  
expect(goodFn).be.has.at.same.throw(Error);  

except模块的相关api

  • any/all
//any:用于检测该参数是否与实际值所对应的构造函数相匹配,在keys断言之前使用any标记(与all相反)
expect(foo).to.have.any.keys('bar', 'baz');

//all:在keys断言之前使用all标记(与any相反)
expect(foo).to.have.all.keys('bar', 'baz')
  • a(type) / .an(type): 用来断言变量类型
// 类型断言
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
expect(null).to.be.a('null');
expect(undefined).to.be.an('undefined');
expect(new Error).to.be.an('error');
expect(new Promise).to.be.a('promise');
expect(new Float32Array()).to.be.a('float32array');
  • include(value) / contains(value):Object | String | Number,包含某个内容
    include()和contains()即可作为属性类断言前缀语言链又可作为作为判断数组、字符串是否包含某值的断言使用。当作为语言链使用时,常用于key()断言之前
expect([1, 2, 3]).to.include(2);
expect('foobar').to.include('bar');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo')
  • not 跟在链式调用后面的否定断言
expect(foo).to.not.equal('bar');  
expect(goodFn).to.not.throw(Error);  
expect({ foo: 'baz' }).to.have.property('foo').and.not.equal('bar');
  • deep 用来深度比较2个对象,一般用在equal和property前面
expect(foo).to.deep.equal({ bar: 'baz' });  
expect({ foo: { bar: { baz: 'quux' } } }).to.have.deep.property('foo.bar.baz', 'quux');
  • ok 断言目标是否为真(只判断值是否为真,类似==,隐式转换)
expect('everthing').to.be.ok;  
expect(1).to.be.ok;  
expect(false).to.not.be.ok;  
  • true/.false 断言目标是否为布尔值true,false(这里与ok的区别是不进行类型转换,只能为true/false才能通过断言)
expect(true).to.be.true;  
expect(1).to.not.be.true;
expect(false).to.be.false;  
expect(0).to.not.be.false;  
  • null 断言目标为null
expect(null).to.be.null;  
expect(undefined).not.to.be.null;  
  • undefined 断言目标为undefined
expect(undefined).to.be.undefined;  
expect(null).to.not.be.undefined;
  • NaN 断言目标为NaN
expect('foo').to.is.be.NaN;
expect(4).is.be.NaN;
  • exist 断言目标存在,既不为null,也不为undefined
expect([]).to.be.empty
expect('').to.be.empty
expect({}).to.be.empty
  • arguments 断言目标是一个参数对象arguments
function(){
     expect(arg).to.be.has.arguements;
}
  • equal(value) 断言目标严格等于(===)value。另外,如果设置了deep标记,则断言目标深度等于value
expect('hello').to.equal('hello');
expect(42).to.equal(42);
  • eql(value) 断言目标深度等于value,相当于deep.equal(value)的简写
expect({ foo: 'bar' }).to.eql({ foo: 'bar' });
expect([1, 2, 3]).to.eql([1, 2, 3]);
  • above(value) 断言目标大于(>)(超过)value,也可接在length后来断言一个最小的长度。相比直接提供长度的好处是提供了更详细的错误消息
expect(50).to.be.above(12);
expect([1, 2, 3]).to.have.length.above(2);
  • least(value) 断言目标不小于(>=),也可接在length后来断言一个最小的长度。相比直接提供长度的好处是提供了更详细的错误消息
expect(23).to.be.least(12);
expect([1, 2, 3]).to.have.length.least(2);
  • below(value) 断言目标小于(<),也可接在length后来断言一个最小的长度。相比直接提供长度的好处是提供了更详细的错误消息
expect(5).to.be.below(12);
expect([1, 2, 3]).to.have.length.below(5);
  • most(value) 断言目标不大于(<=),也可接在length后来断言一个最小的长度。相比直接提供长度的好处是提供了更详细的错误消息
expect(5).to.be.most(12);
expect([1, 2, 3]).to.have.length.most(5);
  • within(star,end) 断言目标在这个范围内
expect([1, 2, 3]).to.have.length.within(2, 4);
  • length 设置.have.length标记作为比较length属性值的前缀
expect('foo').to.have.length.above(2);
expect([1, 2, 3]).to.have.length.within(2, 4);
  • lengthof() 断言目标的length属性为期望的值
expect('foo').is.lengthOf(2);
expect([1, 2, 3]).to.has.lengthOf(2, 4);
  • match(RegExp) 断言目标匹配到一个正则表达式
expect(2323232).is.to.be.match(/^\d+/);
  • string(string) 断言目标字符串包含另一个字符串
expect('foo').to.has.string('fo');
  • instanceof(constructor) 断言目标是构造函数constructor的一个实例
var Tea = function (name) { this.name = name },
  Chai = new Tea('chai');

expect(Chai).to.be.an.instanceof(Tea);
expect([1, 2, 3]).to.be.an.instanceof(Array);
  • property(name, [value]) 断言目标是否拥有某个名为name的属性,可选地如果提供了value则该属性值还需要严格等于(===)value。
    如果设置了deep标记,则可以使用点.和中括号[]来指向对象和数组中的深层属性
    name:String,属性名
    value:Mixed,可选,属性值
// 简单引用
var obj = { foo: 'bar' };
expect(obj).to.have.property('foo');
expect(pbj).to.have.property('foo', 'bar');

// 深层引用
var deepObj = {
  green: { tea: 'matcha' },
  teas: [ 'Chai', 'matcha', { tea: 'konacha' } ]
};

// 下面三个是错误的情况,具体情形有待研究而已
expect(deepObj).to.have.deep.property('green.tea', 'matcha');
expect(deepObj).to.have.deep.property('teas[1]', 'matcha');
expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha')
  • ownProperty(name) 断言目标拥有名为name的自有属性
expect('test').to.have.ownProperty('length');
  • respondTo(method) 断言目标类或对象会响应一个方法(存在这个方法)
    如果需要检查一个构造函数是否会响应一个静态方法(挂载在构造函数本身的方法),请查看itself标记
Klass.prototype.bar = function () {};
expect(Klass).to.respondTo('bar');
expect(obj).to.respondTo('bar');

Klass.baz = function () {};
expect(Klass).itself.to.respondTo('baz');
  • itself 设置itself标记,然后使用respondTo断言
function Foo () {}
Foo.bar = function () {};
Foo.prototype.baz = function () {};

expect(Foo).itself.to.respondTo('bar');
expect(Foo).itself.not.to.respond('baz');

具体代码请见

相关url文章链接:
Chai.js断言库API中文文档
前端自动化测试之chai.js断言库

Promise 对象

Promise 对象

1、Promise 的含义

Promise 是异步编程的一种解决方案,比传统的解决方案 —— 回调函数和事件 —— 更合理和更强大。它由社区最早提出和实现, ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise对象有以下两个特点。
( 1 )对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled )和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是 “ 承诺 ” ,表示其他手段无法改变。
( 2 )一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件( Event )完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

2、基本用法

实例1:基本用法

    var promise = new Promise(function(resolve, reject) {
    	// ... some code
        if (/*  异步操作成功 */){
            resolve(value);
        } else {
            reject(error);
        }
    });

实例2:只要一new Promise后就会立即执行。

    let promise = new Promise(function(resolve, reject) {
        console.log('Promise');
        resolve();
    });
    promise.then(function() {
        console.log('Resolved.');
    });
    console.log('Hi!');
    // Promise
    // Hi!
    // Resolved

实例3:下面是一个用 Promise 对象实现的 Ajax 操作的例子。(非常经典)

    var getJSON = function (url) {
        var promise = new Promise(function (resolve, reject) {
            var client = new XMLHttpRequest();
            client.open("GET", url);
            client.onreadystatechange = handler;
            client.responseType = "json";
            client.setRequestHeader("Accept", "application/json");
            client.send();
    
            function handler() {
                if (this.readyState !== 4) {
                    return;
                }
                if (this.status === 200) {
                    resolve(this.response);
                } else {
                    reject(new Error(this.statusText));
                }
            };
        });
        return promise;
    };
    
    getJSON("/posts.json").then(function (json) {
        console.log('Contents: ' + json);
    }, function (error) {
        console.error(' 出错了 ', error);
    });

3、Promise.prototype.then()

Promise 实例具有then方法,也就是说,then方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是 Resolved 状态的回调函数,第二个参数(可选)是 Rejected 状态的回调函数。
then方法返回的是一个新的 Promise 实例(注意,不是原来那个 Promise 实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

    getJSON("/posts.json").then(function(json) {
        return json.post;
    }).then(function(post) {
        // ...
    });

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个 Promise 对象(即有异步操作),这时后一个回调函数,就会等待该 Promise 对象的状态发生变化,才会被调用。

    getJSON("/post/1.json").then(function (post) {
        return getJSON(post.commentURL);
    }).then(function funcA(comments) {
        console.log("Resolved: ", comments);
    }, function funcB(err) {
        console.log("Rejected: ", err);
    });

上面代码中,第一个then方法指定的回调函数,返回的是另一个 Promise 对象。这时,第二个then方法指定的回调函数,就会等待这个新的 Promise 对象状态发生变化。如果变为 Resolved ,就调用funcA,如果状态变为 Rejected ,就调用funcB。

4、Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
实例:

    getJSON("/posts.json").then(function (posts) {
        // ...
    }).catch(function (error) {
        //  处理 getJSON  和 前一个回调函数运行时发生的错误
        console.log(' 发生错误! ', error);
    });

跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数, Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

    var someAsyncThing = function () {
        return new Promise(function (resolve, reject) {
        //  下面一行会报错,因为 x 没有声明
            resolve(x + 2);
        });
    };
    someAsyncThing().then(function () {
        console.log('everything is great');
    });

5、Promise.all()

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
var p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 对象的实例。

实例:

    Promise.all([checkLogin(),getUserInfo()]).then(([res1,res2])=>{
        console.log(`result1:${res1.result}, result2:${res2.userID}`)
    });

6、Promise.race()

Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

7、Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。

    var jsPromise = Promise.resolve($.ajax('/whatever.json'));  

8、Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。它的参数用法与Promise.resolve方法完全一致。

9、两个有用的附加方法

ES6 的 Promise API 提供的方法不是很多,有些有用的方法可以自己部署。下面介绍如何部署两个不在 ES6 之中、但很有用的方法。

9.1、done()

Promise 对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

    asyncFunc()
        .then(f1)
        .catch(r1)
        .then(f2)
        .done();

9.2、finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

    server.listen(0)
        .then(function () {
    // run test
        })
        .finally(server.stop);

10、Promise的使用

使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

    function getFoo() {
        return new Promise(function (resolve, reject) {
            resolve('foo');
        });
    }
    
    var g = function* () {
        try {
            var foo = yield getFoo();
            console.log(foo);
        } catch (e) {
            console.log(e);
        }
    };
    
    function run(generator) {
        var it = generator();
    
        function go(result) {
            if (result.done) return result.value;
            return result.value.then(function (value) {
                return go(it.next(value));
            }, function (error) {
                return go(it.throw(error));
            });
        }
    
        go(it.next());
    }
    
    run(g);

参考文章

其他Promise相关文档

[shell] 入门 - 3 字符串和数组

字符串和数组

字符串

单双引号的区别:

  • 双引号里可以有变量,单引号则原样输出;
  • 双引号里可以出现转义字符,单引号则原样输出;
  • 单引号字串中不能出现单引号。
#!/bin/bash

str1='i'
str2='love'
str3='you'

echo $str1 $str2 $str3
echo $str1$str2$str3
echo $str1,$str2,$str3

# i love you
# iloveyou
# i,love,you

获取字符串长度

#!/bin/bash/

str='i love you'

echo ${#str}

# 输出:10

截取字符串

#!/bin/bash/

str='i love you'

echo ${str:1} # 从第1个截取到末尾。注意从0开始。
echo ${str:2:2} # 从第2个截取2个。
echo ${str:0} # 全部截取。
echo ${str:-3} # 负数无效,视为0。

查找字符串

#!/bin/bash/

str="i love you"

echo `expr index "$str" l`
echo `expr index "$str" love` #最后一个参数是字符,字符串只保留首字母
echo `expr index "$str" o`
echo `expr length "$str"` #字符串长度
echo `expr substr "$str" 1 6` #从字符串中位置1开始截取6个字符。索引是从0开始的。

*拓展:expr更多关于字符串用法:

STRING : REGEXP   anchored pattern match of REGEXP in STRING

match STRING REGEXP        same as STRING : REGEXP

substr STRING POS LENGTH   #从STRING中POS位置开始截取LENGTH个字符。POS索引是从1开始的。

index STRING CHARS         #在STRING中查找字符CHARS首次出现的位置,没有找到返回0

length STRING              #字符串长度

数组

在Shell中,用括号来表示数组,数组元素用空格符号分割开。定义数组的一般形式为:
array_name=(value1 value2 ... valuen) 也可以这样
array_name=(value0 value1 value2 value3) 也可以这样

array_name=(
    value0
    value1
    value2
    value3
)

还可以这样:

array_name[0]=value0
array_name[1]=value1
array_name[2]=value2

下面来读取数组:

echo ${array_name[2]} #读取下标为2的元素
echo ${array_name[*]} #读取所有元素
echo ${array_name[@]} #读取所有元素

echo ${#array_name[*]} #获取数组长度
echo ${#array_name[@]} #获取数组长度
echo ${#array_name[1]} #获取数组中单个元素的长度

示例

字符串

str="hello"
${#str} # 读取字符串长度
echo ${str} # 读取字符串全部
echo ${str:1} # 截取字符串

数组

arr=(a1,a2,a3)
${#arr[*]} # 读取数组长度
${#arr[1]} # 读取数组某个元素长度

echo ${arr[*]} # 读取数组全部
echo ${arr[1]} # 读取数组某个元素

${#ele*} 用来读取ele元素长度属性 ${ele*} 用来读取或操作ele元素

总结

多看源码,多做项目

Proxy 和 Reflect

Proxy 和 Reflect

1、概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种 “ 元编程 ” ( meta programming ),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层 “ 拦截 ” ,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。 Proxy 这个词的原意是代理,用在这里表示由它来 “ 代理 ” 某些操作,可以译为“ 代理器 ”。
实例1:

    var obj = new Proxy({}, {
        get: function (target, key, receiver) {
            console.log(`getting ${key}!`);
            return Reflect.get(target, key, receiver);
        },
        set: function (target, key, value, receiver) {
            console.log(`setting ${key}!`);
            return Reflect.set(target, key, value, receiver);
        }
    });

上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。

    obj.count = 1
    // setting count!
    ++obj.count
    // getting count!
    // setting count!
    // 2

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
语法:var proxy = new Proxy(target, handler);
实例2:Proxy 实例也可以作为其他对象的原型对象。

    var proxy = new Proxy({}, {
        get: function(target, property) {
            return 35;
        }
    });
    let obj = Object.create(proxy);
    obj.time // 35

实例3:同一个拦截器函数,可以设置拦截多个操作。

    var handler = {
        get: function(target, name) {
            if (name === 'prototype') {
                return Object.prototype;
            }
            return 'Hello, ' + name;
        },
        apply: function(target, thisBinding, args) {
            return args[0];
        },
        construct: function(target, args) {
            return {value: args[1]};
        }
    };
    var fproxy = new Proxy(function(x, y) {
        return x + y;
    }, handler);
    fproxy(1, 2) // 1
    new fproxy(1,2) // {value: 2}
    fproxy.prototype === Object.prototype // true
    fproxy.foo // "Hello, foo"

对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。

( 1 ) get(target, propKey, receiver)
拦截对象属性的读取,比如proxy.foo和proxy['foo']。
最后一个参数receiver是一个对象,可选,参见下面Reflect.get的部分。

( 2 ) set(target, propKey, value, receiver)
拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。

( 3 ) has(target, propKey)
拦截propKey in proxy的操作,以及对象的hasOwnProperty方法,返回一个布尔值。

( 4 ) deleteProperty(target, propKey)
拦截delete proxy[propKey]的操作,返回一个布尔值。

( 5 ) ownKeys(target)
拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回对象所有自身的属性,而Object.keys()仅返回对象可遍历的属性。

( 6 ) getOwnPropertyDescriptor(target, propKey)
拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

( 7 ) defineProperty(target, propKey, propDesc)
拦截Object.defineProperty(proxy, propKey, propDesc ) 、Object.defineProperties(proxy, propDescs),返回一个布尔值。

( 8 ) preventExtensions(target)
拦截Object.preventExtensions(proxy),返回一个布尔值。

( 9 ) getPrototypeOf(target)
拦截Object.getPrototypeOf(proxy),返回一个对象。

( 10 ) isExtensible(target)
拦截Object.isExtensible(proxy),返回一个布尔值。

( 11 ) setPrototypeOf(target, proto)
拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。

( 12 ) apply(target, object, args)
拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。

( 13 ) construct(target, args)
拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

2、Proxy 实例的方法

2.1、get()

get方法用于拦截某个属性的读取操作。上文已经有一个例子,下面是另一个拦截读取操作的例子。
实例1:基本使用

    let person = {
        name: 'yanle'
    };
    
    let proxy = new Proxy(person, {
        get(target, property) {
            if(property in target){
                return console.log(target[property])
            }else{
                throw new Error
            }
        }
    });
    
    proxy.name;//   'yanle'
    proxy.age;//    抛出错误
    //实例2:实现数组读取负数的索引
    function createArray(...elements){
        let handler={
            get(target,propKey,receiver){
                let index=Number(propKey);
                if(index<0){
                    propKey=String(target.length+index)
                }
                return Reflect.get(target,propKey,receiver);
            }
        };
    
        let target=[];
        target.push(...elements);
        return new Proxy(target,handler);
    }
    
    let arr=createArray('a','b','c');
    console.log(arr[-1]);//结果为c
    //实例3:转变执行某个函数,从而实现属性的链式操作
    var pipe = (function () {
        return function (value) {
            var funcStack = [];
            var oproxy = new Proxy({} , {
                get : function (pipeObject, fnName) {
                    if (fnName === 'get') {
                        return funcStack.reduce(function (val, fn) {
                            return fn(val);
                        },value);
                    }
                    funcStack.push(window[fnName]);
                    return oproxy;
                }
            });
            return oproxy;
        }
    }());
    var double = n => n * 2;
    var pow = n => n * n;
    var reverseInt = n => n.toString().split("").reverse().join("") | 0;
    pipe(3).double.pow.reverseInt.get; // 63
    //实例4:实现一个生成各种DOM节点的通用函数
    const dom = new Proxy({}, {
        get(target, property) {
            return function(attrs = {}, ...children) {
                const el = document.createElement(property);
                for (let prop of Object.keys(attrs)) {
                    el.setAttribute(prop, attrs[prop]);
                }
                for (let child of children) {
                    if (typeof child === 'string') {
                        child = document.createTextNode(child);
                    }
                    el.appendChild(child);
                }
                return el;
            }
        }
    });
    const el = dom.div({},
        'Hello, my name is ',
        dom.a({href: '//example.com'}, 'Mark'),
        '. I like:',
        dom.ul({},
            dom.li({}, 'The web'),
            dom.li({}, 'Food'),
            dom.li({}, '…actually that\'s it')
        )
    );
    document.body.appendChild(el);

2.2、set()

set方法用来拦截某个属性的赋值操作。

    //实例1:赛选一个age 不大于两百的整数
    let  validator={
        set:function(obj,prop,value){
            if(prop==='age'){
                if(!Number.isInteger(value)){
                    throw new Error;
                }
    
                if(value>200){
                    throw new RangeError('年龄不能大于200');
                }
            }
            //对于age 以外的属性,直接保存
            obj[prop]=value;
        }
    };
    
    let person=new Proxy({},validator);
    person.age=300;
    //实例2:get和set结合,方式内部属性不被外部改写
    var handler={
        get(target,key){
            invariant(key,'get');
            return target[key]
        },
        set(target,key,value){
            invariant(key,'set');
            return true
        }
    };
    
    function invariant(key,action){
        if(key[0]==='_'){
            throw new Error('内部属性不允许串改');
        }
    }
    var target={};
    var proxy=new Proxy(target,handler);
    proxy.name='yanle';

2.3、apply();

apply方法拦截函数的调用、 call 和 apply 操作。

    //基本语法
    var handler = {
        apply (target, ctx, args) {
            return Reflect.apply(...arguments);
        }
    };

apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

    //实例1:基本使用
    var target=function(){
        return '我是一个目标'
    };
    
    var handler={
        apply(){
            return '我是一个proxy'
        }
    };
    var p =new Proxy(target,handler);
    
    console.log(p());//我是一个proxy

2.4、has()

has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in算符。

    //实例1:下面的例子使用has方法隐藏某些属性,不被in运算符发现。
    var handler = {
        has (target, key) {
            if (key[0] === '_') {
                return false;
            }
            return key in target;
        }
    };
    var target = { _prop: 'foo', prop: 'foo' };
    var proxy = new Proxy(target, handler);
    console.log('_prop' in proxy) // false

如果原对象的属性名的第一个字符是下划线,proxy.has就会返回false,从而不会被in运算符发现。
has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。由于for...in操作内部也会用到HasProperty操作,所以has方法在for...in循环时也会生效。

2.5、construct()

construct方法用于拦截new命令,下面是拦截对象的写法。
基础语法:

    var handler = {
        construct (target, args, newTarget) {
            return new target(...args);
        }
    };

construct方法可以接受两个参数。target : 目标对象,args:构建函数的参数对象

    //实例 1:基本使用
    var p = new Proxy(function() {}, {
        construct: function(target, args) {
            console.log('called: ' + args.join(', '));
            return { value: args[0] * 10 };
        }
    });
    new p(1).value
    // "called: 1"
    // 10
    //实例2:construct方法返回的必须是一个对象,否则会报错。
    var p = new Proxy(function() {}, {
        construct: function(target, argumentsList) {
            return 1;
        }
    });
    new p() //  报错

2.6、deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

2.7、defineProperty()

defineProperty方法拦截了Object.defineProperty操作。

    //实例1:基本使用
    var handler = {
        defineProperty (target, key, descriptor) {
            return false;
        }
    };
    var target = {};
    var proxy = new Proxy(target, handler);
    proxy.foo = 'bar'
    // TypeError: proxy defineProperty handler returned false for property '"foo"'

2.8、getOwnPropertyDescriptor()

getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor,返回一个属性描述对象或者undefined。

2.9、getPrototypeOf()

getPrototypeOf方法主要用来拦截Object.getPrototypeOf()运算符,以及其他一些操作。

2.10、isExtensible()

isExtensible方法拦截Object.isExtensible操作。

2.11、ownKeys()

ownKeys方法用来拦截Object.keys()操作。

    //实例1:拦截第一个字符为下划线的属性名。
    let target={
        _bar: 'foo',
        _prop: 'bar',
        prop: 'baz'
    };
    
    let handler={
        ownKeys(target){
            return Reflect.ownKeys(target).filter(key=>key[0]!=='_');
        }
    };
    
    let proxy=new Proxy(target,handler);
    for(let key of Object.keys(proxy)){
        console.log(target[key])
    };//结果  'baz'

2.12、preventExtensions()

preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值。
这个方法有一个限制,只有当Object.isExtensible(proxy)为false(即不可扩展)时,proxy.preventExtensions才能返回true,否则会报错。

2.13、setPrototypeOf()

setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。

3、Proxy.revocable()

Proxy.revocable 方法返回一个可取消的 Proxy 实例。
实例:Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

    let target={};
    let handler={};
    let {proxy,revoke}=Proxy.revocable(target,handler);
    
    proxy.foo=123;
    console.log(proxy);
    
    revoke();//执行了次函数,就终止了proxy
    proxy.foo;//报错

4、Reflect概述 (有点儿深)

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API 。

5 Reflect 对象的方法

Reflect对象的方法清单如下,共 13 个。

Reflect.apply(target,thisArg,args)
Reflect.construct(target,args)
Reflect.get(target,name,receiver)
Reflect.set(target,name,value,receiver)
Reflect.defineProperty(target,name,desc)
Reflect.deleteProperty(target,name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

其他相关概念可以看这里 传送门->

javascript单元测试框架mochajs详解

目录

前端测试库Mocha 和 Jest

Mocha作为可以说是使用最多的库,官方是主打灵活、简单的一个测试库,提供给开发者的只有一个基础测试结构。所以在使用的时候, 需要外置一个断言库, 例如chai、assert,以及覆盖库istanbulMochai 拥有一些列的插件机制, 可以轻易对Mochai 进行很方便的扩展和增强。

但是说到前端单元测试,就不得不说到JestJest安装配置简单,非常容易上手。内置Istanbul,可以查看到测试覆盖率,完美的支持React组件化测试。

Jest既简单又强大,内置支持以下功能:

  • 灵活的配置:比如,可以用文件名通配符来检测测试文件。
  • 测试的事前步骤(Setup)和事后步骤(Teardown),同时也包括测试范围。
  • 匹配表达式(Matchers):能使用期望expect句法来验证不同的内容。
  • 测试异步代码:支持承诺(promise)数据类型和异步等待async / await功能。
  • 模拟函数:可以修改或监查某个函数的行为。
  • 手动模拟:测试代码时可以忽略模块的依存关系。
  • 虚拟计时:帮助控制时间推移。

这篇文章先介绍mocha的使用, 以后再介绍Jest的使用

开始第一个示例

$ npm install mocha
$ mkdir test
$ $EDITOR test/test.js     # 或者使用你喜欢的编辑器打开

简单的可以编写如下的测试代码:

const expect = require('chai').expect;
describe('#main', function () {
    it('must be array', function () {
        expect([1, 2, 3]).to.be.an.instanceof(Array);
    });
    it('should array length equal 3', function () {
        expect([1,2,3]).length.eq(4);
    })
});

然后运行测试代码:
./node_modules/mocha/bin/mocha就可以得到如下的测试结果:

  #main
    ✓ must be array
    1) should array length equal 3


  1 passing (74ms)
  1 failing

  1) #main
       should array length equal 3:
       AssertionError: expected [ 1, 2, 3 ] to equal 4

同样可以在 package.json 里面设置一个测试启动脚本

"scripts": {
    "test": "mocha"
  }

然后执行 npm run test;就可以得到上面一样的结果了。 默认查找的是test文件夹下面的test.js文件为主启动文件, 如果希望修改启动文件路径, mocha 后面添加路径就可以了 mocha [path]

主要断言库推荐

mocha本身是不带断言库的,可以安装三方断言库, 主要有以下断言库:

  • should.js
  • expect.js
  • chai
  • assert
    其中assert 是node自带的模块, 可以直接就用,但是在下推荐用chai, 非常强大非常好用。
    在后面的文章中, 在下会继续对chai做详细介绍。

异步测试 done() 与超时时间timeout

先来聊聊超时时间timeout问题
Mocha默认每个测试用例最多执行 2000 毫秒,如果到时没有得到结果,就报错。对于涉及异步操作的测试用例,这个时间往往是不够的,需要用 -t或--timeout 参数指定超时门槛。
例如启动的时候这样启动测试用例: mocha -t 5000 timeout.test.js

上面这种设置超时时间是全局的, 针对所有的it测试用例单元。 但是还有一种使用得更多的方式, 就是在每个测试单元, 或者describe 里面设置单独的超时时间。

describe('a suite of tests', function() {
    this.timeout(500);

    it('should take less than 500ms', function(done){
        setTimeout(done, 300);
    });

    it('should take less than 500ms as well', function(done){
        setTimeout(done, 250);
    });

    it('should take less than 500ms', function(done){
        this.timeout(500);
        setTimeout(done, 300);
    });
})

需要注意的地方:
这个地方都是用到了this指针, Mocha传递箭头函数是不好的,由于this的词法作用域的问题,箭头函数是不能够访问mocha的上下文的。例如,由于箭头函数本身的机制,下面的代码会失败。

describe('my suite', () => {
    it('my test', () => {
        // should set the timeout of this test to 1000 ms; instead will fail
        this.timeout(1000);
        assert.ok(true);
    });
});

所以一定要慎用箭头函数, 当我们不用到上下文this的时候, 箭头函数是可以用的, 但是尽量少用箭头函数, 除非迫不得已。 因为箭头函数会导致this上下文的混淆。

slow:
测试中,我们更多的会关注失败的测试用例和耗时较长的用例。那么问题来了,怎么算耗时过长呢?不同的地方可能有不同的要求。Mocha提供了 slow 函数来解决这个事情。当一个用例耗时超过一定值后,Mocha 会在reportor中明显地标记出来。

describe('For compare with Test slow', function() {
    this.slow(100);
    // 标记耗时过长
    it('It would warning', function(done) {
        var callback = function() {
            console.log("------");
            done();
        };
        setTimeout(callback, 200);
    });
});

使用命令行执行测试文件之后, 可以得到以下的测试结果:

  For compare with Test slow
------
    ✓ It would warning (208ms)


  1 passing (264ms)

发现当耗时接近 slow() 函数设定的值的一半时,时间值开始被标记为黄色。 超过slow设定值得时候, 会直接标红。

异步测试 done():
先看一个异步的示例, 测试用例里面,有一个done函数。it块执行的时候,传入一个done参数,当测试结束的时候,必须显式调用这个函数,告诉Mocha测试结束了。否则,Mocha就无法知道,测试是否结束,会一直等到超时报错。

it('测试应该5000毫秒后结束', function(done) {
    var x = true;
    var f = function() {
        x = false;
        expect(x).to.be.not.ok;
        done(); // 通知Mocha测试结束
    };
    setTimeout(f, 4000);
});

其中更加常用的一个示例是做接口api测试的时候, 一定会设计到大量的异步操作。 一个简单的示例如下:

it('异步请求应该返回一个对象', function(done){
    request
        .get('https://api.github.com')
        .end(function(err, res){
            expect(res).to.be.an('object');
            done();
        });
});

对于 Promise 的异步测试, 因为mocha内部是默认支持Promise的,允许直接返回Promise,等到它的状态改变,再执行断言,而不用显式调用done方法。请看下面示例:

it('异步请求应该返回一个对象', function() {
    return fetch('https://api.github.com')
        .then(function(res) {
            return res.json();
        }).then(function(json) {
            expect(json).to.be.an('object');
        });
});

如果node版本7.6+可以使用 async/await, 也可以这样来写异步代码:

describe('#find()', function() {
    it('responds with matching records', async function() {
        const users = await db.find({ type: 'User' });
        users.should.have.length(3);
    });
});

时间钩子函数

Mocha在describe块之中,提供测试用例的四个钩子: before()、after()、beforeEach()和afterEach() 。它们会在指定时间执行。

  • before 在本区块的所有测试用例之前执行
  • after 在本区块的所有测试用例之后执行
  • beforeEach 在本区块的每个测试用例之前执行
  • afterEach 在本区块的每个测试用例之后执行
const assert = require('assert');
describe('Array', function() {
    describe('#indexOf()', function() {
        before(function () {
            console.log('before')
        });
        beforeEach(function() {
            console.log('beforeEach')
        });
        it('should return -1 when the value is not present', function() {
            assert.equal(-1, [1, 2, 3].indexOf(4));
            console.log('it');
        });
        it('just is a console.log', function () {
            console.log('123');
        });
        afterEach(function () {
            console.log('afterEach')
        });
        after(function () {
            console.log('after')
        });
    })
});

输入结果如下:

  Array
    #indexOf()
before
beforeEach
it
      ✓ should return -1 when the value is not present
afterEach
beforeEach
123
      ✓ just is a console.log
afterEach
after

  2 passing (61ms)

这里还有一个需要注意的地方: 如果 beforeEach/before 这样类型的生命中周期中, 有异步操作, 需要等异步操作结束之后, 在进行后续的测试用例。例如下面的示例, 我们是执行测试是运行成功的结果。

describe('异步 beforeEach 示例', function() {
    var foo = false;

    beforeEach(function(done) {
        setTimeout(function() {
            foo = true;
            done();
        }, 50);
    });

    it('全局变量异步修改应该成功', function() {
        expect(foo).to.be.equal(true);
    });
});

测试用例管理 only(), skip()

only()
大型项目有很多测试用例。有时,我们希望只运行其中的几个,这时可以用 only 方法。describe块和it块都允许调用only方法,可以让mocha只测试此用例集合或者用例单元。其他的测试就测试集合或者用例单元就直接跳过不测试了。

describe('Array', function() {
    describe('#indexOf', function() {
        it.only('should return -1 unless preset', function () {
            // 执行测试用例单元
        });
        it('should return the index when present', function () {
            // 不执行
        })
    })
})

在mochav3.0.0版本及以后,.only()可以被定义多次来定义一系列的测试子集。也就是说可以让mocha只测试only标记的用例集合或者用例单元, 其他的测试用例集合或者用例单元直接跳过。

skip()
和only()方法相反,.skip()方法可以用于跳过某些测试测试集合和测试用例。所有被跳过的用例都会被标记为pending用例,在报告中也会以pending用例显示。

it.skip('任何数加0应该等于自身', function() {
  expect(add(1, 0)).to.be.equal(1);
});

上面的测试用例就会直接跳过

有些时候,测试用例需要某些特定的环境或者一些特殊的配置,但我们事先是无法确定的。这个时候,我们可以使用this.skip() 根据条件在运行的时候跳过某些测试用例。

it('should only test in the correct environment', function () {
    if(/* check the environment */) {
        // make assertions
    } else {
        this.skip()
    }
})

这个测试在报告中会以pending状态呈现。为了避免测试逻辑混乱,在调用skip函数之后,就不要再在用例函数或after钩子中执行更多的逻辑了。

我们也可以在异步的测试用例和钩子函数中使用.skip()来跳过多个测试用例或者测试集合。

配置文件mocha.opts

在服务端运行的时候,mocha会去加载test目录下的mocha.opts文件,来读取mocha配置项。这个配置文件中的每一行代表一项配置。如果运行mocha命令的时候,带上的配置参数与这个配置文件中的配置冲突的话,以命令中的为准。
这里介绍一些常见的配置命令行:

添加mocha插件
如果我们的模块是es6语法编写的, 但是mocha默认是不认识es6语法的, 这个时候我们就要借助babel相关插件。以及如果希望加入其它的插件, 我们就可以用下面的方式来添加插件。

--require intelli-espower-loader
--require babel-core/register
--require babel-polyfill

同事需要安装对应的插件:

yarn add intelli-espower-loader babel-core babel-polyfill --dev
或者
npm install intelli-espower-loader babel-core babel-polyfill --save-dev

通过reporter生成漂亮的测试报告
--reporter

--reporter 参数用来指定测试报告的格式,默认是 spec。使用 mochawesome 模块,可以生成漂亮的 HTML 格式的模块。

$ npm install mochawesome -D   // 或者 yarn add mochawesone --dev
$ ../node_modules/.bin/mocha --reporter mochawesome

同样的这个配置我们也可以写入配置文件mocha.opts中去, 这样可以让测试变得更加简单可控

--require intelli-espower-loader
--require babel-core/register
--require babel-polyfill
--reporter mochawesome

上面代码中,mocha命令使用了项目内安装的版本,而不是全局安装的版本,因为mochawesome模块是安装在项目内的。然后,测试结果报告就在mochaawesome-reports子目录生成。

TypeScript测试

首先必须要安装如下几个安装包:

{
  "devDependencies": {
    "@types/chai": "^4.1.6",
    "@types/mocha": "^5.2.5",
    "chai": "^4.2.0",
    "mocha": "^5.2.0",
    "ts-node": "^7.0.1",
    "typescript": "^3.1.1"
  }
}

配置文件mocha.opts的导入

--recursive
--require ./node_modules/ts-node/register
--ui bdd
--timeout 60000
--watch-extensions ts
test/**/*.ts
  • 测试代码的编写
import 'mocha'
describe('main', function() {
    it('should ', function () {
        console.log(123);
    });
})

后续操作和JS的测试是一模一样的了

mocha的命令的基本选项

Options:

    -h, --help                  输出帮助信息
    -V, --version               输出mocha的版本号
    -A, --async-only            强制所有的测试用例必须使用callback或者返回一个promise的格式来确定异步的正确性
    -c, --colors                在报告中显示颜色
    -C, --no-colors             在报告中禁止显示颜色
    -g, --growl                 在桌面上显示测试报告的结果
    -O, --reporter-options <k=v,k2=v2,...>  设置报告的基本选项
    -R, --reporter <name>       指定测试报告的格式
    -S, --sort                  对测试文件进行排序
    -b, --bail                  在第一个测试没有通过的时候就停止执行后面所有的测试
    -d, --debug                 启用node的debugger功能
    -g, --grep <pattern>        用于搜索测试用例的名称,然后只执行匹配的测试用例
    -f, --fgrep <string>        只执行测试用例的名称中含有string的测试用例
    -gc, --expose-gc            展示垃圾回收的log内容
    -i, --invert                只运行不符合条件的测试用例,必须和--grep或--fgrep之一同时运行
    -r, --require <name>        require指定模块
    -s, --slow <ms>             指定slow的时间,单位是ms,默认是75ms
    -t, --timeout <ms>          指定超时时间,单位是ms,默认是200ms
    -u, --ui <name>             指定user-interface (bdd|tdd|exports)中的一种
    -w, --watch                 用来监视指定的测试脚本。只要测试脚本有变化,就会自动运行Mocha
    --check-leaks               检测全局变量造成的内存泄漏问题
    --full-trace                展示完整的错误栈信息
    --compilers <ext>:<module>,...  使用给定的模块来编译文件
    --debug-brk                 启用nodejs的debug模式
    --es_staging                启用全部staged特性
    --harmony<_classes,_generators,...>     all node --harmony* flags are available
    --preserve-symlinks                     告知模块加载器在解析和缓存模块的时候,保留模块本身的软链接信息
    --icu-data-dir                          include ICU data
    --inline-diffs              用内联的方式展示actual/expected之间的不同
    --inspect                   激活chrome浏览器的控制台
    --interfaces                展示所有可用的接口
    --no-deprecation            不展示warning信息
    --no-exit                   require a clean shutdown of the event loop: mocha will not call process.exit
    --no-timeouts               禁用超时功能
    --opts <path>               定义option文件路径 
    --perf-basic-prof           启用linux的分析功能
    --prof                      打印出统计分析信息
    --recursive                 包含子目录中的测试用例
    --reporters                 展示所有可以使用的测试报告的名称
    --retries <times>           设置对于失败的测试用例的尝试的次数
    --throw-deprecation         无论任何时候使用过时的函数都抛出一个异常
    --trace                     追踪函数的调用过程
    --trace-deprecation         展示追踪错误栈
    --use_strict                强制使用严格模式
    --watch-extensions <ext>,... --watch监控的扩展 
    --delay                     异步测试用例的延迟时间

补充知识点儿

在mocha测试文件运行的路径下面可以建立这么一个文件,可以放置对mocha的一些配置命令行
mocha.opts

配置项如下:

--require intelli-espower-loader
--require babel-core/register
--growl
--recursive
--reporter spec

其中intelli-espower-loader 是一个解释性增强的插件

参考文章

Generator 函数

Generator 函数 (重要)

1、简介

1.1、基本概念

执行 Generator 函数会返回一个遍历器对象,也就是说, Generator 函数除了状态机,还是一个遍历器对象生成函数。
返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上, Generator 函数是一个普通函数,但是有两个特征。
一是,function关键字与函数名之间有一个星号;
二是,函数体内部使用yield语句,定义不同的内部状态( yield 语句在英语里的意思就是 “ 产出 ” )。
实例1:基本使用

    function* helloWorldGenerator() {
        yield 'hello';
        yield 'world';
        return 'ending';
    }
    
    var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield语句 “hello” 和 “world” ,即该函数有三个状态: hello , world 和 return 语句(结束执行)。
然后, Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。
不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,
而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象( Iterator Object )。
下一步,必须调用遍历器对象的 next 方法,使得指针移向下一个状态。
也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。
换言之, Generator 函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。
实例1执行结果

    hw.next()
    // { value: 'hello', done: false }
    hw.next()
    // { value: 'world', done: false }
    hw.next()
    // { value: 'ending', done: true }
    hw.next()
    // { value: undefined, done: true }

第一次调用, Generator 函数开始执行,直到遇到第一个yield语句为止。next方法返回一个对象,它的value属性就是当前yield语句的值hello ,done属性的值 false ,表示遍历还没有结束。
第二次调用, Generator 函数从上次yield语句停下的地方,一直执行到下一个yield语句。next方法返回的对象的value属性就是当前yield语句的值world ,done属性的值 false ,表示遍历还没有结束。
第三次调用, Generator 函数从上次yield语句停下的地方,一直执行到return语句(如果没有 return 语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为 undefined ),done属性的值 true ,表示遍历已经结束。
第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为 undefined ,done属性为 true 。以后再调用next方法,返回的都是这个值。
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

1.2、yield 语句

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。

  • ( 1 )遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
  • ( 2 )下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。
  • ( 3 )如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
  • ( 4 )如果该函数没有return语句,则返回的对象的value属性值为undefined。

实例2:

function* gen() {
    yield 123 + 456;
}

2、next 方法的参数

yield句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
实例1:

    function* f() {
        for(var i=0; true; i++) {
            var reset = yield i;
            if(reset) { i = -1; }
        }
    }
    
    var g = f();
    g.next() // { value: 0, done: false }
    g.next() // { value: 1, done: false }
    g.next(true) // { value: 0, done: false }

实例2:来一个复杂而全面的例子

    function* foo(x) {
        var y = 2 * (yield (x + 1));
        var z = yield (y / 3);
        return (x + y + z);
    }
    
    var a = foo(5);
    a.next() //4、通信类
    a.next() // Object{value:NaN, done:false}
    a.next() // Object{value:NaN, done:true}
    
    var b = foo(5);
    b.next() //4、通信类
    b.next(12) // { value:8, done:false }
    b.next(13) // { value:42, done:true }

3、for...of 循环

for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法。
实例1:最基础使用

    function* foo() {
        yield 1;
        yield 2;
        yield 3;
        yield 4;
        yield 5;
        return 6;
    }
    
    for (let v of foo()) {
        console.log(v);
    }
    // 1 2 3 4 5

上面代码使用for...of循环,依次显示 5 个yield语句的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的 6 ,不包括在for...of循环之中。

利用for...of循环,可以写出遍历任意对象( object )的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。
实例2:

    function* objectEntries(obj) {
        let propKeys = Reflect.ownKeys(obj);
        for (let propKey of propKeys) {
            yield [propKey, obj[propKey]];
        }
    }
    
    let jane = { first: 'Jane', last: 'Doe' };
    
    for (let [key, value] of objectEntries(jane)) {
        console.log(`${key}: ${value}`);
    }
    // first: Jane
    // last: Doe

实例3:除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。

    function* numbers() {
        yield 1;
        yield 2;
        return 3;
        yield 4;
    }
    
    //  扩展运算符
    [...numbers()]; // [1, 2]
    
    // Array.form  方法
    Array.from(numbers()); // [1, 2]
    
    //  解构赋值
    let [x, y] = numbers();
    x // 1
    y // 2
    
    // for...of  循环
    for (let n of numbers()) {
        console.log(n)
    }
    // 1
    // 2

4、Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
实例:基本使用

    var g = function* () {
        try {
            yield;
        } catch (e) {
            console.log(' 内部捕获 ', e);
        }
    };
    
    var i = g();
    i.next();
    
    try {
        i.throw('a');
        i.throw('b');
    } catch (e) {
        console.log(' 外部捕获 ', e);
    }
    //  内部捕获 a
    //  外部捕获 b

5、Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
实例1:基本使用

    function* gen() {
        yield 1;
        yield 2;
        yield 3;
    }
    
    var g = gen();
    g.next() // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true }
    g.next() // { value: undefined, done: true }

6、yield* 语句

如果在 Generater 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。
实例1:基本使用

    function* foo() {
        yield 'a';
        yield 'b';
    }
    
    function* bar() {
        yield 'x';
        foo();
        yield 'y';
    }
    
    for (let v of bar()) {
        console.log(v);
    }
    // "x"
    // "y"

7、作为对象属性的 Generator 函数

如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
实例:基础用法

    let obj = {
        * myGeneratorMethod() {
            //···
        }
    };

8、Generator 函数的 this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法。
实例1:

    function* F() {
        this.a = 1;
        yield this.b = 2;
        yield this.c = 3;
    }
    
    var obj = {};
    var f = F.call(obj);
    
    f.next(); // Object {value: 2, done: false}
    f.next(); // Object {value: 3, done: false}
    f.next(); // Object {value: undefined, done: true}
    obj.a // 1
    obj.b // 2
    obj.c // 3

9、含义

9.1 Generator 与状态机

实例:

    var clock = function*() {
        while (true) {
            console.log('Tick!');
            yield;
            console.log('Tock!');
            yield;
        }
    };

Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。

10 应用

Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。

10.1、异步操作的同步化表达

Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 语句里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 语句下面,反正要等到调用 next 方法时再执行。所以, Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
实例1:基本使用方式

    function* loadUI() {
        showLoadingScreen();
        yield loadUIDataAsynchronously();
        hideLoadingScreen();
    }
    
    var loader = loadUI();
    //  加载 UI
    loader.next();
    //  卸载 UI
    loader.next();

实例2:Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。

    function* main() {
        var result = yield request("http://some.url");
        var resp = JSON.parse(result);
        console.log(resp.value);
    }
    
    function request(url) {
        makeAjaxCall(url, function (response) {
            it.next(response);
        });
    }
    
    var it = main();
    it.next();

实例3:通过 Generator 函数逐行读取文本文件。

    function* numbers() {
        let file = new FileReader("numbers.txt");
        try {
            while (!file.eof) {
                yield parseInt(file.readLine(), 10);
            }
        } finally {
            file.close();
        }
    }

10.2、控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
实例1:

    step1(function (value1) {
        step2(value1, function (value2) {
            step3(value2, function (value3) {
                step4(value3, function (value4) {
                    // Do something with value4
                });
            });
        });
    });
    
    //采用 Promise 改写上面的代码。
    Promise.resolve(step1)
        .then(step2)
        .then(step3)
        .then(step4)
        .then(function (value4) {
            // Do something with value4
        }, function (error) {
            // Handle any error from step1 through step4
        })
        .done();
    
    //上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。 Generator 函数可以进一步改善代码运行流程。
    function* longRunningTask(value1) {
        try {
            var value2 = yield step1(value1);
            var value3 = yield step2(value2);
            var value4 = yield step3(value3);
            var value5 = yield step4(value4);
            // Do something with value4
        } catch (e) {
            // Handle any error from step1 through step4
        }
    }
    
    //然后,使用一个函数,按次序自动执行所有步骤。
    scheduler(longRunningTask(initialValue));
    function scheduler(task) {
        var taskObj = task.next(task.value);
        //如果 Generator 函数未结束,就继续调用
        if (!taskObj.done) {
            task.value = taskObj.value;
            scheduler(task);
        }
    }

10.3、部署 Iterator 接口

利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

    function* iterEntries(obj) {
        let keys = Object.keys(obj);
        for (let i=0; i < keys.length; i++) {
            let key = keys[i];
            yield [key, obj[key]];
        }
    }
    
    let myObj = { foo: 3, bar: 7 };
    for (let [key, value] of iterEntries(myObj)) {
        console.log(key, value);
    }
    // foo 3
    // bar 7

DOM 和 BOM 手册

DOM 和 BOM 和 you dont need jquery

目录

DOM 部分

document对象

Document 对象常用的属性和方法

属性 / 方法 描述
document.activeElement 返回当前获取焦点元素
document.addEventListener() 向文档添加句柄
document.anchors 返回对文档中所有 Anchor 对象的引用。anchors集合返回了当前页面的所有超级链接数组 。不过需要注意的是,只有当a标签添加了name以后才能拿现实统计到
document.baseURI 返回文档的绝对基础 URI
document.body 返回文档的body元素
document.open() 打开一个流,以收集来自任何 document.write() 或 document.writeln() 方法的输出。
document.close() 关闭用 document.open() 方法打开的输出流,并显示选定的数据。
document.cookie 设置或返回与当前文档有关的所有 cookie。
document.createAttribute() 创建一个属性节点
document.createComment() createComment() 方法可创建注释节点。
document.createDocumentFragment() 创建空的 DocumentFragment 对象,并返回此对象。
document.createElement() 创建元素节点。
document.createTextNode() 创建文本节点。
document.doctype 返回与文档相关的文档类型声明 (DTD)。
document.documentElement 返回文档的根节点,感觉并不重要
document.documentMode 返回用于通过浏览器渲染文档的模式, documentMode 是 IE 浏览器特定属性,在IE8及之后的IE版本都支持该属性。
document.documentURI 设置或返回文档的位置
document.domain 返回当前文档的域名。
document.embeds 返回文档中所有嵌入的内容(embed)集合
document.forms 返回对文档中所有 Form 对象引用。
document.getElementsByClassName() 返回文档中所有指定类名的元素集合,作为 NodeList 对象。
document.getElementById() 返回对拥有指定 id 的第一个对象的引用。
document.getElementsByName() 返回带有指定名称的对象集合。
document.getElementsByTagName() 返回带有指定标签名的对象集合。
document.images 返回对文档中所有 Image 对象引用。
document.implementation 返回处理该文档的 DOMImplementation 对象。
document.importNode() 把一个节点从另一个文档复制到该文档以便应用。
document.inputEncoding 返回用于文档的编码方式(在解析时)。
document.lastModified 返回文档被最后修改的日期和时间。
document.links 返回对文档中所有 Area 和 Link 对象引用。
document.normalize() 删除空文本节点,并连接相邻节点
document.normalizeDocument() 删除空文本节点,并连接相邻节点的, 主流浏览器不支持
document.querySelector() 返回文档中匹配指定的CSS选择器的第一元素
document.querySelectorAll() document.querySelectorAll() 是 HTML5中引入的新方法,返回文档中匹配的CSS选择器的所有元素节点列表
document.readyState 返回文档状态 (载入中……)
document.referrer 返回载入当前文档的文档的 URL。
document.removeEventListener() 移除文档中的事件句柄(由 addEventListener() 方法添加)
document.renameNode() 重命名元素或者属性节点。
document.scripts 返回页面中所有脚本的集合。
document.strictErrorChecking 设置或返回是否强制进行错误检查。
document.title 返回当前文档的标题。
document.URL 返回文档完整的URL
document.write() 向文档写 HTML 表达式 或 JavaScript 代码。
document.writeln() 等同于 write() 方法,不同的是在每个表达式之后写一个换行符。

2、HTML DOM 元素对象

属性 / 方法 描述
element.accessKey 设置或返回accesskey一个元素
element.addEventListener() 向指定元素添加事件句柄
element.appendChild() 为元素添加一个新的子元素
element.attributes 返回一个元素的属性数组
element.childNodes 返回元素的一个子节点的数组
element.classList 返回元素的类名,作为 DOMTokenList 对象。
element.className 设置或返回元素的class属性
element.clientHeight 在页面上返回内容的可视高度(不包括边框,边距或滚动条)
element.clientWidth 在页面上返回内容的可视宽度(不包括边框,边距或滚动条)
element.cloneNode() 克隆某个元素
element.compareDocumentPosition() 比较两个元素的文档位置。
element.contentEditable 设置或返回元素的内容是否可编辑
element.dir 设置或返回一个元素中的文本方向
element.firstChild 返回元素的第一个子节点
element.focus() 设置文档或元素获取焦点
element.getAttribute() 返回指定元素的属性值
element.getAttributeNode() 返回指定属性节点
element.getElementsByTagName() 返回指定标签名的所有子元素集合。
element. getElementsByClassName() 返回文档中所有指定类名的元素集合,作为 NodeList 对象。
element.getFeature() 返回指定特征的执行APIs对象。
element.getUserData() 返回一个元素中关联键值的对象。
element.hasAttribute() 如果元素中存在指定的属性返回 true,否则返回false。
element.hasAttributes() 如果元素有任何属性返回true,否则返回false。
element.hasChildNodes() 返回一个元素是否具有任何子元素
element.hasFocus() 返回布尔值,检测文档或元素是否获取焦点
element.id 设置或者返回元素的 id。
element.innerHTML 设置或者返回元素的内容。
element.insertBefore() 现有的子元素之前插入一个新的子元素
element.insertAdjacentHTML(position, text); 'beforebegin': Before the element itself;'afterbegin': Just inside the element, before its first child;'beforeend': Just inside the element, after its last child;'afterend': After the element itself.
element.insertAdjacentText(position, element); following strings:'beforebegin': Before the element itself;'afterbegin': Just inside the element, before its first child;'beforeend': Just inside the element, after its last child;'afterend': After the element itself.
element.isContentEditable 如果元素内容可编辑返回 true,否则返回false
element.isDefaultNamespace() 如果指定了namespaceURI 返回 true,否则返回 false。
element.isEqualNode() 检查两个元素是否相等
element.isSameNode() 检查两个元素所有有相同节点。
element.isSupported() 如果在元素中支持指定特征返回 true。
element.lang 设置或者返回一个元素的语言。
element.lastChild 返回的最后一个子元素
element.namespaceURI 返回命名空间的 URI。
element.nextSibling 返回该元素紧跟的一个节点
element.nodeName 返回元素的标记名(大写)
element.nodeType 返回元素的节点类型
element.nodeValue 返回元素的节点值
element.normalize() 使得此成为一个"normal"的形式,其中只有结构(如元素,注释,处理指令,CDATA节和实体引用)隔开Text节点,即元素(包括属性)下面的所有文本节点,既没有相邻的文本节点也没有空的文本节点
element.offsetHeight 返回,任何一个元素的高度包括边框和填充,但不是边距
element.offsetWidth 返回元素的宽度,包括边框和填充,但不是边距
element.offsetLeft 返回当前元素的相对水平偏移位置的偏移容器
element.offsetParent 返回元素的偏移容器
element.offsetTop 返回当前元素的相对垂直偏移位置的偏移容器
element.ownerDocument 返回元素的根元素(文档对象)
element.parentNode 返回元素的父节点
element.previousSibling 返回某个元素紧接之前元素
element.querySelector() 返回匹配指定 CSS 选择器元素的第一个子元素
document.querySelectorAll() 返回匹配指定 CSS 选择器元素的所有子元素节点列表
element.removeAttribute() 从元素中删除指定的属性
element.removeAttributeNode() 删除指定属性节点并返回移除后的节点。
element.removeChild() 删除一个子元素
element.removeEventListener() 移除由 addEventListener() 方法添加的事件句柄
element.replaceChild() 替换一个子元素
element.scrollHeight 返回整个元素的高度(包括带滚动条的隐蔽的地方)
element.scrollLeft 返回当前视图中的实际元素的左边缘和左边缘之间的距离
element.scrollTop 返回当前视图中的实际元素的顶部边缘和顶部边缘之间的距离
element.scrollWidth 返回元素的整个宽度(包括带滚动条的隐蔽的地方)
element.setAttribute() 设置或者改变指定属性并指定值。
element.setAttributeNode() 设置或者改变指定属性节点。
element.setIdAttribute()
element.setIdAttributeNode()
element.setUserData() 在元素中为指定键值关联对象。
element.style 设置或返回元素的样式属性
element.tabIndex 设置或返回元素的标签顺序。
element.tagName 作为一个字符串返回某个元素的标记名(大写)
element.textContent 设置或返回一个节点和它的文本内容
element.title 设置或返回元素的title属性
element.toString() 一个元素转换成字符串
nodelist.item() 返回某个元素基于文档树的索引
nodelist.length 返回节点列表的节点数目。

03、DOM 属性 对象

属性 / 方法 描述
attr.isId 如果属性是 ID 类型,则 isId 属性返回 true,否则返回 false。
attr.name 返回属性名称
attr.value 设置或者返回属性值
attr.specified 如果属性被指定返回 true ,否则返回 false
nodemap.getNamedItem() 从节点列表中返回的指定属性节点。
nodemap.item() 返回节点列表中处于指定索引号的节点。
nodemap.length 返回节点列表的节点数目。
nodemap.removeNamedItem() 删除指定属性节点
nodemap.setNamedItem() 设置指定属性节点(通过名称)

04、HTML DOM 事件

鼠标事件

属性 / 方法 描述 DOM类型
onclick 当用户点击某个对象时调用的事件句柄。 2
oncontextmenu 在用户点击鼠标右键打开上下文菜单时触发
ondblclick 当用户双击某个对象时调用的事件句柄。 2
onmousedown 鼠标按钮被按下。 2
onmouseenter 当鼠标指针移动到元素上时触发。 2
onmouseleave 当鼠标指针移出元素时触发 2
onmousemove 鼠标被移动。 2
onmouseover 鼠标移到某元素之上。 2
onmouseout 鼠标从某元素移开。 2
onmouseup 鼠标按键被松开。 2

键盘事件

属性 / 方法 描述 DOM类型
onkeydown 某个键盘按键被按下。 2
onkeypress 某个键盘按键被按下并松开。 2
onkeyup 某个键盘按键被松开。 2

框架/对象(Frame/Object)事件

属性 / 方法 描述 DOM类型
onabort 图像的加载被中断。 ( ) 2
onbeforeunload 该事件在即将离开页面(刷新或关闭)时触发 2
onerror 在加载文档或图像时发生错误。 ( , 和 )
onhashchange 该事件在当前 URL 的锚部分发生修改时触发。
onload 一张页面或一幅图像完成加载。 2
onpageshow 该事件在用户访问页面时触发
onpagehide 该事件在用户离开当前网页跳转到另外一个页面时触发
onresize 窗口或框架被重新调整大小。 监听屏幕大小变化 2
onscroll 当文档被滚动时发生的事件。 2
onunload 用户退出页面。 ( 和 ) 2

表单事件

属性 / 方法 描述 DOM类型
onblur 元素失去焦点时触发 2
onchange 该事件在表单元素的内容改变时触发( , , , 和 <textarea>) 2
onfocus 元素获取焦点时触发 2
onfocusin 元素即将获取焦点时触发 2
onfocusout 元素即将失去焦点时触发 2
oninput 元素获取用户输入时触发 3
onreset 表单重置时触发 2
onsearch 用户向搜索域输入文本时触发 ( <input="search">)
onselect 用户选取文本时触发 ( 和 <textarea>) 2
onsubmit 表单提交时触发 2

剪贴板事件

属性 / 方法 描述 DOM类型
oncopy 该事件在用户拷贝元素内容时触发
oncut 该事件在用户剪切元素内容时触发
onpaste 该事件在用户粘贴元素内容时触发

打印事件

属性 / 方法 描述 DOM类型
onafterprint 该事件在页面已经开始打印,或者打印窗口已经关闭时触发
onbeforeprint 该事件在页面即将开始打印时触发

拖动事件

属性 / 方法 描述 DOM类型
ondrag 该事件在元素正在拖动时触发
ondragend 该事件在用户完成元素的拖动时触发
ondragenter 该事件在拖动的元素进入放置目标时触发
ondragleave 该事件在拖动元素离开放置目标时触发
ondragover 该事件在拖动元素在放置目标上时触发
ondragstart 该事件在用户开始拖动元素时触发
ondrop 该事件在拖动元素放置在目标区域时触发

多媒体(Media)事件

属性 / 方法 描述 DOM类型
onabort 事件在视频/音频(audio/video)终止加载时触发。
oncanplay 事件在用户可以开始播放视频/音频(audio/video)时触发。
oncanplaythrough 事件在视频/音频(audio/video)可以正常播放且无需停顿和缓冲时触发。
ondurationchange 事件在视频/音频(audio/video)的时长发生变化时触发。
onemptied 当期播放列表为空时触发
onended 事件在视频/音频(audio/video)播放结束时触发。
onerror 事件在视频/音频(audio/video)数据加载期间发生错误时触发。
onloadeddata 事件在浏览器加载视频/音频(audio/video)当前帧时触发触发。
onloadedmetadata 事件在指定视频/音频(audio/video)的元数据加载后触发。
onloadstart 事件在浏览器开始寻找指定视频/音频(audio/video)触发。
onpause 事件在视频/音频(audio/video)暂停时触发。
onplay 事件在视频/音频(audio/video)开始播放时触发。
onplaying 事件在视频/音频(audio/video)暂停或者在缓冲后准备重新开始播放时触发。
onprogress 事件在浏览器下载指定的视频/音频(audio/video)时触发。
onratechange 事件在视频/音频(audio/video)的播放速度发送改变时触发。
onseeked 事件在用户重新定位视频/音频(audio/video)的播放位置后触发。
onseeking 事件在用户开始重新定位视频/音频(audio/video)时触发。
onstalled 事件在浏览器获取媒体数据,但媒体数据不可用时触发。
onsuspend 事件在浏览器读取媒体数据中止时触发。
ontimeupdate 事件在当前的播放位置发送改变时触发。
onvolumechange 事件在音量发生改变时触发。
onwaiting 事件在视频由于要播放下一帧而需要缓冲时触发。

动画事件

属性 / 方法 描述 DOM类型
animationend 该事件在 CSS 动画结束播放时触发
animationiteration 该事件在 CSS 动画重复播放时触发
animationstart 该事件在 CSS 动画开始播放时触发

过渡事件

属性 / 方法 描述 DOM类型
transitionend 该事件在 CSS 完成过渡后触发。

其他事件

属性 / 方法 描述 DOM类型
onmessage 该事件通过或者从对象(WebSocket, Web Worker, Event Source 或者子 frame 或父窗口)接收到消息时触发
onmousewheel 已废弃。 使用 onwheel 事件替代
ononline 该事件在浏览器开始在线工作时触发。
onoffline 该事件在浏览器开始离线工作时触发。
onpopstate 该事件在窗口的浏览历史(history 对象)发生改变时触发。
onshow 该事件当 元素在上下文菜单显示时触发
onstorage 该事件在 Web Storage(HTML 5 Web 存储)更新时触发
ontoggle 该事件在用户打开或关闭
元素时触发
onwheel 该事件在鼠标滚轮在元素上下滚动时触发

事件对象

常量

属性 / 方法 描述 DOM类型
CAPTURING-PHASE 当前事件阶段为捕获阶段(1) 1
AT-TARGET 当前事件是目标阶段,在评估目标事件(1) 2
BUBBLING-PHASE 当前的事件为冒泡阶段 (3) 3

属性

属性 / 方法 描述 DOM类型
bubbles 返回布尔值,指示事件是否是起泡事件类型。 2
cancelable 返回布尔值,指示事件是否可拥可取消的默认动作。 2
currentTarget 返回其事件监听器触发该事件的元素。 2
eventPhase 返回事件传播的当前阶段。 2
target 返回触发此事件的元素(事件的目标节点)。 2
timeStamp 返回事件生成的日期和时间。 2
type 返回当前 Event 对象表示的事件的名称。 2

方法

属性 / 方法 描述 DOM类型
initEvent() 初始化新创建的 Event 对象的属性。 2
preventDefault() 通知浏览器不要执行与事件关联的默认动作。 2
stopPropagation() 不再派发事件。 2

目标事件对象

方法

属性 / 方法 描述 DOM类型
addEventListener() 允许在目标事件中注册监听事件(IE8 = attachEvent()) 2
dispatchEvent() 允许发送事件到监听器上 (IE8 = fireEvent()) 2
removeEventListener() 运行一次注册在事件目标上的监听事件(IE8 = detachEvent()) 2

事件监听对象

方法

属性 / 方法 描述 DOM类型
handleEvent() 把任意对象注册为事件处理程序 2

文档事件对象

方法

属性 / 方法 描述 DOM类型
createEvent() 2

鼠标/键盘事件对象
方法

属性 / 方法 描述 DOM类型
altKey 返回当事件被触发时,"ALT" 是否被按下。 2
button 返回当事件被触发时,哪个鼠标按钮被点击。 2
clientX 返回当事件被触发时,鼠标指针的水平坐标。 2
clientY 返回当事件被触发时,鼠标指针的垂直坐标。 2
ctrlKey 返回当事件被触发时,"CTRL" 键是否被按下。 2
Location 返回按键在设备上的位置 3
charCode 返回onkeypress事件触发键值的字母代码。 2
key 在按下按键时返回按键的标识符。 3
keyCode 返回onkeypress事件触发的键的值的字符代码,或者 onkeydown 或 onkeyup 事件的键的代码。 2
which 返回onkeypress事件触发的键的值的字符代码,或者 onkeydown 或 onkeyup 事件的键的代码。 2
metaKey 返回当事件被触发时,"meta" 键是否被按下。 2
relatedTarget 返回与事件的目标节点相关的节点。 2
screenX 返回当某个事件被触发时,鼠标指针的水平坐标。 2
screenY 返回当某个事件被触发时,鼠标指针的垂直坐标。 2
shiftKey 返回当事件被触发时,"SHIFT" 键是否被按下。 2

方法

属性 / 方法 描述 DOM类型
initMouseEvent() 初始化鼠标事件对象的值 2
initKeyboardEvent() 初始化键盘事件对象的值 3

05、Console 对象

方法 描述
console.assert(expression, message) 如果断言为 false,则在信息到控制台输出错误信息。
clear() 清除控制台上的信息。
count() 记录 count() 调用次数,一般用于计数。
error() 输出错误信息到控制台
group() 在控制台创建一个信息分组。 一个完整的信息分组以 console.group() 开始,console.groupEnd() 结束
groupCollapsed() 在控制台创建一个信息分组。 类似 console.group() ,但它默认是折叠的。
groupEnd() 设置当前信息分组结束
info() 控制台输出一条信息
log() 控制台输出一条信息
console.table(tabledata[Array 或 Object], tablecolumns[Array]) 以表格形式显示数据
time() 计时器,开始计时间,与 timeEnd() 联合使用,用于算出一个操作所花费的准确时间。
timeEnd() 计时结束
trace() 显示当前执行的代码在堆栈中的调用路径。
warn() 输出警告信息,信息最前面加一个黄色三角,表示警告

BOM 部分

01、window对象

Window 对象属性

属性 描述
closed 返回窗口是否已被关闭。
defaultStatus 设置或返回窗口状态栏中的默认文本。
document 对 Document 对象的只读引用。(请参阅对象)
frames 返回窗口中所有命名的框架。该集合是 Window 对象的数组,每个 Window 对象在窗口中含有一个框架。
history 对 History 对象的只读引用。请参数 History 对象。
innerHeight 返回窗口的文档显示区的高度。
innerWidth 返回窗口的文档显示区的宽度。
localStorage 在浏览器中存储 key/value 对。没有过期时间。
length 设置或返回窗口中的框架数量。
location 用于窗口或框架的 Location 对象。请参阅 Location 对象。
name 设置或返回窗口的名称。
navigator 对 Navigator 对象的只读引用。请参数 Navigator 对象。
opener 返回对创建此窗口的窗口的引用。
outerHeight 返回窗口的外部高度,包含工具条与滚动条。
outerWidth 返回窗口的外部宽度,包含工具条与滚动条。
pageXOffset 设置或返回当前页面相对于窗口显示区左上角的 X 位置。
pageYOffset 设置或返回当前页面相对于窗口显示区左上角的 Y 位置。
parent 返回父窗口。
screen 对 Screen 对象的只读引用。请参数 Screen 对象。
screenLeft 返回相对于屏幕窗口的x坐标
screenTop 返回相对于屏幕窗口的y坐标
screenX 返回相对于屏幕窗口的x坐标
sessionStorage 在浏览器中存储 key/value 对。 在关闭窗口或标签页之后将会删除这些数据。
screenY 返回相对于屏幕窗口的y坐标
self 返回对当前窗口的引用。等价于 Window 属性。
status 设置窗口状态栏的文本。
top 返回最顶层的父窗口。

Navigator 对象

Navigator 对象包含有关浏览器的信息。

属性 说明
appCodeName 返回浏览器的代码名
appName 返回浏览器的名称
appVersion 返回浏览器的平台和版本信息
cookieEnabled 返回指明浏览器中是否启用 cookie 的布尔值
platform 返回运行浏览器的操作系统平台
userAgent 返回由客户机发送服务器的user-agent 头部的值

Screen 对象

Screen 对象包含有关客户端显示屏幕的信息。

属性 说明
availHeight 返回屏幕的高度(不包括Windows任务栏)
availWidth 返回屏幕的宽度(不包括Windows任务栏)
colorDepth 返回目标设备或缓冲器上的调色板的比特深度
height 返回屏幕的总高度
pixelDepth 返回屏幕的颜色分辨率(每象素的位数)
width 返回屏幕的总宽度

History 对象

History 对象包含用户(在浏览器窗口中)访问过的 URL。

方法 说明
back() 加载 history 列表中的前一个 URL
forward() 加载 history 列表中的下一个 URL
go() 加载 history 列表中的某个具体页面

Location 对象

属性 描述
hash 返回一个URL的锚部分
host 返回一个URL的主机名和端口
hostname 返回URL的主机名
href 返回完整的URL
pathname 返回的URL路径名。
port 返回一个URL服务器使用的端口号
protocol 返回一个URL协议
search 返回一个URL的查询部分
方法 说明
assign() 载入一个新的文档
reload() 重新载入当前文档
replace() 用新的文档替换当前文档

补充

  • element.matches
    let result = element.matches(selectorString);
    result 的值为 true 或 false.
    selectorString 是个css选择器字符串.
    例子:
<ul id="birds">
  <li>Orange-winged parrot</li>
  <li class="endangered">Philippine eagle</li>
  <li>Great white pelican</li>
</ul>

<script type="text/javascript">
  var birds = document.getElementsByTagName('li');
  for (var i = 0; i < birds.length; i++) {
    if (birds[i].matches('.endangered')) {
      console.log('The ' + birds[i].textContent + ' is endangered!');
    }
  }
</script>

[docker] 入门 - 04 docker的持久化存储与数据共享

docker的持久化存储与数据共享

01、基础概念

如果我们在container 写数据, 那么只限于这个container, 删除就没有了。
有一个这样的需求, 数据库是放在某一个容器里面的, 希望数据不会随着容器的删除而丢失, 希望容器数据库保存的数据可以共享。

Docker持久化数据方案

  • 基于本地文件系统的Volume
  • 基于plugin的Volume, 支持第三方的存储方案, 比如NAS, aws

主要是为第一种方案为主。

Vagrant 插件

如果要把文件弄到虚拟机里面, 最简单的办法就是直接 git clone xxxx 就可以了。

还有一个办法就是使用vagrant 的插件
vagrant plugin list 这个就可以拿到本地 vagrant 的插件有哪些。
通过 vagrant plugin install XXXX 就可以安装查价了。

vagrant plugin install vagrant-scp 去安装插件, 安装这个插件之后, 就可以本地目录拷贝到 vagrant 虚拟机上面了。

插件使用: vagrant scp [local path] [virtualbox-name]:/home/vagrant/labs 就可以把本地目录推送到虚拟机里面去了

02、数据持久化:Data Volume

实际上在创建数据的容器的时候, 就会缠上一个 volume , 用来存放数据库产生的数据

sudo docker run -d --name=mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql 直接启动mysql 容器
sudo docker logs mysql 可以查看日志
sudo docker volume ls 就可以看到刚才创建docker的时候, 就也创建了 volume
sodu docker volume rm [volume ID] 删除创建的volume
sudo docker volume inspect [volume id] 可以看看具体的volume 信息

[vagrant@docker-host ~]$ docker volume inspect c224d6da05a13fe09dd87a75afdf887c203acf72ec3eab545392af0b4f24a39a
[
    {
        "CreatedAt": "2019-06-08T14:20:16Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/c224d6da05a13fe09dd87a75afdf887c203acf72ec3eab545392af0b4f24a39a/_data",
        "Name": "c224d6da05a13fe09dd87a75afdf887c203acf72ec3eab545392af0b4f24a39a",
        "Options": {},
        "Scope": "local"
    }
]

有这样一个信息 "Mountpoint": "/var/lib/docker/volumes/c224d6da05a13fe09dd87a75afdf887c203acf72ec3eab545392af0b4f24a39a/_data", 这个是产生的数据, 就放置的位置。 这个地方是方式在本地位置的。
如果创建两个 mysql 容器, 如果我们删除 创建的mysql 容器, 就会发现一个情况,我们的volume 实际上还是在的。 但是有一个问题,就是volume 的名字很不友好, 是随机生成的。

sudo docker run -d -v mysql:/var/lib/mysql --name=mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql 给volume起别名
如果创建别的数据库的时候, 需要使用到我们的这个volume 的情况, 只需要创建的时候,用一样的别名就OK了

03、数据持久化:Bind Mouting

运行容器的时候, 指定本地目录和容器目录数据存放的一个映射关系。 两个地方地方的容器是同步更新的。

sudo docker run -d -v $(pwd):/var/lib/mysql --name=mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql
使用这种方式的前提条件是, mac 要能够映射到 虚拟机。

对象的扩展

对象的扩展

1、属性的简洁表达方式:

ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
实例1:

    var foo = 'bar';
    var baz = {foo};
    baz // {foo: "bar"}
    
    //  等同于
    var baz = {foo: foo};

实例2:

    function f(x, y) {
        return {x, y};
    }
    //  等同于
    function f(x, y) {
        return {x: x, y: y};
    }
    f(1, 2) // Object {x: 1, y: 2}

实例3:

    var birth = '2000/01/01';
    var Person = {
        name: ' 张三 ',
        
    // 等同于 birth: birth
        birth,
        
    //  等同于 hello: function ()...
        hello() { console.log(' 我的名字是 ', this.name); }
    };

实例4:使用实例

    var ms = {};
    
    function getItem (key) {
        return key in ms ? ms[key] : null;
    }
    function setItem (key, value) {
        ms[key] = value;
    }
    function clear () {
        ms = {};
    }
    
    module.exports = { getItem, setItem, clear };

2、属性名表达式

实例1:

    let propKey = 'foo';
    let obj = {
        [propKey]: true,
        ['a' + 'bc']: 123
    };

3、方法的 name 属性

    var person = {
        sayName() {
            console.log(this.name);
        },
        get firstName() {
            return "Nicholas";
        }
    };
    person.sayName.name // "sayName"
    person.firstName.name // "get firstName"

4、Object.is()

ES6 提出 “Same-value equality” (同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符( === )的行为基本一致。
实例1:

    Object.is('foo', 'foo')
    // true
    
    Object.is({}, {})
    // false

5、Object.assign()

5.1、基本用法

方法用于对象的合并,将源对象( source )的所有可枚举属性,复制到目标对象( target )。
Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
示例1:

    var target = { a: 1, b: 1 };
    var source1 = { b: 2, c: 2 };
    var source2 = { c: 3 };
    Object.assign(target, source1, source2);
    target // {a:1, b:2, c:3}

如果该参数不是对象,则会先转成对象,然后返回。如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。

5.2、注意点

5.2.1、Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
示例1:Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

    var obj1 = {a: {b: 1}};
    var obj2 = Object.assign({}, obj1);
    
    obj1.a.b = 2;
    obj2.a.b // 2

5.2.2、一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。
示例2:

    var target = { a: { b: 'c', d: 'e' } }
    var source = { a: { b: 'hello' } }
    
    Object.assign(target, source)
    // { a: { b: 'hello' } }

5.2.3、Object.assign可以用来处理数组,但是会把数组视为对象。(最好不要用作处理数组)
示例3:Object.assign把数组视为属性名为 0 、 1 、 2 的对象,因此目标数组的 0 号属性4覆盖了原数组的 0 号属性1

    Object.assign([1, 2, 3], [4, 5])
    // [4, 5, 3]

5.3、常见用途

5.3.1、位对象添加属性

    class Point {
        constructor(x, y) {
            Object.assign(this, {x, y});
        }
    }

5.3.2、为对象添加方法

    Object.assign(SomeClass.prototype, {
        someMethod(arg1, arg2) {
            ···
        },
        anotherMethod() {
            ···
        }
    });

5.3.3、克隆对象
示例1:

    function clone(origin) {
        return Object.assign({}, origin);
    }

不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
示例2:

    function clone(origin) {
        let originProto = Object.getPrototypeOf(origin);
        return Object.assign(Object.create(originProto), origin);
    }

5.3.4、合并多个对象 - 最基础使用

5.3.5、为属性指定默认值

    const DEFAULTS = {
        logLevel: 0,
        outputFormat: 'html'
    };
    
    function processContent(options) {
        let options = Object.assign({}, DEFAULTS, options);
    }

5.4、属性的可枚举性

Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象
示例1:

    let obj = { foo: 123 };
    let descriptor= Object.getOwnPropertyDescriptor(obj, 'foo');
    // {
    // value: 123,
    // writable: true,
    // enumerable: true,
    // configurable: true
    // }

5.5、属性的遍历

( 1 ) for...in
for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
( 2 ) Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)。
( 3 ) Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)。
( 4 ) Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性。
( 5 ) Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管是属性名是 Symbol 或字符串,也不管是否可枚举。

6、proto 属性,Object.setPrototypeOf(),Object.getPrototypeOf()

6.1、proto 属性

__proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象。
实例1:

    // es6 的写法
    var obj = {
        method: function() { ... }
    };
    obj.__proto__ = someOtherObj;
    
    // es5 的写法
    var obj = Object.create(someOtherObj);
    obj.method = function() { ... };

无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

6.2、 Object.setPrototypeOf()

Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象。
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
var o = Object.setPrototypeOf({}, null);
实例1:

    let proto = {};
    let obj = { x: 10 };
    Object.setPrototypeOf(obj, proto);
    proto.y = 20;
    proto.z = 40;
    obj.x // 10
    obj.y // 20
    obj.z // 40

6.3、 Object.getPrototypeOf()

该方法与 setPrototypeOf 方法配套,用于读取一个对象的 prototype 对象。
语法格式:Object.getPrototypeOf(obj);
实例1:

    function Rectangle() {
    }
    var rec = new Rectangle();
    Object.getPrototypeOf(rec) === Rectangle.prototype
    // true

6.4、Object.values(),Object.entries()

6.4.1、Object.keys()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键名。
实例1:

    var obj = { foo: "bar", baz: 42 };
    Object.keys(obj)
    // ["foo", "baz"]

6.4.2、Object.values()
Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键值。Object.values只返回对象自身的可遍历属性。
实例1:

    var obj = { foo: "bar", baz: 42 };
    Object.values(obj)
    // ["bar", 42]

6.4.3、Object.entries
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键值对数组。
实例1:

    var obj = { foo: 'bar', baz: 42 };
    Object.entries(obj)
    // [ ["foo", "bar"], ["baz", 42] ]

实例2:Object.entries方法的一个用处是,将对象转为真正的Map结构。

    var obj = { foo: 'bar', baz: 42 };
    var map = new Map(Object.entries(obj));
    map // Map { foo: "bar", baz: 42 }

7、对象扩展符

7.1、Rest 解构赋值

实例1:

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1
    y // 2
    z // { a: 3, b: 4 }

7.2、扩展运算符

扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
实例1:

    let z = { a: 3, b: 4 };
    let n = { ...z };
    n // { a: 3, b: 4 }

实例2:扩展运算符可以用于合并两个对象。

    let ab = { ...a, ...b };
    //  等同于
    let ab = Object.assign({}, a, b);

8、Object.getOwnPropertyDescriptors()

返回某个对象属性的描述对象( descriptor )。主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。
实例1:

    var obj = { p: 'a' };
    Object.getOwnPropertyDescriptor(obj, 'p')
    // Object { value: "a",
    // writable: true,
    // enumerable: true,
    // configurable: true
    // }

实例2:Object.getOwnPropertyDescriptors方法配合Object.defineProperties方法,就可以实现正确拷贝。

    const source = {
        set foo(value) {
            console.log(value);
        }
    };
    const target2 = {};
    Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
    Object.getOwnPropertyDescriptor(target2, 'foo')
    // { get: undefined,
    // set: [Function: foo],
    // enumerable: true,
    // configurable: true }

实例3:对上面的代码精简,逻辑提炼

    const shallowMerge = (target, source) => Object.defineProperties(
        target,
        Object.getOwnPropertyDescriptors(source)
    );

实例4:配合Object.create方法,将对象属性克隆到一个新对象。这属于浅拷贝。

    const clone = Object.create(
        Object.getPrototypeOf(obj),
        Object.getOwnPropertyDescriptors(obj)
    );


    //  或者
    const shallowClone = (obj) => Object.create(
        Object.getPrototypeOf(obj),
        Object.getOwnPropertyDescriptors(obj)
    );

实例5:Object.getOwnPropertyDescriptors方法可以实现,一个对象继承另一个对象。

    //以前,继承另一个对象,常常写成下面这样。
    const obj = {
        __proto__: prot,
        foo: 123,
    };
    
    //如果去除__proto__,上面代码就要改成下面这样。
    const obj = Object.create(prot);
    obj.foo = 123;
    //  或者
    const obj = Object.assign(
        Object.create(prot),
        {
            foo: 123,
        }
    );
    
    //有了Object.getOwnPropertyDescriptors,我们就有了另一种写法。
    const obj = Object.create(
        prot,
        Object.getOwnPropertyDescriptors({
            foo: 123,
        })
    );

[shell] 入门 - 4 条件控制

提交控制

if语句

if [[ expression ]]
then
   Statement(s) to be executed if expression is true
fi

有三种 if ... else 语句:

if ... fi 语句
if ... else ... fi 语句
if ... elif ... else ... fi 语句

示例

#!/bin/bash/

a=10
b=20
if [ $a == $b ]
then 
	echo "a is equal to b"
elif [ $a -gt $b ]
then
	echo "a is greater to b"
else
	echo "a is less to b"
fi

if ... else 语句也经常与 test 命令结合使用

#!/bin/bash/

a=10
b=20
if test $a == $b 
then 
	echo "a is equal to b"
else
	echo "a is not equal to b"
fi

分支控制:case语句

case ... esac 与其他语言中的 switch ... case 语句类似,是一种多分枝选择结构。
语法示例:

#!/bin/bash/

grade="B"

case $grade in 
	"A") echo "Very Good!";;
	"B") echo "Good!";;
	"C") echo "Come On!";;
	*) 
		echo "You Must Try!"
		echo "Sorry!";;
esac

比较全面的例子:

#!/bin/bash
option="${1}"
case ${option} in
   "-f") FILE="${2}"
      echo "File name is $FILE"
      ;;
   "-d") DIR="${2}"
      echo "Dir name is $DIR"
      ;;
   *) 
      echo "`basename ${0}`:usage: [-f file] | [-d directory]"
      exit 1 # Command to come out of the program with status 1
      ;;
esac

运行

./test.sh -f index.html
File name is index.html

下面结合getopts命令介绍下一个经典的例子:从命令行读取参数。

#!/bin/sh
usage()
{
    echo "Usage: $0 -s [start|stop|reload|restart] -e [online|test]"
    exit 1
}

if [ -z $1 ]; then
    usage
fi

while getopts 's:e:h' OPT; do
    case $OPT in
        s) cmd="$OPTARG";;
        e) env="$OPTARG";;
        h) usage;;
        ?) usage;;
    esac
done

echo $cmd
echo $env

运行示例: sh run.sh -s start -e test

for循环

语法

for 变量 in 列表
do
    command1
    command2
    ...
    commandN
done

示例:

#!/bin/bash/

for value in 1 2 3 4 5
do 
	echo "The value is $value"
done

示例2:遍历文件目录

#!/bin/bash
for FILE in *
do
   echo $FILE
done

示例3: 遍历文件内容: city.txt

beijing
tianjin
shanghai

shell

#!/bin/bash

citys=`cat city.txt`
for city in $citys
do
   echo $city
done

while循环

语法:

while command
do
   Statement(s) to be executed if command is true
done

示例:

#!/bin/bash

c=0;
while [ $c -lt 3 ]
do
	echo "Value c is $c"
	c=`expr $c + 1`
done

跳出循环

使用 break 和 continue 来跳出循环。

break:

#!/bin/bash

i=0
while [ $i -lt 5 ]
do
	i=`expr $i + 1`

	if [ $i == 3 ]
		then
			break
	fi
	echo -e $i
done

continue:
continue命令与break命令类似,只有一点差别,它不会跳出所有循环,仅仅跳出当前循环。

#!/bin/bash

i=0
while [ $i -lt 5 ]
do
	i=`expr $i + 1`

	if [ $i == 3 ]
		then
			continue
	fi
	echo -e $i
	
done

字符串的扩展

字符串的扩展

1、字符串的遍历接口 for...of循环遍历

实例1:

    for (let codePoint of 'foo') {
        console.log(codePoint)
    }
    // "f"
    // "o"
    // "o"

2、includes(), startsWith(), endsWith()

includes() :返回布尔值,表示是否找到了参数字符串。
startsWith() :返回布尔值,表示参数字符串是否在源字符串的头部。
endsWith() :返回布尔值,表示参数字符串是否在源字符串的尾部。
这三个方法都支持第二个参数,表示开始搜索的位置。
实例:

    var s = 'Hello world!';
    s.startsWith('world', 6) // true
    s.endsWith('Hello', 5) // true
    s.includes('Hello', 6) // false

3、repeat()

实例:repeat方法返回一个新字符串,表示将原字符串重复n次。

    let str1='x'.repeat(3) // "xxx"
    let str2='hello'.repeat(2) // "hellohello"
    let str3='na'.repeat(0) // ""

4、padStart() , padEnd()

ES7 推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart用于头部补全,padEnd用于尾部补全。
实例1:第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。

    'x'.padStart(5, 'ab') // 'ababx'
    'x'.padStart(4, 'ab') // 'abax'
    'x'.padEnd(5, 'ab') // 'xabab'
    'x'.padEnd(4, 'ab') // 'xaba'

实例2:如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。

    'xxx'.padStart(2, 'ab') // 'xxx'
    'xxx'.padEnd(2, 'ab') // 'xxx'

实例3:如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。

    'abc'.padStart(10, '0123456789')
    // '0123456abc'

用途1:padStart的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。

    '1'.padStart(10, '0') // "0000000001"
    '12'.padStart(10, '0') // "0000000012"
    '123456'.padStart(10, '0') // "0000123456"

用途2:另一个用途是提示字符串格式。

    '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
    '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

5、 模板字符串

实例1:基本使用

    $('#result').append(`
        There are <b>${basket.count}</b> items
        in your basket, <em>${basket.onSale}</em>
        are on sale!
    `);

实例2:所有模板字符串的空格和换行,都是被保留的,比如

    标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

        $('#list').html(`
            <ul>
                <li>first</li>
                <li>second</li>
            </ul>
        `.trim());

    实例3:模板字符串中嵌入变量,需要将变量名写在${}之中。大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。

        var x = 1;
        var y = 2;
        `${x} + ${y} = ${x + y}`
        // "1 + 2 = 3"
            
        `${x} + ${y * 2} = ${x + y * 2}`
        // "1 + 4 = 5"
        
        var obj = {x: 1, y: 2};
        `${obj.x + obj.y}`
        // 3

[设计模式] 06-架构型设计模式

第六篇、架构型设计模式

目录

第三十五章-同步模块模式

描述

模块化
将复杂的系统分解成高内聚、低耦合的模块, 使系统开发变得可控、可维护、可扩展, 提高模块的重复使用率。

同步模块模式:
请求发出去之后, 无论模块是否存在, 立即执行后续的逻辑, 实现模块开发中对模块的立即使用。

实际场景

场景一:
开发导航添加新消息提示引导, 但是别人还在修改导航, 这个时候, 就形成了一种开发排队的现象。
开发一个模块管理核心。

/*
* 模块化开发就是讲复杂的系统分解为高内聚,低耦合的模块。
* 每个工程师都可以去开发自己的模块实现复杂的系统可控, 可维护, 可扩展。 模块相互之间可以调用
* 要点: 首先要有一个模块管理器, 管理模块的创建和调度
* 模块调动: 调用分为两类, 一类同步模块调用的实现, 第二类是一步模块的实现
* */
// 模块管理对象F
class F {
    /**
     * 创建模块
     * @param str
     * @param fn
     * @returns {*}
     */
    static define(str, fn) {
        let parts = str.split('.'),
            old = this,
            parent = this,
            i = 0,
            len = 0;                // i 是模块成绩, len是模块层级长度
        // 如果第一个模块是管理模块器, 则移除
        if (parts[0] === 'F') {
            parts = parts.slice(1);
        }
        // 屏蔽对define与module模块
        if (parts[0] === 'define' || parts[0] === 'module') {
            return false;
        }

        // 遍历路由模块并且定义每层模块
        for (len = parts.length; i < len; i++) {
            // 如果父模块中不存在当前模块
            if (typeof parent[parts[i]] === 'undefined') {
                // 申明当前模块
                parent[parts[i]] = {};
            }
            // 缓存下一层的祖父模块
            old = parent;
            // 缓存下一层的父级模块
            parent = parent[parts[i]];
        }
        // 如果给定模块方法则定义该模块方法
        if (fn) {
            // 此时 i 等于 parts.length , 所以要减一
            old[parts[--i]] = fn();
        }
        return this;
    };

    // 使用模块
    static module() {
        let args = Array.prototype.slice.call(arguments),               // 参数转为数组
            fn = args.pop(),                // 获取执行的函数
            parts = args[0] && args[0] instanceof Array ? args[0] : args,
            modules = [],               // 依赖模块列表
            modIDs = [],                // 模块路由
            i = 0,                      // 依赖路由
            ilen = parts.length,        // 依赖模块长度
            parent, j, jlen;            // 分别是福模块, 模块路由层级索引, 模块路由层级长度

        // 遍历依赖模块
        while (i < ilen) {
            if(typeof parts[i] === 'string') {
                parent = this;
                modIDs = parts[i].replace(/^F\./, '').split('.');
                // 遍历模块路由层级
                for(j = 0, jlen = modIDs.length; j< jlen; j++) {
                    // 重置父模块
                    parent = parent[modIDs[j]] || false;
                }
                // 将模块添加到依赖模块列表中去。
                modules.push(parent);
            } else {
                // 直接头添加依赖模块列表中
                modules.push(parts[i]);
            }
            // 获取下一个依赖
            i++;
        }
        fn.apply(null, modules);
    }
}

代码示例: 场景1-排队开发的场景

第三十六章-异步模块模式

描述

模块化
将复杂的系统分解成高内聚、低耦合的模块, 使系统开发变得可控、可维护、可扩展, 提高模块的重复使用率。

异步模块模式:
请求发出去之后, 继续执行其他业务逻辑, 知道模块加载完成执行后续的逻辑, 实际模块开发中对模块加载完成后引用。

实际场景

场景一
对于加载的文件模块调用, 及时加载这个文件还是调用不到的。

/*
* 上面的module方法里面, 有两个方法我们还没有定义的, 一个是loadModule方法, 还有一个是 setModule方法。
* 我们先实现loadModule, loadModule 又分为三种情况
* 1、模块已经加载过, 我们要区分文件已经加载完成还是正在加载中
* 2、文件没有加载完成, 我们要将加载完成回调函数缓存入模块加载完成回调函数容器中。
* 3、一俩模块对应的文件未被要求加载过, 那么我们要加载该文件, 并且将该依赖模块的初始化信息写入模块缓存器中
* */
let getUrl = function (moduleName) {
    return String(moduleName).replace(/\.js$/g, '') + '.js';
};
loadScript = function (src) {
    // 创建script
    let _script = document.createElement('script');
    _script.type = 'type/JavaScript';
    _script.charset = 'UTF-8';
    _script.async = src;
    _script.src = src;
    document.getElementsByTagName('head')[0].appendChild(_script);
};
let moduleCache = {},
    setModule = function (moduleName, params, callback) {
        let _module, fn;
        if (moduleCache[moduleName]) {
            _module = moduleCache[moduleName];
            _module.status = 'loaded';
            _module.exports = callback ? callback.apply(_module, params) : null;
            while (fn = _module.onload.shift()) {
                fn(_module.exports);
            }
        } else {
            callback && callback.apply(null, params);
        }
    };
let loadModule = function (moduleName, callback) {
    // 依赖模块
    let _module;
    if (moduleCache[moduleName]) {
        _module = moduleCache[moduleName];
        if (_module.satus === 'loaded') {
            setTimeout(callback(_module.exports), 0);
        } else {
            _module.onload.push(callback);
        }
    } else {
        moduleCache[moduleName] = {
            moduleName: moduleName,
            status: 'loading',
            exports: null,
            onload: [callback]
        };
        loadScript(getUrl(moduleName));
    }
};

/*
* 创建和调度模块
* */
F.module = function (url, modDeps, modCallBack) {
    let args = Array.prototype.call(arguments),
        callback = args.pop(),
        deps = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : [],
        url = arugs.length ? args.pop() : null,
        params = [],                // 依赖模块列表
        depsCount = 0,
        i = 0,
        len;
    if (len = deps.length) {
        // 遍历依赖模块
        while (i < len) {
            (function (i) {
                // 添加未加载依赖模块数量统计
                depsCount++;
                loadModule(deps[i], function (mod) {
                    // 依赖模块序列中添加依赖模块接口引用
                    params[i] = mod;
                    depsCount--;
                    if (depsCount === 0) {
                        setModule(url, params, callback);
                    }
                });
            })(i);
            i++;
        }
    } else {
        // 在模块缓存器中矫正模块, 并且执行构造函数
        setModule(url, [], callback);
    }
};

代码示例: 异步模块加载

第三十七章-Widget模式

描述

Widget模式就是借用web Widget**将页面分解为部件, 针对部件开发, 最终组合为完整的页面。
模块化开发使页面的功能细化, 逐一实现每个胃功能, 完成系统需求, 这是一个很好的编程实践。

具体实现和场景略过, 三大框架都是基于模板引擎的, 就算是没有模板引擎, 直接用handlebars 也是一样的。

useMemo、useCallback、useContext 你真的玩明白了吗

useMemo、useCallback、useContext 你真的玩明白了吗

这两个 hook 在首次 render 时需要做一些额外工作来提供缓存, 同时既然要提供缓存那必然需要额外的内存来进行缓存。

正确使用场景

使用场景:

  1. 缓存 useEffect 的引用类型依赖
  2. 缓存子组件 props 中的引用类型

缓存 useEffect 的引用类型依赖

import { useEffect } from 'react'

export default () => {
  const msg = {
    info: 'hello world',
  }
  useEffect(() => {
    console.log('msg:', msg.info)
  }, [msg])
}

上面: 每次组件在render 的时候 msg 都会被重新创建,msg 的引用在每次 render 时都是不一样的。 所以这里 useEffect 在每次render 的时候都会重新执行。

改进 - 使用 useMemo:

import { useEffect, useMemo } from "react";

const App = () => {
  const msg = useMemo(() => {
    return {
      info: "hello world",
    };
  }, []);
  useEffect(() => {
    console.log("msg:", msg.info);
  }, [msg]);
};
export default App;

改进 - 使用 userCallback:

import { useEffect, useCallback } from "react";

const App = (props) => {
  const print = useCallback(() => {
    console.log("msg", props.msg);
  }, [props.msg]);
  useEffect(() => {
    print();
  }, [print]);
};

export default App;

缓存子组件 props 中的引用类型

这个为了解决子组件非必要渲染场景。
引起子组件重新渲染的原因:

  1. 组件的 props 或 state 变化会导致组件重新渲染
  2. 父组件的重新渲染会导致其子组件的重新渲染

有几个误区:

  1. 子组件没有使用 memo:
import { useCallback, useState } from "react";

const Child = (props) => {
};
const App = () => {
  const handleChange = useCallback(() => {
  }, []);
  const [count, setCount] = useState(0);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1)
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} />
    </>
  );
};
export default App;
  1. 父组件没有保持对传递方法的引用:
import { useCallback, useState, memo } from "react";

const Child = memo((props) => {});
const App = () => {
  const handleChange = () => {};
  const [count, setCount] = useState(0);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} />
    </>
  );
};
export default App;

handleChangeApp 组件每次重新渲染的时候都会重新创建生成,引用当然也是不一样,势必会造成 Child 组件重新渲染。

正确解锁姿势:

import { useCallback, useState, memo, useMemo } from "react";

const Child = memo((props) => {});
const App = () => {
  const [count, setCount] = useState(0);
  const handleChange = useCallback(() => {}, []);
  const list = useMemo(() => {
    return [];
  }, []);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} list={list} />
    </>
  );
};

export default App;

对子应用添加 memo, 父组件在传递方法的时候, 用 useCallback, 传递值的时候使用 useMemo
仅仅在需要发生变更的场景下, 对其传递的值或者应用进行变更。

useContext 使用

useContext 这个 api,同时结合 useReducer 是可以代替 redux 来做状态管理的。
看一个例子:
https://codesandbox.io/s/laughing-cookies-s4qjco?file=/src/App.tsx

import React, { createContext, useContext, useReducer } from "react";

const ContainerContext = createContext({ count: 0 });
const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const { state, dispatch } = useContext(ContainerContext);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

function Tip() {
  return <span>计数器</span>;
}

function Container() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ContainerContext.Provider value={{ state, dispatch }}>
      <Counter />
      <Tip />
    </ContainerContext.Provider>
  );
}

export default Container;

useContext 的机制是使用这个 hook 的组件在 context 发生变化时都会重新渲染。
例如在 ContainerContext.Provider 组件下面的 Tip 组件, 会因为 context 发生变化而重新渲染;

解决办法 - Provider 单独封装

我们把状态管理单独封装到一个 Provider 组件里面,然后把子组件通过 props.children 的方式传进去;

// ...
function Provider(props) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ContainerContext.Provider value={{ state, dispatch }}>
      {props.children}
    </ContainerContext.Provider>
  );
}

const App = () => {
  return (
    <Provider>
      <Counter />
      <Tip />
    </Provider>
  );
};
// ...

这个时候 APP 组件就成为了无状态组件,state 变化的时候 props.children 不会改变,不会被重新渲染,这个时候再看 Tip 组件,状态更新的时候就不会跟着重新渲染了。

但是这样依然会有别的问题:
当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。 官方文档 对这部分内容也有说明。

最好的办法是对 value 进行缓存:

// ...
function Provider(props) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const value = useMemo(() => ({ state, dispatch }), [state]);
  return (
    <ContainerContext.Provider value={value}>
      {props.children}
    </ContainerContext.Provider>
  );
}
// ...

memo 优化直接被穿透,不再起作用

很多时候我们又会在使用 memo 的组件中使用 context,用 context 的地方在context发生变化的时候无论如何都会发生重新渲染,所以很多时候会导致 memo 优化失效。
具体可以看这里的讨论,react 官方解释说设计如此;
项目中主要解决方案是把 context 往上提,然后通过属性传递:

以前的写法:

React.memo(()=> {
 const {count} = useContext(ContainerContext);
 return <span>{count}</span>
})

现在的写法:

const Child = memo((props)=>{
    // ...
})
function Parent() {
  const {count} = useContext(ContainerContext);
  return <Child count={count} />;
}

对 context 进行原子化

随着业务代码越来越复杂,在不经意间我们就会把一些不相关的数据放在同一个context 里面。这样就导致了context 中任何数据的变化都会导致使用这个 context 的组件重新 render。
所以一般来说, 需要对 context 进行组合拆分, 原子化原则。

参考文档

[shell] 入门 - 2 运算符

运算符

算术运算符

expr 是一款表达式计算工具,使用它能完成表达式的求值操作。

# 命令行直接计算
expr 2 + 2   #4
expr 3 - 2   #1
expr 3 / 2   #1
expr 3 \* 2   #6

# 使用表达式
a=10
b=20
val=`expr $a + $b`
echo "a + b : $val"

算术运算符列表

运算符 说明 举例
+ 加法 expr $a + $b 结果为 30。
- 减法 expr $a - $b 结果为 10。
* 乘法 expr $a \* $b 结果为 200。
/ 除法 expr $b / $a 结果为 2。
% 取余 expr $b % $a 结果为 0。
= 赋值 a=$b 将把变量 b 的值赋给 a。
== 相等。 用于比较两个数字,相同则返回 true。 [ $a == $b ] 返回 false。
!= 不相等。 用于比较两个数字,不相同则返回 true。 [ $a != $b ] 返回 true。

关系运算符

#!/usr/bin/env bash

a=10
b=20
if [[ ${a} -eq ${b} ]]; then
    echo "${a} -eq ${b} : a is equal to b"
else
    echo "${a} -eq ${b} : a is not equal to b"
fi

关系运算符列表

运算符 说明
-eq 检测两个数是否相等,相等返回 true。同算数运算符==
-ne 检测两个数是否相等,不相等返回 true
-gt 检测左边的数是否大于右边的,如果是,则返回 true。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。
-ge 检测左边的数是否大等于右边的,如果是,则返回 true。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。

布尔运算符

if [ 3 -eq 3 -a 3 -lt 5 ]
then
    echo 'ok'
fi;

布尔运算符列表

运算符 说明
! 非运算,表达式为 true 则返回 false,否则返回 true。
-o 或运算(or),有一个表达式为 true 则返回 true。
-a 与运算(and),两个表达式都为 true 才返回 true。

字符串运算符

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否为0,不为0返回 true。 [ -n $a ] 返回 true。
str 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

文件测试运算符

#!/bin/sh
file="/tmp/test.sh"

if [ -e $file ]
then
   echo "File exists"
else
   echo "File does not exist"
fi
操作符 说明 举例
-b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false。
-c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false。
-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false。
-p file 检测文件是否是具名管道,如果是,则返回 true。 [ -p $file ] 返回 false。
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。
-r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true。
-w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true。
-x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true。
-s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。 [ -s $file ] 返回 true。
-e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true。

请求超时重试

看过很多请求超时重试的样例, 很多都是基于 axios interceptors 实现的。 但是有没有牛逼的原生方式实现呢?

最近在看 fbjs 库里面的代码, 发现里面有一个超时重试的代码, 只有一百多行代码, 封装的极其牛逼。 直接贴代码地址:github.com/facebook/fb…

不过这里的代码是 Flow 类型检测的代码, 而且有一些外部小依赖, 接下来, 咱们解除依赖, 然后一步一步来实现一下这部分逻辑。

这里简单介绍一下 fbjs 这个库

fbjs(Facebook JavaScript)是一个由 Facebook 开发和维护的 JavaScript 工具库。它提供了一组通用的 JavaScript 功能和实用工具,用于辅助开发大型、高性能的 JavaScript 应用程序。

1.先封装一个正常的请求

我们先用 fetch 封装一个非常正常的请求, 这个没有什么好说的, 直接上代码:

// 发起请求
const sendTimedRequest = (url: string, fetchConfig: RequestInit) => {
  const request = fetch(url, fetchConfig);

  return new Promise((resolve, reject) => {
    request.then(response => {
      if (response.status >= 200 && response.status < 300) {
        resolve(response);
      } else {
        const error: any = new Error(`response error.`);
        error.response = response;
        reject(error);
      }
    }).catch(error => {
      reject(error);
    });
  });
};

2.请求超时判定

需要再次封装一个 参数 fetchTimeout, 这个参数的作用就是指明超时时间。 计算超时时间是从请求发起的时候开始计算, 如果超过 fetchTimeout 证明请求就超时了, 那么直接阻断该请求的;

要实现超时时间和阻断请求, 使用的原理也很简单, 就是 闭包 + setTimeout + flag

所以因为引入了闭包, 我们需要将上面的 sendTimedRequest 放置在一个闭包函数里面, 直接上代码:

interface InitWithRetries extends RequestInit {
  fetchTimeout?: number | null;
}

const DEFAULT_TIMEOUT = 1000 * 1.5;


const fetchWithRetries = (url: string, initWithRetries?: InitWithRetries): Promise<any> => {
  // fetchTimeout 请求超时时间
  // 请求
  const { fetchTimeout, ...init } = initWithRetries || {};

  // 超时时间
  const _fetchTimeout = fetchTimeout != null ? fetchTimeout : DEFAULT_TIMEOUT;

  // 开始时间
  let requestStartTime = 0;

  return new Promise((resolve, reject) => {
    // 申明发送请求方法
    const sendTimedRequest = (): void => {
      // 发起请求时间
      requestStartTime = Date.now();

      // 是否需要处理后续请求
      let isRequestAlive = true;

      // 发起请求
      const request = fetch(url, init);

      // 请求超时情况
      const requestTimeout = setTimeout(() => {
        // 需要阻断正常的请求返回
        isRequestAlive = false;

        // 需要重新发起请求
        sendTimedRequest();
      }, _fetchTimeout);

      // 正常请求发起
      request.then(response => {
        // 正常请求返回的场景, 清空定时器
        clearTimeout(requestTimeout);

        // 如果进入了超时流程, 那么正常返回的逻辑, 就直接阻断
        if (isRequestAlive) {
          if (response.status >= 200 && response.status < 300) {
            resolve(response);
          } else {
            const error: any = new Error(`response error.`);
            error.response = response;
            reject(error);
          }
        }
      }).catch(error => {
        reject(error);
      });
    };

    sendTimedRequest();
  });
};

3.上面代码存在问题

上面的代码其实是存在问题的;我们设置的超时时间是 1.5s , 那么如果接口时间过长, 会存在的情况是啥? 无限重复请求

就像下面这样子:

image.png

那么接下来要解决的问题就是, 重复请求次数问题, 我们需要把重复发起请求的次数限定在一个可控范围内;那么就需要加入重复请求次数的概念。

重复请求次数的概念, fbjs 里面的设计就非常巧妙了。因为他是一个数组,每个元素都是数字,每个数字对应的就是延迟重复请求的时间。

比如:

const DEFAULT_RETRIES = [1000, 3000];

上面的设置中, 表示首次请求超时之后, 会再次发起两次重复请求, 第一次重复请求延迟时间为 1000 ms 的时候发起, 第二次重复请求延迟时间为 3000ms 的时候发起。如果两次重复请求均失败, 那么最后再把最终失败结果作为 promise.reject 返回。

再例如, 如果设置时间为:

const DEFAULT_RETRIES = [0, 0];

那么会重复请求 2 次, 不会进行延迟请求, 第一次请求如果超时时间为 1.5 秒之后, 接口没有返回, 那么会立马进行第一次重试请求, 第一次重试请求 1.5秒 之后, 接口还是没有返回, 就进行第二次重试请求。

同时还需要一个概念就是, 如何判定是否需要再次请求, 即 shouldRetry 函数, 判定需要是否发起重复请求;

说到这儿了, 直接上完整代码

interface InitWithRetries extends RequestInit {
  fetchTimeout?: number | null;
  retryDelays?: number[] | null;
}

const DEFAULT_TIMEOUT = 1000 * 1.5;
const DEFAULT_RETRIES = [0, 0];

const fetchWithRetries = (url: string, initWithRetries?: InitWithRetries): Promise<any> => {
  // fetchTimeout 请求超时时间
  // 请求
  const { fetchTimeout, retryDelays, ...init } = initWithRetries || {};

  // 超时时间
  const _fetchTimeout = fetchTimeout != null ? fetchTimeout : DEFAULT_TIMEOUT;

  // 重复时间数组
  const _retryDelays = retryDelays != null ? retryDelays : DEFAULT_RETRIES;

  // 开始时间
  let requestStartTime = 0;

  // 重试请求索引
  let requestsAttempted = 0;

  return new Promise((resolve, reject) => {
    // 申明发送请求方法
    const sendTimedRequest = (): void => {
      // 自增索引与请求次数
      requestsAttempted++;

      // 发起请求时间
      requestStartTime = Date.now();

      // 是否需要处理后续请求
      let isRequestAlive = true;

      // 发起请求
      const request = fetch(url, init);

      // 请求超时情况
      const requestTimeout = setTimeout(() => {
        // 需要阻断正常的请求返回
        isRequestAlive = false;

        // 需要重新发起请求
        if (shouldRetry(requestsAttempted)) {
          console.warn("fetchWithRetries: HTTP timeout, retrying.");
          retryRequest();
        } else {
          reject(new Error(
            `fetchWithRetries(): Failed to get response from server, tried ${requestsAttempted} times.`,
          ));
        }
      }, _fetchTimeout);

      // 正常请求发起
      request.then(response => {
        // 正常请求返回的场景, 清空定时器
        clearTimeout(requestTimeout);

        // 如果进入了超时流程, 那么正常返回的逻辑, 就直接阻断
        if (isRequestAlive) {
          if (response.status >= 200 && response.status < 300) {
            resolve(response);
          } else if (shouldRetry(requestsAttempted)) {
            console.warn("fetchWithRetries: HTTP error, retrying.");
            retryRequest();
          } else {
            const error: any = new Error(`response error.`);
            error.response = response;
            reject(error);
          }
        }
      }).catch(error => {
        clearTimeout(requestTimeout);
        if (shouldRetry(requestsAttempted)) {
          retryRequest();
        } else {
          reject(error);
        }
      });
    };

    // 发起重复请求
    const retryRequest = (): void => {
      // 重复请求 delay 时间
      const retryDelay = _retryDelays[requestsAttempted - 1];

      // 重复请求开始时间
      const retryStartTime = requestStartTime + retryDelay;

      // 延迟时间
      const timeout = retryStartTime - Date.now() > 0 ? retryStartTime - Date.now() : 0;

      // 重复请求
      setTimeout(sendTimedRequest, timeout);
    };

    // 是否可以发起重复请求
    const shouldRetry = (attempt: number): boolean => attempt <= _retryDelays.length;

    sendTimedRequest();
  });
};

fetchWithRetries("http://127.0.0.1:3000/user/")

4.测试

测试代码就是上面的完整代码, 如果我们有一个接口, 1s 左右返回, 因为超时时间为 1.5 s 那么, 请求会直接成功, 只会请求一次即可:

image.png

那么, 如果接口时间改为 2 s 时间返回:

image.png

5.彩蛋

上面使用到了一个 mock 接口, 这里推荐一个非常非常非常好用的 mock 工具, 使用简单又好使: webpro/dyson

比如 mock 上面的 user 请求, 那么只需要下面代码就可以了: 文件 /src/index.js, 代码如下

module.exports = {
  path: '/user/',
  method: 'GET',
  delay: 2000,
  cache: false,
  template: (params, query, body, cookies, headers) => {
    return {
      message: 'success',
      status: 200,
    }
  }
}

直接启动命令行即可:

dyson ./src 3000

更多使用文档可以访问 github 官方文档

源码链接

直接丢链接: github.com/yanlele/nod…

JS数组42个方法汇总

JS数组42个方法汇总

改变数组本身 (9个)

pop和push尾部删除添加

这两个方法用于数组结尾的删除和添加

const arr = [ 1, 2, 3, 4, 5 ]
//添加到数组的尾端
arr.push(6) //[1,2,3,4,5,6]
//再次调用pop方法就删除了最后一位
arr.pop()//[1,2,3,4,5]

unshift和shift头部删除添加

用于在数组的首位进行删除和添加

const arr = [ 1, 2, 3, 4, 5 ]
//添加到数组的前端
arr.unshift(6) //[6,1,2,3,4,5]
//再次调用shift方法就删除了第一位
arr.shift()//[1,2,3,4,5]

sort 排序

进行对数组就地排序,不会复制或返回一个新数组,接收可选参数,一个回调函数,有a,b两个参数,当返回a<b时返回-1从小到大排序,当返回a>b时返回1从大到小排序,a==b时返回0,保持原来的排序(默认排序是将元素转换为字符串,然后按照它们的 UTF-16 码元值升序排序。)

const arr = ["March", "Jan", "Feb", "Dec", 6, 2, "A", "a"];
arr.sort(function (a, b) {
  if (a < b) {
    return -1;
  } else if (a > b) {
    return 1;
  } else {
    return 0;
  }
});
console.log(arr);//['A','Dec', 'Feb','Jan','March',2, 6,'a']

reverse 反转

对数组进行就地反转,顺序颠倒

const arr = ["March", "Jan", 6, 2, "A", "a"];
arr.reverse();
console.log(arr);//[ 'a', 'A', 2, 6, 'Jan', 'March' ]

splice 截取新增数据

可以选择删除数组中的某一个值,也可以在数组中的某个位置添加一些数据,接收可选参数,三个或以上的参数,第一个为截取的索引位置,number类型,第二个截取的个数,number类型,第三个或更多实在截取位置添加的参数,可以是任何类型

const arr = ["March", "Jan", 6, 2, "A", "a"];
//在索引为2的位置截取一个,并在索引2的位置后添加8
arr.splice(2, 1, 8);
console.log(arr);//[ 'March', 'Jan', 8, 2, 'A', 'a' ]
//截取位数不够,就将有的全部且去掉
arr.splice(2, 6);
console.log(arr);//[ 'March', 'Jan' ]

copyWithin 将数组得一部分赋值到另一个位置

copyWithin是一种移动数组数据的高性能方法,copyWithin() 方法是通用的。它只期望 this 值具有 length 属性和整数键属性。虽然字符串也是类似数组的,但这种方法不适用于它们,因为字符串是不可变的。
copyWithin不会改变数组的长度,只会修改内容,它接收三个参数,第一个为复制到的目标位置(索引值),第二个是复制的起始位置(可选),如果为负数,则相当于从后往前数,第三个为结束位置,不包含此索引的位置(可选),起始位置不可小于结束位置,否者方法无效。返回一个浅拷贝的新数组,并且改变原数组

const arr1 = ["March", "Jan", 6, 2, "A", "a"];
//从索引为三个位置开始复制,到索引为5的位置,但不包含5,从索引为1的位置粘贴并覆盖
const newArr = arr1.copyWithin(1, 3, 5);
console.log(arr1,newArr);//[ 'March', 2, 'A', 2, 'A', 'a' ] [ 'March', 2, 'A', 2, 'A', 'a' ]

//为负数时从后往前数-2从A的位置到-1不包括-1的位置,也就是将A赋值并覆盖到了索引为0的位置
const newArr = arr1.copyWithin(0, -2, -1);
console.log(newArr);//[ 'A', 'Jan', 6, 2, 'A', 'a' ]

//这种结束索引位置在开始索引位置之前的都不生效
const newArr = arr1.copyWithin(0, -2, 2);
const newArr = arr1.copyWithin(0, -2, -4);
console.log(newArr);//[ 'March', 'Jan', 6, 2, 'A', 'a' ]

fill 填充

对数组内容进行覆盖填充,有三个参数,第一个为填充的值,第二个为起始位置(可选),第三个为结束位置,不包含此索引位置(可选)。与copyWithin比较类似,只不过一个是移动数组内的元素,一个填充数组的内的元素,不会改变数组的长度。返回一个浅拷贝的新数组,并且改变原数组

const arr1 = ["March", "Jan", 6, 2, "A", "a"];
//将666填充到1-4不包括4索引的位置
const newArr = arr1.fill(666, 1, 4);
console.log(newArr);//[ 'March', 666, 666, 666, 'A', 'a' ]

不改变原数组 (11个)

filter 数据过滤

需要一定条件返回对应的数据,接收一个回调函数,有回调函数有三个参数,第一个是当前遍历的元素,第二个为当前索引,第三个是数组本身,需要一个返回值,filter方法会根据符合这个返回值条件的数据返回一个新数组

const arr = ["March", "Jan", 6, 2, "A", "a"];
//这里是一个简单的例子,返回类型为string的元素
const newArr = arr.filter((item, index) => typeof item === "string");
console.log(newArr);//[ 'March', 'Jan', 'A', 'a' ]

map

map方法只是单纯的返回一个新数组,可以是处理后的,也可以是原数组,接收一个回调函数,回调函数有三个参数第一个是当前遍历的元素,第二个为当前索引,第三个是数组本身,需要一个返回值,从map内部处理过后,回调函数的返回值返回一个新数组

const arr = ["March", "Jan", 6, 2, "A", "a"];
//返回一个number的数组,不是number类型的就返回它们的字段长度
const newArr = arr.map((item, index) => (typeof item === "number" ? item : item.length);
console.log(newArr);//[ 5, 3, 6, 2, 1, 1 ]

reduce 数据累加

reduce是一个功能非常强大的方法,但平常很少使用,因为他的功能他的方法都可以实现,它也能实现其他的一些方法,有时候合理的使用reduce会大大减少代码量。接收两个参数,第一个为回调函数,回调函数有四个参数,第一个参数为上一次回调函数return的结果,首次默认为第二个参数值,如果没有第二个参数值,则默认当前数组下标为0的参数,第二个参数为当前元素,第三个为当前索引值,第四个为数组本身,reduce第二个参数指定一个默认值,可选

//使用reduce实现filter方法
const arr = ["March", "Jan", 6, 2, "A", "a"];
//定义第二个参数的默认值为一个数组
const newArr = arr.reduce((acc, cur, index) => {
  typeof cur === "string" && acc.push(cur);
  return acc;
}, []);
console.log(newArr);//[ 'March', 'Jan', 'A', 'a' ]
//使用reduce实现数字的求和
//第二个参数默认定义0 number类型
const newArr = arr.reduce((acc, cur, index) => {
  typeof cur === "number" && (acc += cur);
  return acc;
}, 0);
console.log(newArr);//8

reduceRight 从右开始 数据累加

这个是reduce的右边开始一种写法,运算时会从右向左执行,参数与使用方法和reduce一致。适用于当你想对一个数组进行反转加过滤等操作的时候,这个方法就完全突出了他的便携

const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.reduceRight((acc, cur, index) => {
  typeof cur === "string" && acc.push(cur);
  return acc;
}, []);
//这里打印之后可以看出,毕竟过滤了非字符串的参数,还将数组反转了
console.log(newArr);//[ 'a', 'A', 'Jan', 'March' ]

slice 数组截取

可以对一个数组进行浅拷贝,接收两个参数,第一个为截取的初始位置,第二个为终止位置(不包括此索引值),如果只填一个参数则从当前索引值截取到最

const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.slice(0, 3);
console.log(newArr);//[ 'March', 'Jan', 6 ]

const newArr = arr.slice(3);
console.log(newArr);//[ 2, 'A', 'a' ]

concat 数组合并

需要两个或以上的数组合并的时候就可以使用cancat快速合并,当然在ES6之后大多都使用扩展运算符进行数组合并了,此方法接收一个或以上得任意类型参数

const arr1 = ["March", "Jan"];
const arr2 = [6, 2, "A", "a"];
const arr3 = {
  name: "Tom",
  age: 18,
  sex: "男",
};
//如果参数是数组则会合并
const newArr = arr1.concat(arr2);
console.log(newArr);//[ 'March', 'Jan', 6, 2, 'A', 'a' ]
//一个以上的参数 如果是值类型 则会直接添加到数组得最后面
const newArr = arr1.concat(arr2,'Tom');
console.log(newArr);//[ 'March', 'Jan', 6, 2, 'A', 'a','Tom' ]
//一个以上的参数,为一个对象类型,会直接添加到对象中
const newArr = arr1.concat(arr2,arr3);
console.log(newArr);//[ 'March', 'Jan', 6, 2, 'A', 'a', { name: 'Tom', age: 18, sex: '男' } ]

flatMap 扁平化map

flatMap与map相似,都是接收一个回调函数,进行处理后返回一个数组,但有一处差别就是flatMap可以对数组进行一层扁平化(仅数组)

const arr1 = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr1.flatMap((item, index) => {
  return [item, index];
});
//可以看出本应该是双层数组的,却被扁平化了
console.log(newArr);//['March', 0,'Jan', 1,6,2,2,3,'A',4,'a', 5]

const newArr = arr1.flatMap((item, index) => {
  return [[item, index]];
});
//仅只能扁平化一层
console.log(newArr);//[[ 'March', 0 ],[ 'Jan', 1 ], [ 6, 2 ], [ 2, 3 ],[ 'A', 4 ],[ 'a', 5 ]]

with 修改指定索引值得复制方法

此方法兼容性不好,暂时不推荐使用,node版本需要20.0.0以上,浏览器就不用说了

我们都知道,我们再修改数组中得某一个值得时候可以使用arr[index]=xxx 来进行修改,但是这样是改变了原数组,当我们既想使用索引值来改变某一个值,还不想改变原数组得时候就可以使用with方法,它接收两个参数,第一个为索引值,第二个是要修改成为数据

const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.with(3, "Tom");

console.log(newArr);//[ 'March', 'Jan', 6, 'Tom', 'A', 'a' ]

toReversed 反转数组的复制版

此方法兼容性不好,暂时不推荐使用,node版本需要20.0.0以上,浏览器就不用说了

使用reverse可以反转数组,但是会改变原数组,如果不想让原数组改变的并反转数组的话就可以使用它的复制版本toReveresed

const arr1 = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr1.toReversed();

console.log(newArr);//[ 'a', 'A', 2, 6, 'Jan', 'March' ]

toSorted 排序的复制版

此方法兼容性不好,暂时不推荐使用,node版本需要20.0.0以上,浏览器就不用说了

使用sort可以反转数组,但是会改变原数组,一样的可以使用toSorted,不会改变原数组,会返回一个排好序的数组,接受的参数和sort一致,参考sort

const arr1 = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr1.toSorted();
console.log(newArr);//[ 2, 6, 'A', 'Jan', 'March', 'a' ]

toSpliced 截取新增数组的复制版

此方法兼容性不好,暂时不推荐使用,node版本需要20.0.0以上,浏览器就不用说了

使用splice可以对数组进行截取和指定位置新增数据,但是会改变原数组,可以使用toSpliced,不会改变原数组,会返回一个新的数组,接受的参数使用方法和splice一致,参考splice

const arr1 = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr1.toSpliced(0, 1, 4);
console.log(newArr);//[ 4, 'Jan', 6, 2, 'A', 'a' ]

其他 功能性方法(22个)

forEach 数组遍历

这个方法应该都非常熟悉了,为什么把他分为其他里面呢,因为他的作用只是遍历,其他什么作用都没有,接收一个回调函数,又三个参数,第一个当前元素,第二个为索引值,第三个为数组本身因为都比较熟悉了,就随便写个例子

const arr = ["March", "Jan", 6, 2, "A", "a"];
//使用这中方法改变数组本身
arr.forEach((item, index) => {
  arr[arr.length - index - 1] = item;
});
//返回这种数据应为数组本身在遍历的时候被改变了
console.log(arr);//[ 'March', 'Jan', 6, 6, 'Jan', 'March' ]

Array.from() 转换成数组

此方法可以将一些可迭代的以及为数组的数据转换成真正的数组,并返回一个那个新数组,比如字符串,dom伪数组等,接收两个参数,第一个为要转化的参数,第二个是一个回调函数(可选),回调函数有两个参数当前遍历的对象和索引

const newArr = Array.from("March");
console.log( newArr);//[ 'M', 'a', 'r', 'c', 'h' ]

(function () {
//arguments为一个伪数组,转化成真正的数组,并经过第二个回调函数进行处理返回
  const arr = Array.from(arguments, (item, index) => item + index);
  console.log(arr,Array.isArray(arr));//[ 1, 4, 6, 8 ] true
})(1, 3, 4, 5);

Array.fromAsync Array.from异步版本

Array.fromAsync() 迭代异步可迭代对象的方式与 for await...of 很相似。Array.fromAsync() 在行为上与 Array.from() 几乎等价

  • Array.fromAsync() 可以处理异步可迭代对象。
  • Array.fromAsync() 返回一个会兑现为数组实例的 Promise
  • 如果使用非异步可迭代对象调用 Array.fromAsync(),则要添加到数组中的每个元素(无论是否为 Promise)都会先等待其兑现
  • 如果提供了 mapFn,则其输入和输出会在内部等待兑现。

Array.fromAsync()Promise.all() 都可以将一个 promise 可迭代对象转换为一个数组的 promise。然而,它们有两个关键区别:

  • Array.fromAsync() 会依次等待对象中产生的每个值兑现。Promise.all() 会并行等待所有值兑现。
  • Array.fromAsync() 惰性迭代可迭代对象,并且不会获取下一个值,直到当前值被兑现。Promise.all() 预先获取所有值并等待它们全部兑现。
//我也没用过,凑合看吧  手动滑稽(≧∇≦)ノ
const asyncIterable = (async function* () {
  for (let i = 0; i < 5; i++) {
    await new Promise((resolve) => setTimeout(resolve, 10 * i));
    yield i;
  }
})();

Array.fromAsync(asyncIterable).then((array) => console.log(array));
// [0, 1, 2, 3, 4]

Array.isArray 判断是不是数组

在类型判断的时候,我们通常使用typeof ,但是使用typeof的时候数组判断出来的就是Object类型,可以说数组是特殊的对象,使用typeof判断不出数组,就可以使用Array.isArray方法

(function () {
//在这可以看出arguments并不是一个数组
  console.log(Array.isArray(arguments));//false
})(1, 3, 4, 5);

const arr = ["March", "Jan", 6, 2, "A", "a"];
//看的出是可以识别出来的 但typeof却识别不出来
console.log(typeof arr, Array.isArray(arr));//object true

includes 判断某个值数组中是否存在

在数组中查抄某一个值,返回一个布尔值,有两个参数,第一个你要查找的值,第二个从哪个索引位置开始找

const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.includes(6);
console.log(newArr);//true

//也可以利用这一特性简化判断条件
let name='a'
//name是一个变量,可能有很多种可能,判断条件中就会非常冗余
if ( name === 'a' || name === 'A' || name === 6...) {
  //...
}
//可以改成这种,看着也非常明了简便
if (['a',"A",6,...].includes(name)) {
  //...
}

indexOf 判断数组中是否存在某个值,并返回索引

写法和includes类似,有两个参数第一个是要找的值,第二个为开始索引,indexOf会在查找到第一个符合条件的参数跳出循环并返回索引,没找到则返回-1

const arr = ["March", "Jan", 6, 2, "A", 6, "a"];
const newArr = arr.indexOf(6);
//返回索引值
console.log(newArr);//2

//查找6,从索引为3的位置开始找
const newArr = arr.indexOf(6,3);
console.log(newArr);//5

lastIndexOf 判断数组中是否存在某个值,并返回最后的索引

与indexOf一致,只不过是返回最后的索引位置,也可以理解为他是从数组的右边开始往左找元素,并返回第一个找到的元素的索引,没找到则返回-1

//所有的结果恰恰与indexOf反过来了
const arr = ["March", "Jan", 6, 2, "A", 6, "a"];
const newArr = arr.lastIndexOf(6);
//返回索引值
console.log(newArr);//5

//查找6,从索引为3的位置开始找
const newArr = arr.lastIndexOf(6,3);
console.log(newArr);//2

find 查找符合条件的元素

find查找符合条件的的一个元素并返回那个元素本身,没有则返回undefined,接收一个回调函数,回调函数有三个形参,第一个当前元素,第二个当前索引,第三个数组本身

const arr = ["March", "Jan", 6, 2, "A", 6, "a"];
const newArr = arr.find((item, index) => {
  return item.length > index;
});
//只会返回符合条件的第一个值
console.log(newArr);//March

//也可以用在数组对象上
const arr = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const newArr = arr.find((item, index) => {
  return item.id > index;
});
//返回对象元素本身
console.log(newArr);//{ id: 1 }

findIndex 查找符合条件的元素,返回索引版

find使用方法一致,findIndex查找符合条件的的一个元素并返回那个元素的索引值,没有则返回-1,接收一个回调函数,回调函数有三个形参,第一个当前元素,第二个当前索引,第三个数组本身

const arr = ["March", "Jan", 6, 2, "A", 6, "a"];
const newArr = arr.findIndex((item, index) => {
  return item.length > index;
});
//只会返回符合条件的第一个索引
console.log(newArr);//0

//也可以用在数组对象上
const arr = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const newArr = arr.findIndex((item, index) => {
  return item.id > index;
});
//返回对象元素所在位置的索引
console.log(newArr);//0

findLast 从右向左查找符合条件的元素

此方法兼容性不好,暂时不推荐使用,node版本需要18.0.0以上find使用方法一致,findLast从右向左查找符合条件的的一个元素,并返回那个元素,没有则返回undefined,接收一个回调函数,回调函数有三个形参,第一个当前元素,第二个当前索引,第三个数组本身

const arr = ["March", "Jan", 6, 2, "A", 6, "a"];
const newArr = arr.findLast((item, index) => {
  return item.length > index;
});
//返回Jan,从右向左第一个符合条件的就是Jan,索引值是不变的,例如arr数组,遍历的时候索引值是6、5、4、3、2、1、0
console.log(newArr);//Jan

//也可以用在数组对象上
const arr = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const newArr = arr.findLast((item, index) => {
  return item.id > index;
});
//返回第一个符合条件的对象元素本身
console.log(newArr);//{ id: 5 }

findLastIndex 从右向左查找符合条件的元素,返回索引版

此方法兼容性不好,暂时不推荐使用,node版本需要18.0.0以上findLast使用方法一致,findLastIndex从右向左查找符合条件的的一个元素,并返回那个元素的索引值,没有则返回-1,接收一个回调函数,回调函数有三个形参,第一个当前元素,第二个当前索引,第三个数组本身

const arr = ["March", "Jan", 6, 2, "A", 6, "a"];
const newArr = arr.findLastIndex((item, index) => {
  return item.length > index;
});
//从右向左查找,返回符合条件的第一个索引
console.log(newArr);//1

//也可以用在数组对象上
const arr = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const newArr = arr.findLastIndex((item, index) => {
  return item.id > index;
});
//从右向左查找,返回对象元素所在位置的索引
console.log(newArr);//4

at 返回索引位置的值

此方法兼容性一般,暂时不推荐使用,node版本需要16.6.0以上 at接收一个number的参数,可以为负数,正数时获取到索引为的值,当参数为负数时,从右向左查找对应的值

const arr = ["March", "Jan", 6, 2, "A", "a"];
//正数与直接索引取值无异
const newArr = arr.at(2);
console.log(newArr);//6
//从右向左找 可以简化代码量,有些情况下效果很明显
const newArr = arr.at(-2);
//等价于  const newArr = arr.at(arr.length - 2);
console.log(newArr);//A

Array.of 创建可变的数组

使用静态方法创建一个可变的数组,可以接收任意类型,任意个数的参数

const newArr = Array.of("March", "Jan", 6, 2, "A", "a");
console.log(newArr);//[ 'March', 'Jan', 6, 2, 'A', 'a' ]

//使用of创建数组和直接使用Array实例创建数组有所不同
const newArr = Array.of(6);
const arr = Array(6);
//传入6 of则创建一个只包含6得数组,Array传入6则创建有六个空位置得数组
console.log(newArr, arr);[ 6 ] [ <6 empty items> ]

flat 扁平化数组

通常在扁平化数组的时候都要使用递归函数,flat方法避免了页面中写递归函数造成大量的代码冗余,flat本身也是使用递归方法来达到数组扁平化的,接收一个number类型的参数,参数是几就可以扁平几层,在不确定有几维数组的情况下,参数为Infinity(无限大),可以扁平任意层次的数组

const arr = [[[[["March"]]]], [[["Jan"]]], [[6]], [[2]], "A", ["a"]];
//扁平参数对等的层数
const newArr = arr.flat(2);
console.log(newArr);//[ [ [ 'March' ] ], [ 'Jan' ], 6, 2, 'A', 'a' ]
//使用Infinity关键字 可以扁平化任意层数数组
const newArr = arr.flat(Infinity);
console.log(newArr);//[ 'March', 'Jan', 6, 2, 'A', 'a' ]

every 所有元素是否通过测试

every用于所有元素是否都能通过测试,返回一个布尔值,只有当所有元素都通过了测试,才会返回true,接收一个回调函数,回调函数有三个形参,第一个为当前元素,第二个为当前索引,第三个为数组本身,另外,当数组为空的时候使用every,条件不论是怎么样的,都会返回true(这种情况属于无条件正确,因为空集的所有元素都符合给定的条件。)

const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.every((item) => typeof item === "string");
//并不是所有的元素都符合条件 所以返回false
console.log(newArr);//false
//只要是数组是空数组,后面的条件不管跟什么返回的永远为true
console.log([].every((item) => item > 10));//true

some 数组中至少有一个元素通过测试

some用于数组中参数其中一个或多个通过了测试,返回一个布尔值,如果有一个或以上通过测试就返回true,一个都没通过返回false,接收一个回调函数,有三个形参,第一个为当前元素,第二个为当前索引,第三个为数组本身,另外,当数组为空时使用some,不论判断条件如何,都会返回false,并且他不会改变数组

const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.some((item) => typeof item === "string");
//其中一个或以上的元素符合条件就返回true
console.log(newArr);//true
//只要是数组是空数组,后面的条件不管跟什么返回的永远为false
console.log([].some((item) => item==undefined));//false

join 选定格式转换成字符串

join用于将数组转换成字符串的方法,接收一个参数(可以为任意类型,但引用类型则会默认转换成[object Object]等),为数组元素转换成字符串的间隔符,不传参数默认以 ‘,’号隔开

const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.join();
console.log(newArr);//March,Jan,6,2,A,a
//可以是number类型
const newArr = arr.join(3);
console.log(newArr);//March3Jan36323A3a
//也可以是引用类型,但会自动转换
const newArr = arr.join({ id: 1 });
console.log(newArr);//March[object Object]Jan[object Object]6[object Object]2[object Object]A[object Object]a

toString 转换成字符串

toString是几乎所有数据类型都有的一个方法,就是单纯的转换成字符串,数组中转换成字符串默认以‘,’号隔开,有一个小技巧,如果多维数组的类型都是值类型的,可以使用toString进行扁平化

//简单的数组转字符串
const arr = ["March", "Jan", 6, 2, "A", "a"];
const newArr = arr.toString();
console.log(newArr);//March,Jan,6,2,A,a

//不论是几维数组,在toString的时候都会转化成字符串,在使用字符串方法转成数据就可以了
//弊端是因为转的时候是toString转换成了字符串,任意值类型到最后都是字符串形式,undefined和null则会转换成空字符串
const arr = [[["March"]], [[[["Jan"]]]], [[[6]]], [[2]], [[["A"]]], [[["a"]]]];
const newArr = arr.toString().split(",");
console.log(newArr);//[ 'March', 'Jan', '6', '2', 'A', 'a' ]

toLocaleString

此方法用于格式转换,最后返回字符串代表数组中所有的元素,接收两个参数第一个带有 BCP 47 语言标签的字符串,或者此类字符串的数组。对于 locales 参数的一般形式和说明,可以参见 Intl 主页面的参数说明。第二个,一个具有配置属性的对象。对于数字,请参见 Number.prototype.toLocaleString();对于日期,请参见 Date.prototype.toLocaleString()

此方法的使用方法记得东西比较多,详细使用方法可以点击上面的链接查看

const arr = ["¥7", 500, 8123, 12];
const newArr = arr.toLocaleString("ja-JP", { style: "currency", currency: "JPY" });
console.log(newArr);//¥7,¥500,¥8,123,¥12
//如果不传参数,则效果于toString一样
const newArr = arr.toLocaleString();
console.log(newArr);//¥7,500,8,123,12

entries 返回数组迭代器的对象,包含键和值

返回一个数组迭代器对象,数组迭代器( array[Symbol.iterator]),如果不太清楚的可以看一下Symbol篇章,或者点击数组迭代器查看

const arr = ["March", "Jan", 6, 2, "A", "a"];
//返回迭代器对象,有一个next方法,使用next方法会返回一个对象,里面value值就是我们想要的值
const newArr = arr.entries();
//查看的时候需要使用next,value为我们想要的值,done是否结束
console.log(newArr.next());//{ value: [ 0, 'March' ], done: false }
console.log(newArr.next());//{ value: [ 1, 'Jan' ], done: false }
console.log(newArr.next());//{ value: [ 2, 6 ], done: false }
console.log(newArr.next());//{ value: [ 3, 2 ], done: false }
console.log(newArr.next());//{ value: [ 4, 'A' ], done: false }
console.log(newArr.next());//{ value: [ 5, 'a' ], done: false }
//已经没有值了,并且done变成了true
console.log(newArr.next());//{ value: undefined, done: true }

//entries返回一个迭代器对象,所以可以被for...of..遍历
 for (const iterator of newArr) {
  console.log(iterator);
 }
 //[ 0, 'March' ]
//[ 1, 'Jan' ]
//[ 2, 6 ]
//[ 3, 2 ]
//[ 4, 'A' ]
//[ 5, 'a' ]

keys 返回数组迭代对象,键

返回一个只包含键的迭代对象,使用方法与entries一致

const arr = ["March", "Jan", 6, 2, "A", "a"];
//返回只包含键的可迭代对象,数组中的键也就是索引
const newArr = arr.keys();
//使用for...of..遍历打印
for (const iterator of newArr) {
  console.log(iterator);
}
//0
//1
//2
//3
//4
//5

values 返回数组迭代对象,值

返回一个只包含值得可迭代对象,使用方法与entries一致

const arr = ["March", "Jan", 6, 2, "A", "a"];
//返回只包含键的可迭代对象,数组中的键也就是索引
const newArr = arr.values();
//使用for...of..遍历打印
for (const iterator of newArr) {
  console.log(iterator);
}
//March
//Jan
//6
//2
//A
//a

结尾

截止到ES2023得42种数组方法就这些,看是否有哪些方法从来就没有使用过吧,有很多方法也许我们平常根本用不到,但也可以相对得了解以下,还有几种最近新出的方法兼容性没那么好,在实际得开发环境中还是需要谨慎使用,如果有写的不对得地方欢迎纠正!

参考文章

数据库MySql基础

第一部分、基础篇

目录

01章、mysql的安装与配置

这个部分的东西太过于基础,直接看网上搜索的文章就可以了:

02章、sql基础

sql语句分类

DDL: 数据定义语言 - create、drop、alter等
DML: 数据操作语句 - insert、delete、update、select等
DCL: 数据控制语句 - grant、revoke等

DDL语句

1、创建数据库

CREATE DATABASE dbname
然后可以输入: show databases ;可以查看已经创建的数据库

use dbname 选择具体的数据库
show tables 查看所有数据表

2、删除数据库

drop database dbname 就可以删除相对应的数据库了

3、创建表

CREATE TABLE tablename (
    column_name_1 column_type1 constraints,
    column_name_2 column_type2 constraints,
    column_name_3 column_type3 constraints,
    ..............
    column_name_n column_typen constraints,
)

column_name是列的名字;column_type是列的数据类型;constraints是列的约束条件

实例:

create table if not exists emp(
  ename varchar(10),
  hiredate date,
  sal decimal(10,2),
  deptno int(2)
);          

desc emp; 可以查看表的定义
show create table emp; 可以查看更加全面的具体sql语句信息

4、删除表

drop table if exists emp;

5、修改表
column_definition 表示一个明确的字段定义 包括名字和属性

5.1、修改表的类型

alter table tablename MODIFY [COLUMN] column_definition [FIRST|AFTER col_name]
例如 要修改emp表的ename字段的定义,想varchar(10)改为varchar(20);
alter table emp modify ename varchar(20);

5.2、添加表字段

alter table tablename ADD [COLUMN] column_definition [FIRST|AFTER col_name]
例如 要给emp表加新字段age, 类型为int(3): alter table emp add column age int(3);

5.3、删除字段

alert table tablename DROP [COLUMN] col_name
例如我们要删除 age 字段 : alter table emp drop column age;

5.4、修改字段名

alter table tablename change [column] old_col_name column_definition [first|after col_name]
例如 把emp表的age改名为age1,同事修改字段类型为int(4): alter table emp change age age1 int(4);

5.5、修改字段排列顺序

添加介绍的 add/change/modify 中还有一个可选项 first|after column_name 这个可以修改字段在表的位置;
例如 add添加新字段默认在表的最后的位置, 比如添加birth data 在ename 之后: alter table emp add birth date after ename;
例如 修改age,将他放在最近前: alter table emp modify age int(3) first;

5.6、修改表名

alter table tablename rename [to] new_tablename
例如 把emp改为emp1: alter table emp rename emp1;

DML语句

1、插入语句

insert into tablename (field1, field2, ......, fieldn) values (value1, value2, ......, valuen);
例如 我们向emp中插入一条数据: insert into emp (ename, hiredate, sal, deptno) values ('yanle', '2018-08-01', '10000', 1);
例如 可以不指定字段名称,但是后面 values 后面的顺序应该和字段是一样的排列: insert into emp values ('lele', '2018-08-01', '10000', '2');
例如 只对ename和sal字段实现插入值: insert into emp (ename, sal) values ('dony', 7000);
例如 查看实际插入的值: select * from emp;

一次性插入多个数据:

insert into tablename (field1, field2, ......, fieldn)
 values 
 (record1_value1, record2_value2, ......, recordn_valuen),
 ..............
 (recordn_value1, recordn_value2, ......, recordn_valuen);

例如 对emp表一次性插入两条数据:

insert into emp (ename, hiredate, sal, deptno)
values
       ('yanle3', '2018-08-01', '10000', 3),
       ('yanle4', '2018-09-15', '1000', 4);

第二种更新数据的办法:

insert into emp set name='admin001', email='[email protected]', password='123456';

2、更新记录

update tablename set field1=value1, field2=value2, .... fieldn=valuen [where condition]
例如 把dony的sal 从7000 改为 4000: update emp set sal=4000 where ename='dony';

同时更新多个表中的数据:
update t1, t2,...... tn set t1.field1=expr1, t2.field2=expr2, ...... tn.fieldn=exprn [where condition]
例如 同时更新emp表中的sal字段和dept表中deptname字段的数据:

create table if not exists dept(
  deptno int(3),
  deptname varchar(10)
);
insert into dept(deptno, deptname)
VALUES
       (1, 'tech'),
       (2, 'sale'),
       (5, 'fin');
select * from dept;
update emp, dept set emp.sal=emp.sal * dept.deptno, dept.deptname=emp.ename where emp.deptno=dept.deptno;
update emp a, dept b set a.sal=a.sal * b.deptno, b.deptname=a.ename where a.deptno=b.deptno;

上面最后两条插入语句执行的效果是一样的。只是最后一句语句添加了一个别名而已。

3、删除记录

delete from tablename [where condition]
例如 在emp中, 将ename为'dony'的记录全部删除: delete from emp where ename='dony';

同时删除多个表的数据(from后面的表要用别名,则delete后面的也要用相应的别名):
delete t1, t2, ...tn from t1, t2,...tn [where condition];
例如 同时删除emp和dept中deptno为3的记录: delete a,b from emp a, dept b where a.deptno=b.deptno and a.deptno=3;

4、查询记录

select * from tablename [where condition];
例如 把所有记录都查出来: select * from emp;
例如 用逗号分隔想要查询的数据: select ename,sal,deptno from emp;

4.1、查询不重复的记录:使用关键字distinct实现
select distinct deptno from emp;

4.2、多条件查询
例如 查询所有deptno为2的记录: select * from emp where deptno=2;

多条件查询中,除了 = 还可以使用,>, <, >=, <=, != 等比较运算符号, 还可以使用and 和 or 等逻辑运算符:
例如 select * from emp where deptno<=5 and sal>5000;

4.3、排序和限制

select * from tablename [where condition] [order by field1 [desc|asc], field2 [desc|asc], ... fieldn [desc|asc]];
asc 升序;desc 降序;默认是由低到高的排列; 如果排序字段值一样,则相同的字段按照第二个排列字段进行排序;

例如 emp表按照sal由低到高排序:

select * from emp order by sal;
select * from emp order by sal asc;

例如 emp表按照sal 降序: select * from emp order by sal desc;
例如 对于deptno相同的两条记录,可以按照工资降序排列: select * from emp order by deptno, sal desc;

对于后面的记录,只希望查询一部分,而不是全部,可以用limit关键字来限制:
select ... [limit offset_start, row_count]
如果offset_start偏移量为0 ,可以省略。
例如 查询emp表中对sal 排序后的钱三条: select * from emp order by sal limit 3;
例如 第二条开始,查询三条: select * from emp order by sal limit 1,3;

4.4、聚合操作

select [field1, field2,... fieldn] fun_name from tablename [where where_contition] [group by field1, field2, ... fieldn [with rollup]] [having where_contition]
参数说明:
fun_name 表示要聚合操作, 也是聚合函数, 常用的有sum(求和)、count(*)(记录数)、max、min;
group my 表示要进行分类聚合的字段,比如按照部门分类统计员工数量,部门就应该写在group by 后面;
WITH ROLLUP 可选语法,表示是否对分类聚合后的结果进行在汇总;
HAVING 表示分类后的结果在进行条件的赛选

例如 要统计emp的总人数: select count(1) from emp;
例如 要在此基础上统计各个部门的人数: select deptno,count(1) from emp group by deptno;
例如 既要统计各个部门的人数,又要统计总人数: select deptno,count(1) from emp group by deptno with rollup;
例如 统计人数大于1的部门: select deptno,count(1) from emp group by deptno having count(1)>1;
例如 最后统计公司所有员工的薪水总额,最高薪水和最低薪水: select sum(sal),max(sal),min(sal) from emp;

4.5、表连接
当同事需要显示多个表的字段是,就要用到表连接。表连接分为:内连接和外链接。
区别:内连接仅选出两张表中相互匹配的记录;外链接会选出其他不匹配的记录。常用捏连接

例如 要查处所有的雇员名字和所在部门,员工在emp表,部门在dept表中: select ename,deptname from emp,dept where emp.deptno=dept.deptno;

外链接又分为左连接和右连接
左连接: 包含所有左边表中的记录,甚至是右边表中没有和它匹配的记录
右连接: 包含所有左边表中的记录,甚至是左边表中没有和它匹配的记录
例如 查询emp中所有用户名和所在部门名称:

select ename,deptname from emp left join dept on emp.deptno=dept.deptno;
select ename,deptname from dept right join emp on emp.deptno=dept.deptno;
select ename,deptname from dept right join emp on dept.deptno=emp.deptno;

上面三种查询费结果都是一样的。

4.6、子查询
如果需要的条件是另外一个select语句的结果,就要用到子查询。
关键字: in、not in、=、!=、exists、not exists

例如 从emp 中查询出所有部门在dept中的所有记录: select * from emp where deptno in(select deptno from dept);

如果子查询记录数唯一,可以用 = 代替 in:
例如 select * from emp where deptno =(select deptno from dept);会报错,以为select deptno from dept 查询出来的deptno不止一个;
例如 select * from emp where deptno =(select deptno from dept limit 1); 就不报错了;

例如 子查询可以转为表连接查询:

select emp.*,dept.* from emp, dept where emp.deptno=dept.deptno;
select emp.* from emp, dept where emp.deptno=dept.deptno;
select dept.* from emp, dept where emp.deptno=dept.deptno;
select ename,deptname from emp, dept where emp.deptno=dept.deptno;
select emp.* from emp, dept where emp.deptno=dept.deptno;
select emp.*, deptname from emp, dept where emp.deptno=dept.deptno;
select emp.*, dept.deptname from emp, dept where emp.deptno=dept.deptno;

4.7、记录联合

将两个表按照一定的查询条件查询出来之后,要把结果联合并到一起现实出来 关键词 union 和 union all
select * from t1 union|union all select * from t2 ...... union|union all select * from tn;

union all 是把结果合并到在一起;
union 是将union all后的记过进行了以此distinct,去重处理;
例如 将emp和dept中部门编号联合起来现实:

select deptno from emp union all select deptno from dept;
select deptno from emp union select deptno from dept;

DCL语句

例如 创建一个yanle数据库用户,对于sakila数据库中所有的表 select/insert 权限:

grant select, insert on sakila.* to 'yanle'@'localhost' identified by '123456';
GRANT ALL PRIVILEGES ON *.* TO 'user1'@'localhost';

FLUSH PRIVILEGES;

例如 收回权限

REVOKE ALL PRIVILEGES ON *.* FROM 'user1'@'localhost';

revoke insert on sakila.* from 'yanle'@'localhost';
revoke select on sakila.* from 'yanle'@'localhost';

03章、mysql数据类型

主要包括以下五大类:

整数类型:BIT、BOOL、TINY INT、SMALL INT、MEDIUM INT、 INT、 BIG INT

浮点数类型:FLOAT、DOUBLE、DECIMAL

字符串类型:CHAR、VARCHAR、TINY TEXT、TEXT、MEDIUM TEXT、LONGTEXT、TINY BLOB、BLOB、MEDIUM BLOB、LONG BLOB

日期类型:Date、DateTime、TimeStamp、Time、Year

其他数据类型:BINARY、VARBINARY、ENUM、SET、Geometry、Point、MultiPoint、LineString、MultiLineString、Polygon、GeometryCollection等

数值类型

1、整型

MySQL数据类型 含义(有符号)
tinyint(m) 1个字节 范围(-128~127)
smallint(m) 2个字节 范围(-32768~32767)
mediumint(m) 3个字节 范围(-8388608~8388607)
int(m) 4个字节 范围(-2147483648~2147483647)
bigint(m) 8个字节 范围(+-9.22*10的18次方)

取值范围如果加了unsigned,则最大值翻倍,如tinyint unsigned的取值范围为(0~256)。通常情况是保存非负数或者有较大上限值的时候使用。

对于整型数据,支持后面的小括号内指定现实的宽度。例如int(5) 表示当数值宽度小于5的时候,在数字前面填满宽度。默认快读为11。一般配合serofill使用。
如果插入值大于宽度限制,对插入的数据没有任何影响,还是会按照实际精度进行保存。

整数类型还有一个属性: AUTO_INCREMENT。 在需要产生唯一标识符或者顺序值的时候,就可以用到它。设置了这个属性的值,会从1开始,每行自动增加1。对于任何都想要是用AUTO_INCREMENT属性的列,
应该设置NOT NULL, 并定义为 PRIMARY KEY或者定义为UNIQUE键。
例如 创建一个auto_increment列:

create table if not exists AI(
  id int auto_increment not null primary key
);
create table if not exists AI(
  id int auto_increment not null, primary key(id)
);
CREATE TABLE IF NOT EXISTS AI(
  id int auto_increment not null , unique (id)
)

2、浮点型(float和double)

MySQL数据类型 含义
float(m,d) 单精度浮点型 8位精度(4字节) m总个数,d小数位
double(m,d) 双精度浮点型 16位精度(8字节) m总个数,d小数位

3、点数类型
浮点型在数据库中存放的是近似值,而定点类型在数据库中存放的是精确值。

MySQL数据类型 含义
decimal(m,d) 参数m<65 是总个数,d<30且 d<m 是小数位。

对于浮点型和点数类型来说,(m,d)规则都是一样的。 但是值得注意的是浮点数后面跟(m,d)是非标准用法。不建议这么使用。不指定精度的时候,贵根基实际精度来现实。
decimal不指定精度的时候,会默认整数位为10,小数位为0;

日期时间类型

MySQL数据类型 含义
date 日期 '2008-12-2'
time 时间 '12:25:36'
datetime 日期时间 '2008-12-2 22:06:44'
timestamp 自动存储记录修改时间
year 年 范围为1901~2155

可以用now()函数插入当前日期

timestamp
例如:timestamp的问题研究

create table if not exists t(
  id timestamp
);
insert into t value (null);
select id from t;   # 这个时候可以得到id的一个自动保存的时间

alter table t add id2 timestamp;
show create table t;

如果存在第二个timestamp类型。则默认设置为0;可以修改为其他常量日期,但是不能设置为current_timestamp, 以为mysql中timestamp只能有一列默认值为current_timestamp;

timestamp是受时区影响的,其他的类型倒是不会受时区影响
查看当前时区 show varialbes like 'time_zone'
例如我们修改库中的时区: set tiem_zone='+9:00'
插入某一列记录的时候,如果插入null 或者不明确给出赋值,那么就自动取系统默认值

例如 创建一个时间自增的表

create table if not exists test(
  id int(11) auto_increment not null,
  name varchar(100),
  age int(3),
  create_time date,
  update_time timestamp,
  primary key (id)
);
alter table test modify create_time datetime;
desc test;
insert into test(name, age, create_time)
values ('yanle1', 26, now());
select * from test;
update test set name='yanlele' where id=1;

datetime
是不严格的语法,允许很多种类型的时间格式,插入时间里面去。

补充时间比较大小

select * from product where add_time = '2013-01-12'

select * from product where Date(add_time) = '2013-01-12'

select * from product where date(add_time) between '2013-01-01' and '2013-01-31'

// 你还可以这样写:
select * from product where Year(add_time) = 2013 and Month(add_time) = 1

mysql> SELECT something FROM table 
WHERE TO_DAYS(NOW()) - TO_DAYS(date_col) <= 30;

字符串类型

01

CHAR 和 VARCHAR类型

char列的长度为创建表时候申明的长度,长度可以为0~255之间任何值;
varchar 接受长度更加长而已;
检索的时候char列删除了尾部的空格,而varchar保留了这些空格。
都是固定长度,指定了字符串长度了之后,就不能超过指定的长度。

例如 创建测试表

# 创建vc测试库
create table if not exists vc(v varchar(4), c char(4));

# 插入 'ab  '
insert into vc(v,c) values ('ab  ', 'ab  ');

# 现实结果
select length(v),length(c) from vc;

# 加上 + 更加可以看的清楚
select concat(v, '+'),concat(c,'+') from vc;

BINARY和VARBINARY类型

类似于 CHAR 和 VARCHAR,不同的是他们包含二进制字符串,而不包含非二进制字符串。通常来说,用不上

ENUM类型

它值范围需要在创建的时候通过美剧的方式现实指定, 而且每次插入的时候只允许从集合中取单个值,不能取多个值.
而且是忽略大小写的。会自动转为定义的类型。

例如

create table if not exists test(gender enum('M','F'));
insert into test (gender)
values ('m'),('1'),('f'),(null);
select * from test;
drop table if exists test;

SET类型

与enum类型,但是不同的地方是,可以一次性选择多个成员。
例如

create table if not exists test(col set('a','b','c','d'));
insert into test
values ('a,b'),('a,d,a'),('a,b'),('d');
select * from test;
drop table if exists test;

值得注意的是重复的成员集合只取一次;

04章、mysql中的运算符

算数运算符

+、-、#、/、%

比较运算符

02
03

比较结果为真,返回1, 为假返回0;
'BETWEEN' 的使用格式为 a BETWEEN min and max; 比较包含上下界相等的情况;
'IN' 使用格式为 a IN (value1, value2, ...)
'NULL' 使用格式为 a IS NULL 或者 a IS NOT NULL
'LIKE'' 使用格式为 a LIKE %123%,a中有'123'时,返回1; 例如: select 123456 like '123%', 123456 like '%123%', 123456 like '%321%';

逻辑运算符

04

位运算符

略。。。。。。。

05、常用函数

常用字符串函数

05
06

CONCAT(S1,S2,......): 包传入的参数连接成为一个字符串
字符串与null连接的结果都将是null

INSERT(str,x,y,instr): 字符串str从第X位开始,Y个字符串的子串替换为字符串instr
例如 select insert('beijing2008you', 12, 3, 'me');

LOWER(str)和UPPER(str): 把字符串转为大写或者小写

LEFT(str,x)和RIGHT(str,y): 分别返回字符串最左边的x字符串和最右边的y个字符;

LPAD(str,n,pad)和RPAD(str,n,pad): 用字符串pad对str最左边和左右边进行填充,直到长度为n个字符长度;

LTRIM(str)和RTRIM(str): 祛除字符串左和右侧的空格;

REPEAT(str,x): 返回str重复X次;

REPLACE(str,a,b): 用字符串b替换字符串str出现的字符串a;

TRIM(str): 祛除左右两侧空格;

SUBSTRING(str,x,y): 返回字符串str从x位置起,y个字符长度的字符串;

数值函数

07

日期和时间函数

08

流程函数 重要

09

例如 一个关于薪水问题的一个测试

create table if not exists salary(
  userid int(11) auto_increment,
  salary decimal(9,2),
  primary key (userid)
);
insert into salary (salary)
values (1000),
       (2000),
       (3000),
       (4000),
       (5000),
       (null);
select * from salary;
# 如果月薪2000以上的属于高工资,用height 表示,反之 用 low表示
select if(salary>2000, 'height', 'low') from salary;
# 如果出现null直接用0替换就是了
select ifnull(salary, 0) from salary;
# 实现高低工资的问题
select case when salary<=2000 then 'low' else 'height' end from salary;
# 薪水分为高中低三种情况
select case salary when 1000 then 'low' when 2000 then 'middle' else 'height' end from salary;
drop table if exists salary;

其他常用函数

10

06、图形化工具的使用

使用mysql世界上还有比datagrip还好用的工具吗?没有!!!
所以略过

07、补充知识点儿:查看mysql数据库及表编码格式

1、查看数据库编码格式
mysql> show variables like 'character_set_database';

2、查看数据表的编码格式
mysql> show create table <表名>;

3、创建数据库时指定数据库的字符集
mysql>create database <数据库名> character set utf8;

4、创建数据表时指定数据表的编码格式

create table tb_books (
    name varchar(45) not null,
    price double not null,
    bookCount int not null,
    author varchar(45) not null ) default charset = utf8;

5、修改数据库的编码格式
mysql>alter database <数据库名> character set utf8;

6、修改数据表格编码格式
mysql>alter table <表名> character set utf8;

7、修改字段编码格式

mysql>alter table <表名> change <字段名> <字段名> <类型> character set utf8;

mysql>alter table user change username username varchar(20) character set utf8 not null;

8、添加外键

mysql>alter table tb_product add constraint fk_1 foreign key(factoryid) references tb_factory(factoryid);
mysql>alter table <表名> add constraint <外键名> foreign key<字段名> REFERENCES <外表表名><字段名>;

9、删除外键

mysql>alter table tb_people drop foreign key fk_1;
mysql>alter table <表名> drop foreign key <外键名>;

Iterator 和 for...of 循环

Iterator 和 for...of 循环

1、Iterator 的概念

JavaScript 原有的表示 “ 集合 ” 的数据结构,主要是数组( Array )和对象( Object ), ES6 又添加了 Map 和 Set 。
这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是 Map , Map 的成员是对象。
这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器( Iterator )就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。
任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环, Iterator 接口主要供for...of消费。

Iterator 的遍历过程是这样的。

  • ( 1 )创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  • ( 2 )第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  • ( 3 )第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  • ( 4 )不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。
具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

2、数据结构的默认 Iterator 接口

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
在 ES6 中,有三类数据结构原生具备 Iterator 接口:数组、某些类似数组的对象、 Set 和 Map 结构。

实例:

    let arr = ['a', 'b', 'c'];
    let iter = arr[Symbol.iterator]();
    iter.next() // { value: 'a', done: false }
    iter.next() // { value: 'b', done: false }
    iter.next() // { value: 'c', done: false }
    iter.next() // { value: undefined, done: true }

上面提到,原生就部署 Iterator 接口的数据结构有三类,对于这三类数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

3、调用 Iterator 接口的场合

有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,还有几个别的场合。

3.1、解构赋值

对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
实例1:

    let set = new Set().add('a').add('b').add('c');
    let [x,y] = set;
    // x='a'; y='b'
    let [first, ...rest] = set;
    // first='a'; rest=['b','c'];

3.2、扩展运算符

扩展运算符( ... )也会调用默认的 iterator 接口。
实例2:

    //  例一
    var str = 'hello';
    [...str] // ['h','e','l','l','o']
    //  例二
    let arr = ['b', 'c'];
    ['a', ...arr, 'd']
    // ['a', 'b', 'c', 'd']

3.3、yield*

yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
实例3:

    let generator = function* () {
        yield 1;
        yield* [2,3,4];
        yield 5;
    };
    var iterator = generator();
    iterator.next() // { value: 1, done: false }
    iterator.next() // { value: 2, done: false }
    iterator.next() // { value: 3, done: false }
    iterator.next() // { value: 4, done: false }
    iterator.next() // { value: 5, done: false }
    iterator.next() // { value: undefined, done: true }

3.4、其他场合

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet() (比如new Map([['a',1],['b',2]]))
  • Promise.all()
  • Promise.race()

4、Iterator 接口与 Generator 函数

Symbol.iterator方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
实例:

    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
        yield 1;
        yield 2;
        yield 3;
    };
    [...myIterable] // [1, 2, 3]
    
    //  或者采用下面的简洁写法
    let obj = {
        * [Symbol.iterator]() {
            yield 'hello';
            yield 'world';
        }
    };
    for (let x of obj) {
        console.log(x);
    }
    // hello
    // world

5、for...of 循环 - 重点!!!

ES6 借鉴 C++ 、 Java 、 C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of 循环可以使用的范围包括数组、 Set 和 Map 结构、某些类似数组的对象(比如 arguments 对象、 DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

5.1、数组

数组原生具备 iterator 接口,for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。
实例1:

    const arr = ['red', 'green', 'blue'];
    let iterator = arr[Symbol.iterator]();
    
    for(let v of arr) {
        console.log(v); // red green blue
    }
    
    for(let v of iterator) {
        console.log(v); // red green blue
    }

JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。 ES6 提供for...of循环,允许遍历获得键值。
实例2:

    var arr = ['a', 'b', 'c', 'd'];
    
    for (let a in arr) {
        console.log(a); // 0 1 2 3
    }
    
    for (let a of arr) {
        console.log(a); // a b c d
    }

上面代码表明,for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法,参见《数组的扩展》章节。

实例3:for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。

    let arr = [3, 5, 7];
    arr.foo = 'hello';
    
    for (let i in arr) {
        console.log(i); // "0", "1", "2", "foo"
    }
    
    for (let i of arr) {
        console.log(i); // "3", "5", "7"
    }

5.2、Set 和 Map 结构

Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。
实例1:基本使用

    var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
    for (var e of engines) {
        console.log(e);
    }
    // Gecko
    // Trident
    // Webkit
    
    var es6 = new Map();
    es6.set("edition", 6);
    es6.set("committee", "TC39");
    es6.set("standard", "ECMA-262");
    for (var [name, value] of es6) {
        console.log(name + ": " + value);
    }
    4、通信类
    // committee: TC39
    // standard: ECMA-262

Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
实例2:

    let map = new Map().set('a', 1).set('b', 2);
    for (let pair of map) {
        console.log(pair);
    }
    // ['a', 1]
    // ['b', 2]
    
    for (let [key, value] of map) {
        console.log(key + ' : ' + value);
    }
    // a : 1
    // b : 2

5.3、计算生成的数据结构

有些数据结构是在现有数据结构的基础上,计算生成的。比如, ES6 的数组、 Set 、 Map 都部署了以下三个方法,调用后都返回遍历器对象。

  • entries() 返回一个遍历器对象,用来遍历[ 键名 , 键值 ]组成的数组。对于数组,键名就是索引值;对于 Set ,键名与键值相同。 Map 结构的iterator 接口,默认就是调用 entries 方法。
  • keys() 返回一个遍历器对象,用来遍历所有的键名。
  • values() 返回一个遍历器对象,用来遍历所有的键值。

实例:

    let arr = ['a', 'b', 'c'];
    
    for (let pair of arr.entries()) {
        console.log(pair);
    }
    // [0, 'a']
    // [1, 'b']
    // [2, 'c']

5.4、对象

对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。
实例:

    var es6 = {
        edition: 6,
        committee: "TC39",
        standard: "ECMA-262"
    };
    
    for (e in es6) {
        console.log(e);
    }
    // edition
    // committee
    // standard
    
    for (e of es6) {
        console.log(e);
    }
    // TypeError: es6 is not iterable

一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。

    for (var key of Object.keys(someObject)) {
        console.log(key + ": " + someObject[key]);
    }

另一个方法是使用 Generator 函数将对象重新包装一下。

    function* entries(obj) {
        for (let key of Object.keys(obj)) {
            yield [key, obj[key]];
        }
    }
    for (let [key, value] of entries(obj)) {
        console.log(key, "->", value);
    }
    // a -> 1
    // b -> 2
    // c -> 3

6、对比JS中的几种遍历:for forEach for...in for...of

理解 JavaScript 中的 for…of 循环

for...of 语句创建一个循环来迭代可迭代的对象。
在 ES6 中引入的 for...of 循环,以替代 for...in 和 forEach() ,并支持新的迭代协议。
for...of 允许你遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。
对象数据结构是不可以用于for...of 的

语法:

for (variable of iterable) {
    statement
}
  • variable:每个迭代的属性值被分配给该变量。
  • iterable:一个具有可枚举属性并且可以迭代的对象。

Arrays(数组)

Arrays(数组)就是类列表(list-like)对象。数组原型上有各种方法,允许对其进行操作,比如修改和遍历等操作。
下面手在一个数组上进行的 for...of 操作:

// array-example.js
const iterable = ['mini', 'mani', 'mo'];
 
for (const value of iterable) {
  console.log(value);
}
 
// Output:
// mini
// mani
// mo

Maps(映射)

Map 对象就是保存 key-value(键值) 对。对象和原始值可以用作 key(键)或 value(值)。
Map 对象根据其插入方式迭代元素。换句话说, for...of 循环将为每次迭代返回一个 key-value(键值) 数组。

// map-example.js
const iterable = new Map([['one', 1], ['two', 2]]);
 
for (const [key, value] of iterable) {
  console.log(`Key: ${key} and Value: ${value}`);
}
 
// Output:
// Key: one and Value: 1
// Key: two and Value: 2

Set(集合)

Set(集合) 对象允许你存储任何类型的唯一值,这些值可以是原始值或对象。
Set(集合) 对象只是值的集合。 Set(集合) 元素的迭代基于其插入顺序。
Set(集合) 中的值只能发生一次。如果您创建一个具有多个相同元素的 Set(集合) ,那么它仍然被认为是单个元素

// set-example.js
const iterable = new Set([1, 1, 2, 2, 1]);
 
for (const value of iterable) {
  console.log(value);
}
// Output:
// 1
// 2

String(字符串)

// string-example.js
const iterable = 'javascript';
 
for (const value of iterable) {
  console.log(value);
}
 
// Output:
// "j"
// "a"
// "v"
// "a"
// "s"
// "c"
// "r"
// "i"
// "p"
// "t"

Arguments Object(参数对象)

// arguments-example.js
function args() {
  for (const arg of arguments) {
    console.log(arg);
  }
}
 
args('a', 'b', 'c');
// Output:
// a
// b
// c

Generators(生成器)

// generator-example.js
function* generator(){ 
  yield 1; 
  yield 2; 
  yield 3; 
}
 
for (const g of generator()) { 
  console.log(g); 
}
 
// Output:
// 1
// 2
// 3

退出迭代

avaScript 提供了四种已知的终止循环执行的方法:break、continue、return 和 throw。让我们来看一个例子:

const iterable = ['mini', 'mani', 'mo'];
 
for (const value of iterable) {
  console.log(value);
  break;
}
 
// Output:
// mini

普通对象不可迭代

for...of 循环仅适用于迭代。 而普通对象不可迭代。 我们来看一下:

const obj = { fname: 'foo', lname: 'bar' };
 
for (const value of obj) { // TypeError: obj[Symbol.iterator] is not a function
    console.log(value);
}

在这里,我们定义了一个普通对象 obj ,并且当我们尝试 for...of 对其进行操作时,会报错:TypeError: obj[Symbol.iterator] is not a function。

我们可以通过将类数组(array-like)对象转换为数组来绕过它。该对象将具有一个 length 属性,其元素必须可以被索引。我们来看一个例子:

// object-example.js
const obj = { length: 3, 0: 'foo', 1: 'bar', 2: 'baz' };
 
const array = Array.from(obj);
for (const value of array) { 
    console.log(value);
}
// Output:
// foo
// bar
// baz

Array.from() 方法可以让我通过类数组(array-like)或可迭代对象来创建一个新的 Array(数组) 实例。

For…of vs For…in

for...of 更多用于特定于集合(如数组和对象),但不包括所有对象。
注意:任何具有 Symbol.iterator 属性的元素都是可迭代的。

for...in 不考虑构造函数原型的不可枚举属性。它只需要查找可枚举属性并将其打印出来。

git 常用命令汇总

git 常用命令汇总

名词

  • master: 默认开发分支
  • origin: 默认远程版本库
  • Index / Stage:暂存区
  • Workspace:工作区
  • Repository:仓库区(或本地仓库)
  • Remote:远程仓库

1、新建代码库

在当前目录新建一个git代码仓库: git init

新建一个目录,将其初始化为git代码仓库: git init [project-name]

下载一个项目和他的整个代码历史: git clone [url]

2、配置

显示当前git配置: git config --list

编辑git配置文件: git config -e [--global]

设置提交代码时候的用户信息:

git config [--global] user.name "[name]"     
git config [--global] user.emial "[emial address]"                        

3、增加/删除/修改文件

查看状态: git status

查看变更内容: git diff

添加指定文件到本地缓存区: git add [file1] [file2] ...

添加指定文件目录到本地缓存区,包含子目录: git add [dir]

添加当前目录的所有文件到本地缓存区: gti add .

添加每个变化文件前,都会要求确认。对同一个文件的多处变化,可以实现分次提交: git add -p

删除工作区文件,并且将这次删除放入本地缓存区: git rm [file1] [file2] ...

停止追踪指定文件,但是该文件会保留在工作区: git rm --cached [file]

更改文件,并且将这个改名放入本地缓存区: git mv [file-originame] [file-newname]

4、代码提交

提交缓存区到仓库: git commit -m [message]

提交缓存区指定文件到仓库: git commit [file1] [file2] ... -m [message]

提交工作区子长此commit之后的变化,直接到仓库区: git commit -a

提交是显示所有diff信息: git commit -v

使用一次新的commit,替换上一次的提交
如果代码没有任何新变化,则用来改写上一次commit的提交信息: git commit --amend -m [message]

重新做一次commit, 并包括指定文件的新变化: git commit --amend [file1] [file2] ...

5、分支

显示所有本地分支: git branch

列出所有远程分支: git branch -r

列出所有本地分支和远程分支: git branch -a

创建一个新分支,但是依然停留在当前分支: git branch [branch-name]

新建一个分支,与指定的远程分支建立追踪关系: git branch --track [branch] [remote-branch]

删除分支: git branch -d [branch-name]

删除远程分支:

git push origin --delete [branch-name]          
git branch -dr [remote/branch]

新建一个分支,并切换到改分支上面: git checkout -b [branch]

切换到指定分支,并更新工作区: git checkout [branch-name]

切换到删给一个分支: git checkout -

建立追踪关系,在现有分支和指定的远程分支之间: git branch --set-upstream [branch] [remote-branch]

提交新建的本地分支到远程: git push --set-upstream origin [branch-name]

合并指定分支到当前分支: git merge [branch]

衍合指定分支到当前分支: git rebase <branch>

选择一个commit, 合并进当前分支: git cherry-pick [commit]

6、标签

列出所有本地标签: git tag

基于最新提交创建标签: git tag <tagname>

删除标签: git tag -d <tagname>

删除远程tag: git push origin :refs/tags/[tagname]

查看tag信息: git show [tag]

提交指定tag: git push [remote] [tag]

提交所有tag: git push [remote] --tags

新建一个分支, 指定某个tag: git checkout -b [branch] [tag]

7、查看信息

查看有变更的文件: git status

显示当前分支的所有历史版本: git log

显示commit历史,以及每次commit发生变更的文件: git log --stat

搜索提交历史, 根据关键词: git log -S [keyword]

显示某个commit之后的所有变动,每个commit占据一行: git log [tag] HEAD --pretty=format:%s

显示某个commit之后的所有变动,其“提交说明”必须符合搜索条件: git log [tag] HEAD --grep feature

显示某个文件的版本历史, 包括文件改名:

git log --follow [file]
git whatchanged [file]

显示指定文件相关的每一次diff: git log -p [file]

显示过去5次提交: git log -5 --pertty --oneline

显示所有提交过的用户, 按提交次数排序: git shortlog -sn

显示指定文件是什么人在什么时间修改过: git blame [file]

显示缓存区和工作去的差异: git diff

显示暂存区和上一个commit的差异: git diff --cached [file]

显示工作区和当前分支最新的commit之间的差异: git diff HEAD

显示两次提交之间的差异: git diff [first-branch]...[second-branch]

显示今天提交了多少行代码 - 代码统计: git diff --shortstat "@{0 day ago}"

显示某次提交的元素数据和内容变化: git show [commit]

显示某次提交发生变化的文件: git show --name-only [commit]

显示某次提交时候,某个文件的内容: git show [commit]:[filename]

显示当然分支的最近几次提交: git reflog

8、远程操作

下载远程仓库的所有变动: git fetch [remote]

取回远程仓库的变化,并与本地分支合并: git pull [remote] [branch]

显示所有远程仓库: git remote -v

显示某个远程仓库的信息: git remote show [remote]

添加一个新的远程仓库,并命名: git remote add [shortname] [url]

上传本地指定分支到远程仓库: git push [remote] [branch]

强行推送当前分支到远程仓库,即便是有冲突: git push [remote] --force

推送所有分支到远程仓库: git push [remote] --all

9、撤销

撤销工作目录中所有未提交的文件的修改内容: git reset --head HEAD

撤销指定的未提交文件的修改内容: git checkout HEAD <file>

撤销指定的提交: git revert <commit>

退回到之前1天的版本: git log --before="1 days"

恢复暂存区的指定文件到工作区: git checkout [file]

恢复某个commit的文件到暂存区和工作区: git checkout [commit] [file]

恢复暂存区的所有文件到工作区: git checkout .

重置暂存区的指定文件,与上一次commit保持一致,但是工作区不变: git reset [file]

重置暂存区与工作区,与上一次commit保持一致: git reset --hard

重置当前分支的指针为commit,同事重置暂存区,但是工作区不变: git rest [commit]

重置当前分支的HEAD为指定commit, 同事重置暂存区和工作去,与指定commit一致: git rest --hard [commit]

重置当前HEAD为指定commit, 但保持暂存区和工作去不变: git rest --keep [commit]

创建一个新的commit, 用来撤销指定的commit。
后者所有变化都将被前者抵消,并且应用于当前分支: git revert [commit]

暂时将未提交的变化移除,稍后在移入:

git stash
git stash pop

10、其他

打包生成一个可供发布的压缩包: git archive

强制提交回退的代码: git push -f origin master;

一个回滚的方法: https://www.cnblogs.com/human/p/5128482.html

修改git commit message

修改最近的一次提交

# 修改最近提交的 commit 信息
$ git commit --amend --message="modify message by daodaotest" --author="jiangliheng <[email protected]>"

# 仅修改 message 信息
$ git commit --amend --message="modify message by daodaotest"

# 仅修改 author 信息
$ git commit --amend --author="jiangliheng <[email protected]>"

修改提交到远端的 commit message

比如要修改的commit是倒数第三条,使用下述命令
git rebase -i HEAD~3

把相对应的把pick改为edit, 然后:wq 保存退出, 接下来按照提示一路走下来就可以了。有提示的。

使用命令行:git commit --amend 就可以进入到修改message 的vim 里面去, 修改信息之后:wq 退出

在使用命令行: git rebase --continue

最后强行提交: git push --force

具体文章也可以参看这个文章: 如何修改Git commit的信息

处理提交commit revert 的情况

git log 找到提交的HEAD
撤销指定的提交: git revert <commit>
git push 就OK了

多个提交合并

git rebase -i commitId 或者 git rebase -i HEAD~n 这样可以检出我们需要的rebase
需要注意的是: s 命令是指针对新分支衍合到老分支

改了之后, 就可以修改合并的message 了

然后强行push : git push --force

说明: 如果废弃rebase : git rebase --abort

cherry-pick合并多个commit

1、使用方法以及作用

git cherry-pick可以选择某一个分支中的一个或几个commit(s)来进行操作(操作的对象是commit)。
例如,假设我们有个稳定版本的分支,叫v2.0,另外还有个开发版本的分支v3.0,我们不能直接把两个分支合并,
这样会导致稳定版本混乱,但是又想增加一个v3.0中的功能到v2.0中,这里就可以使用cherry-pick了。

就是对已经存在的commit 进行 再次提交;

使用方法如下: git cherry-pick <commit id>

查询commit id 的查询可以使用git log查询(查询版本的历史),最简单的语法如下: git log
详细的git log 语法如下: git log [<options>] [<since>..<until>] [[--] <path>...]
主要参数选项如下:
--p:按补丁显示每个更新间的差异
--stat:显示每次更新的修改文件的统计信息
--shortstat:只显示--stat中最后的行数添加修改删除统计
--name-only:尽在已修改的提交信息后显示文件清单
--name-status:显示新增、修改和删除的文件清单
--abbrev-commit:仅显示SHA-1的前几个字符,而非所有的40个字符
--relative-date:使用较短的相对时间显示(例如:"two weeks ago")
--graph:显示ASCII图形表示的分支合并历史
--pretty:使用其他格式显示历史提交信息

结果大概如下:

commit 0771a0c107dbf4c96806d22bbc6ef4c58dfe7075
Author: zhengcanrui <[email protected]>
Date:   Mon Aug 8 14:41:54 2016 +0800

    [modify] [what] commit的备注信息 

其中0771a0c107dbf4c96806d22bbc6ef4c58dfe7075就是我们的commit id
注意:当执行完 cherry-pick 以后,将会 生成一个新的提交;这个新的提交的哈希值和原来的不同,但标识名 一样;(commit id会变)

2、实践

首先切换到你要添加commit的分支,如:你要将A分支上面的commit添加到B分支上面,我们可以要先切换到B分支上面。
(注意:cherry-pick是一个本地的操作,假如你pull代码之后有人在A分支上有了新的commit,
需要你先pull代码在进行cherry-pick,原因及其错误提示请见最后)。

git checkout B
将0771a0c107dbf4c96806d22bbc6ef4c58dfe7075这个commit(提交)合并到B分支上面。
正常情况下,可以给出全部的commit id,也可以只给出前面的一段,只要你提交中没有这一段重复的就好,剩下的部分git会帮你填充。
git cherry-pick 0771a0c107dbf4c#将上面的commit id为0771a0c107dbf4c96806d22bbc6ef4c58dfe7075的提交添加到B分支上面

参看文档

commit优化

type代表某次提交的类型,比如是修复一个bug还是增加一个新的feature。所有的type类型如下:

  • feat: 新增feature
  • fix: 修复bug
  • docs: 仅仅修改了文档,比如README, CHANGELOG, CONTRIBUTE等等
  • style: 仅仅修改了空格、格式缩进、都好等等,不改变代码逻辑
  • refactor: 代码重构,没有加新功能或者修复bug
  • perf: 优化相关,比如提升性能、体验
  • test: 测试用例,包括单元测试、集成测试等
  • chore: 改变构建流程、或者增加依赖库、工具等
  • revert: 回滚到上一个版本

放弃已经commit但是没有push的代码

  • 如果已经用add 命令把文件加入stage了,就先需要从stage中撤销: git reset HEAD <file>...
  • 放弃工作区和index的改动,HEAD指针仍然指向当前的commit: git reset --hard HEADID

文件暂存

更改remote

git remote set-url origin XXXXXXX

git 命令终极文档

给已经存在的项目添加git

第一步: git init
第二步: git add .
第三步: git commit -m "Initial commit"
第四步:

输入:git remote add origin + 你的仓库地址
例如:git remote add origin https://git.oschina.net/hhh/GitDemo.git

第五步: git push -u origin master

强行同步远端

git本地即使有修改如何强制更新:

本地有修改和提交,如何强制用远程的库更新本地。我尝试过用git pull -f,总是提示 You have not concluded your merge. (MERGE_HEAD exists)。

我需要放弃本地的修改,用远程的库的内容就可以,应该如何做?傻傻地办法就是用心的目录重新clone一个,正确的做法是什么?

正确的做法应该是:

git fetch --all

git reset --hard origin/master // 远程分支名称

git fetch

只是下载远程的库的内容,不做任何的合并git reset 把HEAD指向刚刚下载的最新的版本

其他办法
git fetch -p

强行对齐 - 本地推送到远端

git push origin dev:master -f // 强行对齐开发分支到master

git checkout master --> git reset --hard origin/master // 拉去最新的master代码

强行对齐 - 远端对齐到本地

git pull <远程主机名> <远程分支名>:<本地分支名>

举一个例子: git pull --force origin master:master

删除某一次提交

git rebase -i commit_id //commit_id为想要删除的某次提交的前一个提交记录 id

然后删除想要删除的记录信息,或者把记录信息前面的pick 修改成drop,根据提示进行信息保存。

然后进行git 强制提交(确保已经取出分支保护)

git push --force

github----向开源框架提交pr的过程

GIT上fork的项目获取最新源代码

git clean的用法

git clean -n: 是一次clean的演习, 告诉你哪些文件会被删除. 记住他不会真正的删除文件, 只是一个提醒

git clean -f: 删除当前目录下所有没有track过的文件. 他不会删除.gitignore文件里面指定的文件夹和文件, 不管这些文件有没有被track过

git clean -f <path>: 删除指定路径下的没有被track过的文件

git clean -df: 删除当前目录下没有被track过的文件和文件夹

git clean -xf: 删除当前目录下所有没有track过的文件. 不管他是否是.gitignore文件里面指定的文件夹和文件

git 如何更新 fork 的项目到原项目的最新版本

1.查看远程的版本库地址

git remote -v 
origin  https://github.com/xx/spring-boot-demo.git (fetch)
origin  https://github.com/xx/spring-boot-demo.git (push)

2.添加原项目 git 地址到本地版本库

git remote add upstream https://github.com/xkcoding/spring-boot-demo.git

3.检查版本库是否添加成功

git remote -v
origin  https://github.com/xx/spring-boot-demo.git (fetch)
origin  https://github.com/xx/spring-boot-demo.git (push)
upstream        https://github.com/xx/spring-boot-demo.git (fetch)
upstream        https://github.com/xx/spring-boot-demo.git (push)

4.原项目更新内容同步到本地

git fetch upstream                             
remote: Enumerating objects: 83, done.
remote: Counting objects: 100% (83/83), done.
remote: Total 311 (delta 83), reused 83 (delta 83), pack-reused 228
Receiving objects: 100% (311/311), 331.79 KiB | 429.00 KiB/s, done.
Resolving deltas: 100% (102/102), completed with 39 local objects.
From https://github.com/xkcoding/spring-boot-demo
 * [new branch]      dev        -> upstream/dev
 * [new branch]      master     -> upstream/master
 * [new branch]      v-1.5.x    -> upstream/v-1.5.x

5.查看本地分支

git branch -a 
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/dev
  remotes/origin/master
  remotes/origin/v-1.5.x
  remotes/upstream/dev
  remotes/upstream/master
  remotes/upstream/v-1.5.x

6.同步更新内容到本地对应分支

git merge upstream/master

7.提交更新内容到 fork 地址

git push

如何完整迁移git仓库到另一个远程地址

项目中遇到git仓库迁移,很常见。如何把一个项目中所有的分支,tag等迁移到另一个仓库地址,
需要执行一个特别的克隆命令,然后镜像push到新的仓库地址。具体步骤如下:

1. 以bare的方式克隆老的仓库
git clone --bare https://github.com/exampleuser/old-repository.git

2. 镜像push到新的仓库地址
cd old-repository.git
git push --mirror https://github.com/exampleuser/new-repository.git

3. 在电脑中删掉老得仓库,把新的仓库重新拉下来
cd ..
rm -rf old-repository.git

其他参考

Symbol

Symbol

Symbol概述

JavaScript基本数据类型有6种:Undefined、Null、Boolean、String、Number、Object。
ES6新增了一种数据类型:Symbol,表示独一无二的值,Symbol最大的用途是用来定义对象的唯一属性名。
ES5的对象属性名都是字符串,容易造成属性名的冲突。
如使用了一个其他人提供的对象,但又想为其添加新的方法(mixin模式),那么新方法的名字有可能与已有方法产生冲突。
因此,需要保证每个属性的名字都是独一无二,以防止属性名的冲突。这就是ES6引入Symbol的原因。

Symbol值通过Symbol函数生成。

var symbol1 = Symbol();
var symbol2 = Symbol("Alice");
console.log(symbol1, symbol2) // 输出:Symbol() Symbol(Alice)

typeof运算符用于Symbol类型值,返回symbol。

console.log(typeof Symbol("Alice")) // 输出:symbol

Symbol类型的值是一个独一无二的值,Symbol函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的

console.log(Symbol() === Symbol()); // 输出:false
console.log(Symbol("Alice") === Symbol("Alice")); // 输出:false

Symbol不是一个构造函数,如果用new Symbol会报错(Symbol是一个原始类型的值,不是对象)。

var symbol = new Symbol(); // 报错:TypeError

由于Symbol值不是对象,所以不能添加属性。

var symbol = Symbol();
symbol.name = "Alice"; // 报错:TypeError

Symbol值可以显式转为字符串,也可以转为布尔值,但是不能转为数值。

var symbol = Symbol("Alice");
console.log(symbol.toString()); // 输出:Symbol(Alice)
console.log(Boolean(symbol)); // 输出:Symbol(Alice)
if (symbol)
	console.log("YES"); // 输出:Yes
console.log(Number(symbol)); // 报错:TypeError

作为对象属性名的Symbol

由于每一个Symbol值都是不相等的,这意味着Symbol值可以用于对象的属性名,
保证不会出现同名的属性,这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
对象的属性名可以有两种类型,一种是原来的字符串,另一种是新增的Symbol类型。
凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
通过方括号结构和Object.defineProperty,将对象的属性名指定为一个Symbol值。

方法一:

var name = Symbol();
var obj = {
	[name]: "Alice"
};

方法二:

var name = Symbol();
var obj = {};
obj[name] = "Alice";

方法三:

var obj = {};
Object.defineProperty(obj, name, { value: 'Alice' });

在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中,如果不放在方括号中,该属性名就是字符串,而不是代表的Symbol值。

var name = Symbol();
var obj1 = {
	[name]: "Alice"
};
var obj2 = {
	name: "Bruce"
};
console.log(obj1.name); // 输出:undefined
console.log(obj1[name]); // 输出:Alice
console.log(obj2.name); // 输出:Bruce
console.log(obj2[name]); // 输出:undefined

Symbol值作为对象属性名时,不能用点运算符。由于点运算符后面总是字符串,所以不会读取name作为标识名所指代的那个值,
导致属性名实际上是一个字符串,而不是一个Symbol值。

var obj = {};
var name = Symbol();
obj.name = 'Alice';
console.log(obj.name);
console.log(obj[name]);
console.log(obj['name']);

作为对象函数名的Symbol

var func = Symbol();
var obj = {
	func: function() {
		console.log("YES");
	}
};
obj.func(); // 输出:YES

获取对象属性的两种方法

  • Object.getOwnPropertySymbols()方法:返回只包含Symbol类型的属性名的数组
  • Object.getOwnPropertyNames()方法:返回只包含字符串类型的属性名的数组
var obj = {};
var age = Symbol("age");
var job = Symbol("job");
obj[age] = "Alice";
obj[job] = "student";
obj.age = 23;
var symbols = Object.getOwnPropertySymbols(obj);
var names = Object.getOwnPropertyNames(obj);
console.log(symbols.length); // 输出:2
console.log(symbols); // 输出:[Symbol(age), Symbol(job)]
console.log(obj[symbols[0]]); // 输出:Alice
console.log(names.length); // 输出:1
console.log(obj[names[0]]); // 输出:23

Symbol.for()和Symbol.keyFor()方法

Symbol.for()方法

类似于单例模式,首先在全局中搜索有没有以该参数为名称的Symbol值,如果有则返回该Symbol值,否则新建并返回一个以该参数为名称的Symbol值。

var symbol1 = Symbol.for('Alice');
var symbol2 = Symbol.for('Alice');
console.log(symbol1 === symbol2) // 输出:true

Symbol.keyFor()方法

返回一个已创建的Symbol类型值的key,实质是检测该Symbol是否已创建。

var symbol1 = Symbol.for("Alice");
console.log(Symbol.keyFor(symbol1)); // 输出:"Alice"
var symbol2 = Symbol("Alice");
console.log(Symbol.keyFor(symbol2)); // 输出:undefined

参考资料

数值的扩展

数值的扩展

1、Number.isFinite(), Number.isNaN()

Number.isFinite()用来检查一个数值是否为有限的( finite )。
Number.isNaN()用来检查一个值是否为NaN。
它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回false。
实例1:

    isFinite(25) // true
    isFinite("25") // true
    Number.isFinite(25) // true
    Number.isFinite("25") // false
    
    isNaN(NaN) // true
    isNaN("NaN") // true
    Number.isNaN(NaN) // true
    Number.isNaN("NaN") // false

2、Number.parseInt(), Number.parseFloat()

ES6 将全局方法parseInt()和parseFloat(),移植到 Number 对象上面,行为完全保持不变。
这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。
实例:

    // ES5 的写法
    parseInt('12.34') // 12
    parseFloat('123.45#') // 123.45
    
    // ES6 的写法
    Number.parseInt('12.34') // 12
    Number.parseFloat('123.45#') // 123.45

3、Number.isInteger()

Number.isInteger()用来判断一个值是否为整数。需要注意的是,在 JavaScript 内部,整数和浮点数是同样的储存方法,所以 3 和 3.0 被视为同一个值。

    Number.isInteger(25) // true
    Number.isInteger(25.0) // true
    Number.isInteger(25.1) // false
    Number.isInteger("15") // false
    Number.isInteger(true) // false

4、安全整数和 Number.isSafeInteger() - 非常冷门,不重要

JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

实例1:

    Number.MAX_SAFE_INTEGER === 9007199254740991
    // true
    Number.MIN_SAFE_INTEGER === -9007199254740991
    // true

实例2:Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

    Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
    Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
    Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
    Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false

实例3:如果只验证运算结果是否为安全整数,很可能得到错误结果。下面的函数可以同时验证两个运算数和运算结果。

    function trusty(left, right, result) {
        if (
            Number.isSafeInteger(left) &&
            Number.isSafeInteger(right) &&
            Number.isSafeInteger(result)
        ) {
            return result;
        }
        throw new RangeError('Operation cannot be trusted!');
    }
    
    trusty(9007199254740993, 990, 9007199254740993 - 990)
    // RangeError: Operation cannot be trusted!
    trusty(1, 2, 3)
    // 3

5、Math 对象的扩展

5.1、Math.trunc():方法用于去除一个数的小数部分,返回整数部分。

实例1:

    Math.trunc(4.1) // 4
    Math.trunc(4.9) // 4
    Math.trunc(-4.1) // -4
    Math.trunc(-4.9) // -4
    Math.trunc(-0.1234) // -0

对于非数值,Math.trunc内部使用Number方法将其先转为数值。

5.2、Math.sign()

参数为正数,返回 +1 ;
参数为负数,返回 -1 ;
参数为 0 ,返回 0 ;
参数为 -0 ,返回 -0;
其他值,返回 NaN 。

    Math.sign(-5) // -1
    Math.sign(5) // +1
    Math.sign(0) // +0
    Math.sign(-0) // -0
    Math.sign(NaN) // NaN
    Math.sign('foo'); // NaN
    Math.sign(); // NaN

5.3、Math.imul()

Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数

    Math.imul(2, 4) // 8
    Math.imul(-1, 8) // -8
    Math.imul(-2, -2) // 4

[数据结构] 学习javascript数据结构

学习javascript数据结构与算法

学习javascript数据结构与算法 [巴西] Loiane Groner 著 孙晓博等译

目录结构

01章、javascript基础

1.5、面向对象编程

一般来说创建对象有三种方式
方式1:

//方式1
let obj = new Object({});

方式2:

//方式2
let obj = {};
obj = {
    name: {
        first: 'Gandalf',
        last: 'the Grey'
    },
    address: 'Middle Earth'
};

方式3:

//方式3
function Book(title,pages, isbn) {
    this.title = title;
    this.pages = pages;
    this.isbn = isbn;
}
let book =new Book('title', 'page', 'isbn');
console.log(book.title); //输出书名
book.title = 'new title'; //修改书名
console.log(book.title); //输出新的书名
//使用原型扩展
Book.prototype.printTitle = function() {
    console.log(this.title)
};
console.log(book);
console.log(book.__proto__);

03章、栈

3.1、栈的创建

/**
   push(element(s)) :添加一个(或几个)新元素到栈顶。
   pop() :移除栈顶的元素,同时返回被移除的元素。
   peek() :返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)。
   isEmpty() :如果栈里没有任何元素就返回 true ,否则返回 false 。
   clear() :移除栈里的所有元素。
   size() :返回栈里的元素个数。这个方法和数组的 length 属性很类似。
 * @constructor
 */
let Stack = function() {
    let items = [];
    this.push = function (element) {
        items.push(element)
    };
    this.pop = function () {
        return items.pop();
    };
    this.peek = function () {
        return items[items.length - 1]
    };
    this.isEmpty = function() {
        return items.length === 0
    };
    this.clear = function() {
        items = [];
    };
    this.print = function() {
        console.log(items.toString());
    };
    this.size = function() {
        return items.length;
    }
};

可以去看如下的示例代码
堆栈的一个简单使用

3.2、从十进制到二进制

我们在十进制转为2进制的时候,就需要使用到上面的堆栈对象来实现,我们可以吧堆栈直接方封装为一个模块,然后通过module.exports = Stack这种方式抛出

let Stack = require('../01、栈的创建/index');
//十进制转换为2进制
function divideBy2(decNumber) {
    let remStack = new Stack(), rem, binaryString = '';
    while (decNumber > 0) {
        rem = Math.floor(decNumber % 2);
        remStack.push(rem);
        decNumber = Math.floor(decNumber / 2);
    }
    while (!remStack.isEmpty()) {
        binaryString += remStack.pop().toString()
    }
    return binaryString;
}

还可以创建另外的一个方法,让我们的十进制可以转为其他进制数

//十进制转为其他进制
function baseConverter(decNumber, base) {
    let remStack = new Stack(), rem, baseString = '', digits = '0123456789ABCDEF';
    while (decNumber > 0) {
        rem = Math.floor(decNumber % base);
        remStack.push(rem);
        decNumber = Math.floor(decNumber / base)
    }
    while (!remStack.isEmpty()) {
        baseString += digits[remStack.pop()];
    }
    return baseString;
}
console.log(baseConverter(100345, 2)); //输出11000011111111001
console.log(baseConverter(100345, 8)); //输出303771
console.log(baseConverter(100345, 16)); //输出187F9

04章、队列

4.1、创建队列

/**
   enqueue(element(s)) :向队列尾部添加一个(或多个)新的项。
   dequeue() :移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。
   front() :返回队列中第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息——与 Stack 类的 peek 方法非常类似)。
   isEmpty() :如果队列中不包含任何元素,返回 true ,否则返回 false 。
   size() :返回队列包含的元素个数,与数组的 length 属性类似。
 * @constructor
 */
function Queue() {
    let items = [];
    this.enqueue = function(element) {
        items.push(element)
    };
    this.dequeue = function() {
        return items.shift()
    };
    this.front = function() {
        return items[0]
    };
    this.isEmpty = function() {
        return items.length === 0
    };
    this.clear = function() {
        items = [];
    };
    this.size = function() {
        return items.length
    };
    this.print = function() {
        console.log(items.toString())
    }
}
module.exports = Queue;

4.2、优先队列

实现一个优先队列,有两种选项:设置优先级,然后在正确的位置添加元素;或者用入列操作添加元素,然后按照优先级移除它们。在这个示例中,我们将会在正确的位置添加元素,因此可以对它们使用默认的出列操作

function PriorityQueue() {
    let items = [];

    function QueueElement(element, priority) { // {1}
        this.element = element;
        this.priority = priority;
    }

    this.enqueue = function (element, priority) {
        let queueElement = new QueueElement(element, priority);
        if (this.isEmpty()) {
            items.push(queueElement); // {2}
        } else {
            let added = false;
            for (let i = 0; i < items.length; i++) {
                if (queueElement.priority <
                    items[i].priority) {
                    items.splice(i, 0, queueElement); // {3}
                    added = true;
                    break; // {4}
                }
            }
            if (!added) { //{5}
                items.push(queueElement);
            }
        }
    };

    this.enqueue = function(element) {
        items.push(element)
    };

    this.dequeue = function() {
        return items.shift()
    };

    this.front = function() {
        return items[0]
    };

    this.isEmpty = function() {
        return items.length === 0
    };

    this.clear = function() {
        items = [];
    };

    this.size = function() {
        return items.length
    };

    this.print = function() {
        console.log(items.toString())
    }
}

module.exports = PriorityQueue

测试示例:

let PriorityQueue = require('./index');

let priorityQueue = new PriorityQueue();
priorityQueue.enqueue("John", 2);
priorityQueue.enqueue("Jack", 1);
priorityQueue.enqueue("Camila", 1);
priorityQueue.print();

示例请见

4.3、循环队列

还有另一个修改版的队列实现,就是循环队列。循环队列的一个例子就是击鼓传花游戏(HotPotato)。在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈结束游戏。重复这个过程,直到只剩一个孩子(胜者)。
在下面这个示例中,我们要实现一个模拟的击鼓传花游戏

05章、链表

要存储多个元素,数组(或列表)可能是最常用的数据结构。正如本书之前提到过的,每种
语言都实现了数组。这种数据结构非常方便,提供了一个便利的 [] 语法来访问它的元素。然而,
这种数据结构有一个缺点:(在大多数语言中)数组的大小是固定的,从数组的起点或中间插入
或移除项的成本很高,因为需要移动元素(尽管我们已经学过的JavaScript的 Array 类方法可以帮
我们做这些事,但背后的情况同样是这样)。

5.1、创建一个链表

以下是我们的 LinkedList类的骨架:

function LinkedList() {
var Node = function(element){ // {1}
this.element = element;
this.next = null;
};
var length = 0; // {2}
var head = null; // {3}
this.append = function(element){};
this.insert = function(position, element){};
this.removeAt = function(position){};
this.remove = function(element){};
this.indexOf = function(element){};
this.isEmpty = function() {};
this.size = function() {};
this.toString = function(){};
this.print = function(){};
}

具体实现如下:请见1、创建一个链表

5.2、双向链表

链表有多种不同的类型,这一节介绍双向链表。双向链表和普通链表的区别在于,在链表中,
一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,
另一个链向前一个元素

略。。。。。。。。。。

06章、集合

迄今为止,我们已经学习了数组(列表)、栈、队列和链表(及其变种)等顺序数据结构。在这一章中,我们要学习集合这种数据结构。
集合是由一组无序且唯一(即不能重复)的项组成的。这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
在深入学习集合的计算机科学实现之前,我们先看看它的数学概念。在数学中,集合是一组不同的对象(的集)。
比如说,一个由大于或等于0的整数组成的自然数集合:N = {0, 1, 2, 3, 4, 5, 6, …}。集合中的对象列表用“{}”(大括号)包围。

还有一个概念叫空集。空集就是不包含任何元素的集合。比如24和29之间的素数集合。由于24和29之间没有素数(除了1和自身,没有其他正因数的大于1的自然数),这个集合就是空集。空集用“{ }”表示。

你也可以把集合想象成一个既没有重复元素,也没有顺序概念的数组。

在数学中,集合也有并集、交集、差集等基本操作。在这一章中我们也会介绍这些操作。

6.1、创建一个集合

目前的JavaScript实现是基于2011年6月发布的ECMAScript 5.1(现代浏览器均已支持),它包
括了我们在之前章节已经提到过的 Array 类的实现。ECMAScript 6(官方名称ECMAScript 2015,
2015年6月发布)包括了 Set 类的实现。

在这一章中,我们要实现的类就是以ECMAScript 6中 Set 类的实现为基础的。
 add(value) :向集合添加一个新的项。
 remove(value) :从集合移除一个值。
 has(value) :如果值在集合中,返回 true ,否则返回 false 。
 clear() :移除集合中的所有项。
 size() :返回集合所包含元素的数量。与数组的 length 属性类似。
 values() :返回一个包含集合中所有值的数组。

class Set{
    constructor() {
        this.items = {};
    }

    has(value) {
        return this.items.hasOwnProperty(value);
    }

    add(value) {
        if(!this.has(value)) {
            this.items[value] = value;
            return true;
        }
        return false;
    }

    remove(value) {
        if(this.has(value)) {
            delete this.items[value];
            return true;
        }
        return false;
    }

    clear() {
        this.items = {};
    }

    size() {
        return Object.keys(this.items).length;
    }

    values() {
        return Object.keys(this.items);
    }
}

module.exports = Set;

完整示例请见:01、创建一个集合

6.2、集合操作

对集合可以进行如下操作。
 并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合。
 交集:对于给定的两个集合,返回一个包含两个集合**有元素的新集合。
 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合。
 子集:验证一个给定集合是否是另一集合的子集。

6.2.1、并集
并集的数学概念,集合A和B的并集,表示为A∪B,定义如下:
A∪B = { x | x ∈ A∨x ∈ B }
意思是x(元素)存在于A中,或x存在于B中。

现在来实现 Set 类的 union 方法:

union(otherSet) {
    let unionSet = new Set();
    let values = this.values();

    for (let i = 0; i < values.length; i++) {
        unionSet.add(values[i]);
    }

    values = otherSet.values();
    for (let i = 0; i < values.length; i++) {
        unionSet.add(values[i]);
    }

    return unionSet;
}

6.2.2、交集
交集的数学概念,集合A和B的交集,表示为A∩B,定义如下:
A∩B = { x | x ∈ A∧x ∈ B }
意思是x(元素)存在于A中,且x存在于B中。

具体实现:

// 交集
intersection(otherSet) {
    let intersectionSet = new Set();
    let values = this.values();

    for(let i = 0; i < values.length; i++) {
        if(otherSet.has(values[i])) {
            intersectionSet.add(values[i]);
        }
    }
    return intersectionSet;
}

6.2.3、差集
差集的数学概念,集合A和B的差集,表示为A - B,定义如下:
A-B = { x | x ∈ A ∧ x  B }
意思是x(元素)存在于A中,且x不存在于B中。

现在来实现 Set 类的 difference 方法:

difference(otherSet) {
    let difference = new Set();
    let values = this.values();

    for(let i = 0;i < values.length; i++) {
        if(!otherSet.has(values[i])) {
            difference.add(values[i]);
        }
    }
    return difference;
}

6.2.4、子集
我们要介绍的最后一个集合操作是子集。子集的数学概念,集合A是B的子集(或集合B包含了A),表示为A⊆B,定义如下:
∀x { x ∈ A → x ∈ B }
意思是集合A中的每一个x(元素),也需要存在于B中。

现在来实现 Set 类的 subset 方法:

// 检验是否为子集
subset(otherSet) {
    if (this.size() > otherSet.size()) {
        return false;
    } else {
        let values = this.values();
        for (let i = 0; i < values.length; i++) {
            if(!otherSet.has(values[i])) {
                return false;
            }
        }
        return true;
    }
}

完整示例请见:02、集合操作

07章、字典和散列表

字典和散列表来存储唯一值(不重复的值)的数据结构。

集合、字典和散列表可以存储不重复的值。在集合中,我们感兴趣的是每个值本身,并把它当作主要元素。在字典中,我们用[键,值]的形式来存储数据。
在散列表中也是一样(也是以[键,值]对的形式来存储数据)。但是两种数据结构的实现方式略有不同,本章中将会介绍。

7.1、字典

集合表示一组互不相同的元素(不重复的元素)。在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。
字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称作映射。

在本章中,我们会介绍几个在现实问题上使用字典数据结构的例子:一个实际的字典(单词和它们的释义)以及一个地址簿。

7.1.1 创建一个字典
与 Set 类相似,ECMAScript 6同样包含了一个 Map 类的实现,即我们所说的字典。

这是我们的 Dictionary 类的骨架:
 set(key,value) :向字典中添加新元素。
 remove(key) :通过使用键值来从字典中移除键值对应的数据值。
 has(key) :如果某个键值存在于这个字典中,则返回 true ,反之则返回 false 。
 get(key) :通过键值查找特定的数值并返回。
 clear() :将这个字典中的所有元素全部删除。
 size() :返回字典所包含元素的数量。与数组的 length 属性类似。
 keys() :将字典所包含的所有键名以数组形式返回。
 values() :将字典所包含的所有数值以数组形式返回。

代码实现:

class Dictionary {
    constructor() {
        this.items = {};
    }

    has(key) {
        return key in this.items;
    }

    set(key, value) {
        this.items[key] = value;
    }

    remove(key) {
        if (this.has(key)) {
            delete this.items[key];
            return true;
        }
        return false;
    }

    get(key) {
        return this.has(key) ? this.items[key] : undefined;
    }

    values(key) {
        let values = [];
        for (let k in this.items) {
            if (this.has(k)) {
                values.push(this.items[k])
            }
        }
        return values;
    }

    clear() {
        this.items = {};
    }

    size() {
        return Object.keys(this.items).length;
    }

    keys() {
        return Object.keys(this.items);
    }

    getItems() {
        return this.items;
    }
}

module.exports = Dictionary;

完整示例请见:01、字典

7.2、散列表

在本节中,你将会学到 HashTable 类,也叫 HashMap 类,是 Dictionary 类的一种散列表实现方式。
散列算法的作用是尽可能快地在数据结构中找到一个值。在之前的章节中,你已经知道如果要在数据结构中获得一个值(使用 get 方法),
需要遍历整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,
然后返回值在表中的地址。
举个例子,我们继续使用在前一节中使用的电子邮件地址簿。我们将要使用最常见的散列函数——“lose lose”散列函数,方法是简单地将每个键值中的每个字母的ASCII值相加。
散列表7-2-1

7.2.1、创建一个散列表
搭建类的骨架开始:
 put(key,value) :向散列表增加一个新的项(也能更新散列表)。
 remove(key) :根据键值从散列表中移除值。
 get(key) :返回根据键值检索到的特定的值。

具体实现:

class HashTable {
    constructor() {
        this.table = [];
    }

    loseloseHashCode(key) {
        let hash = 0;
        for(let i = 0; i < key.length; i++) {
            hash +=key.charCodeAt(i);
        }
        return hash % 37;
    }

    put(key, value) {
        let position = this.loseloseHashCode(key);
        console.log(position + ' - ' + key);
        this.table[position] = value;
    }
    
    get(key) {
        return this.table[this.loseloseHashCode(key)]
    }
    
    remove(key) {
        this.table[this.loseloseHashCode(key)] = undefined;
    }
}

完整示例和测试请见:02、散列表

7.2.2 散列表和散列集合:
散列表和散列映射是一样的,我们已经在本章中介绍了这种数据结构。

在一些编程语言中,还有一种叫作散列集合的实现。散列集合由一个集合构成,但是插入、
移除或获取元素时,使用的是散列函数。我们可以重用本章中实现的所有代码来实现散列集合,
不同之处在于,不再添加键值对,而是只插入值而没有键。例如,可以使用散列集合来存储所有
的英语单词(不包括它们的定义)。和集合相似,散列集合只存储唯一的不重复的值。

7.2.3 处理散列表中的冲突:
有时候,一些键会有相同的散列值。不同的值在散列表中对应相同位置的时候,我们称其为冲突。
例如,我们看看下面的代码会得到怎样的输出结果:

var hash = new HashTable();
hash.put('Gandalf', '[email protected]');
hash.put('John', '[email protected]');
hash.put('Tyrion', '[email protected]');
hash.put('Aaron', '[email protected]');
hash.put('Donnie', '[email protected]');
hash.put('Ana', '[email protected]');
hash.put('Jonathan', '[email protected]');
hash.put('Jamie', '[email protected]');
hash.put('Sue', '[email protected]');
hash.put('Mindy', '[email protected]');
hash.put('Paul', '[email protected]');
hash.put('Nathan', '[email protected]');

输出结果如下:

19 - Gandalf
29 - John
16 - Tyrion
16 - Aaron
13 - Donnie
13 - Ana
5 - Jonathan
5 - Jamie
5 - Sue
32 - Mindy
32 - Paul
10 – Nathan

Tyrion 和 Aaron 有相同的散列值( 16 )。 Donnie 和 Ana 有相同的散列值( 13 ),Jonathan 、 Jamie 和 Sue 有相同的散列值( 5 ), Mindy 和 Paul 也有相同的散列值( 32 )。

处理冲突有几种方法:分离链接、线性探查和双散列法。在本书中,我们会介绍前两种方法。

7.2.3.1、分离链接
分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。它是解决冲突的最简单的方法,但是它在 HashTable 实例之外还需要额外的存储空间。
7-2-2
在位置5上,将会有包含三个元素的 LinkedList 实例;在位置13、16和32上,将会有包含两个元素的 LinkedList 实例;在位置10、19和29上,将会有包含单个元素的 LinkedList 实例。

为了实现一个使用了分离链接的 HashTable 实例,我们需要一个新的辅助类来表示将要加入LinkedList 实例的元素。我们管它叫 ValuePair 类(在 HashTable 类内部定义):

class ValuePair {
    constructor(key, value) {
        this.key = key;
        this.value = value;
    }
    toString() {
        return `[ ${this.key} - ${this.value} ]`
    }
}

这个类只会将 key 和 value 存储在一个 Object 实例中。我们也重写了 toString 方法,以便之后在浏览器控制台中输出结果。

完整代码请见:index2
完整测试代码请见:test2

7.2.3.2、线性探查
另一种解决冲突的方法是线性探查。当想向表中某个位置加入一个新元素的时候,如果索引为index的位置已经被占据了,
就尝试index+1的位置。如果index+1的位置也被占据了,就尝试index+2的位置,以此类推。
让我们继续实现需要重写的三个方法。
第一个是 put 方法:

put(key, value) {
    let position = this.loseloseHashCode(key);
    if(this.table[position] === undefined) {
        this.table[position] = new ValuePair(key ,value);
    } else {
        let index = ++position;
        while (this.table[index] !==undefined) {
            index ++;
        }
        this.table[index] = new ValuePair(key, value);
    }
}

如果再次执行 test2

linkedList.put('Gandalf', '[email protected]');
linkedList.put('John', '[email protected]');
linkedList.put('Tyrion', '[email protected]');
linkedList.put('Aaron', '[email protected]');
linkedList.put('Donnie', '[email protected]');
linkedList.put('Ana', '[email protected]');
linkedList.put('Jonathan', '[email protected]');
linkedList.put('Jamie', '[email protected]');
linkedList.put('Sue', '[email protected]');
linkedList.put('Mindy', '[email protected]');
linkedList.put('Paul', '[email protected]');
linkedList.put('Nathan', '[email protected]');

节中插入数据的代码,下图展示使用了线性探查的散列表的最终结果:
7-2-3

让我们来模拟一下散列表中的插入操作。
(1) 试着插入Gandalf。它的散列值是19,由于散列表刚刚被创建,位置19还是空的——可以在这里插入数据。
(2) 试着在位置29插入John。它也是空的,所以可以插入这个姓名。
(3) 试着在位置16插入Tyrion。它是空的,所以可以插入这个姓名。
(4) 试着插入Aaron,它的散列值也是16。位置16已经被Tyrion占据了,所以需要检查索引值为position+1的位置(16+1)。位置17是空的,所以可以在位置17插入Aaron。
(5) 接着,试着在位置13插入Donnie。它是空的,所以可以插入这个姓名。
(6) 想在位置13插入Ana,但是这个位置被占据了。因此在位置14进行尝试,它是空的,所以可以在这里插入姓名。
(7) 然后,在位置5插入Jonathan,这个位置是空的,所以可以插入这个姓名。
(8) 试着在位置5插入Jamie,但是这个位置被占了。所以跳至位置6,这个位置是空的,因此可以在这个位置插入姓名。
(9) 试着在位置5插入Sue,但是位置被占据了。所以跳至位置6,但也被占了。接着跳至位置7,这里是空的,所以可以在这里插入姓名。

第二个 get 方法:

get(key) {
    let position = this.loseloseHashCode(key);
    if(this.table[position] !== undefined) {
        if(this.table[position].key === key) {
            return table[position].value;
        } else {
            let index = ++position;
            while (this.table[index] === undefined || this.table[index].key !== key) {
                index++
            }
            if(this.table[index].key === key) {
                return this.table[index].value;
            }
        }
    }
}

第三个方法 remove :

remove(key) {
    let position = this.loseloseHashCode(key);
    if(this.table[position] !== undefined) {
        if(this.table[position].key === key) {
            this.table[position] = undefined;
            return true
        } else {
            let index = ++position;
            while (this.table[index] === undefined || this.table[index].key !== key) {
                index++
            }
            if(this.table[index].key === key) {
                this.table[index] = undefined;
                return true;
            }
        }
    }
    return false;
}

具体代码实现请见:index3.js
测试请见:test3.js

7.2.4、创建更好的散列函数
们实现的“lose lose”散列函数并不是一个表现良好的散列函数,因为它会产生太多的冲突。如果我们使用这个函数的话,会产生各种各样的冲突。
一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),当然也包括较低的冲突可能性。
我们可以在网上找到一些不同的实现方法,或者也可以实现自己的散列函数。

djb2(key) {
    let hash = 5381;
    for (let i = 0; i < key.length; i++) {
        hash = hash * 33 + key.charCodeAt(i);
    }
    return hash % 1013;
}

08章、树

树是一种分层数据的抽象模型。现实生活中最常见的树的例子是家谱,或是公司的组织架构图,如下图所示:
8-01

8.1、树的相关术语

一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点:
8-02
位于树顶部的节点叫作根节点(11)。它没有父节点。树中的每个元素都叫作节点,节点分为内部节点和外部节点。
至少有一个子节点的节点称为内部节点(7、5、9、15、13和20是内部节点)。
没有子元素的节点称为外部节点或叶节点(3、6、8、10、12、14、18和25是叶节点)。

一个节点可以有祖先和后代。一个节点(除了根节点)的祖先包括父节点、祖父节点、曾祖父节点等。
一个节点的后代包括子节点、孙子节点、曾孙节点等。例如,节点5的祖先有节点7和节点11,后代有节点3和节点6。

有关树的另一个术语是子树。子树由节点和它的后代构成。例如,节点13、12和14构成了上图中树的一棵子树。

节点的一个属性是深度,节点的深度取决于它的祖先节点的数量。比如,节点3有3个祖先节点(5、7和11),它的深度为3。

树的高度取决于所有节点深度的最大值。一棵树也可以被分解成层级。根节点在第0层,它的子节点在第1层,以此类推。上图中的树的高度为3(最大高度已在图中表示——第3层)。

8.2、二叉树和二叉搜索树

二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。这些定义有助于我们写出更高效的向/从树中插入、查找和删除节点的算法。二叉树在计算机科学中的应用非常广泛。

二叉搜索树(BST)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或者等于)的值。上一节的图中就展现了一棵二叉搜索树。

8.2.1 创建 BinarySearchTree 类
结构申明:

class Node {
    constructor(key) {
        this.key = key;
        this.left = null;
        this.right = null;
    }
}

class BinarySearchTree {
    constructor() {
        this.root = null;
    }
}

二叉搜索树数据结构的组织方式:
8-03

具体实现的方法:
 insert(key) :向树中插入一个新的键。
 search(key) :在树中查找一个键,如果节点存在,则返回 true ;如果不存在,则返回false 。
 inOrderTraverse :通过中序遍历方式遍历所有节点。
 preOrderTraverse :通过先序遍历方式遍历所有节点。
 postOrderTraverse :通过后序遍历方式遍历所有节点。
 min :返回树中最小的值/键。
 max :返回树中最大的值/键。
 remove(key) :从树中移除某个键。

8.2.2 向树中插入一个键
第一步是创建用来表示新节点的 Node 类实例(行 {1} )。只需要向构造函数传递我们想用来插入树的节点值,它的左指针和右指针的值会由构造函数自动设置为 null 。
第二步要验证这个插入操作是否为一种特殊情况。这个特殊情况就是我们要插入的节点是树的第一个节点(行 {2} )。如果是,就将根节点指向新节点。
第三步是将节点加在非根节点的其他位置。这种情况下,需要一个私有的辅助函数(行 {3} ),函数定义如下:

class Tool {
    static insertNode(node, newNode) {
        if(newNode.key < node.key) {
            if(node.left === null) {
                node.left = newNode;
            }else {
                this.insertNode(node.left, newNode);
            }
        } else {
            if(node.right === null) {
                node.right = newNode;
            } else {
                this.insertNode(node.right, newNode);
            }
        }
    }
}

下面是这个函数实现的步骤。
 如果树非空,需要找到插入新节点的位置。因此,在调用 insertNode 方法时要通过参数
传入树的根节点和要插入的节点。

 如果新节点的键小于当前节点的键(现在,当前节点就是根节点)(行 {4} ),那么需要检
查当前节点的左侧子节点。如果它没有左侧子节点(行 {5} ),就在那里插入新的节点。
如果有左侧子节点,需要通过递归调用 insertNode 方法(行 {7} )继续找到树的下一层。
在这里,下次将要比较的节点将会是当前节点的左侧子节点。

 如果节点的键比当前节点的键大,同时当前节点没有右侧子节点(行 {8} ),就在那里插
入新的节点(行 {9} )。如果有右侧子节点,同样需要递归调用 insertNode 方法,但是要
用来和新节点比较的节点将会是右侧子节点。

现在,来考虑下图所示树结构的情况:
8-04

let tree =new BinarySearchTree();
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(25);

// 同时我们想要插入一个值为 6 的键,执行下面的代码:
tree.insert(6);

下面的步骤将会被执行。
(1) 树不是空的,行 {3} 的代码将会执行。 insertNode 方法将会被调用( root, key[6] )。
(2) 算法将会检测行 {4} ( key[6] < root[11] 为真),并继续检测行 {5} ( node.left[7]不是 null ),
然后将到达行 {7} 并调用 insertNode ( node.left[7], key[6] )。
(3) 将再次进入 insertNode 方法内部,但是使用了不同的参数。它会再次检测行 {4} ( key[6]< node[7] 为真),
然后再检测行 {5} ( node.left[5] 不是 null ),接着到达行 {7} ,调用insertNode ( node.left[5], key[6] )。
(4) 将再一次进入 insertNode 方法内部。它会再次检测行 {4} ( key[6] < node[5] 为假),
然后到达行 {8} ( node.right 是 null ——节点5没有任何右侧的子节点),然后将会执行行 {9} ,在节点 5 的右侧子节点位置插入键 6 。
(5) 然后,方法调用会依次出栈,代码执行过程结束。

这是插入键6后的结果:
8-05

8.3 树的遍历

遍历一棵树是指访问树的每个节点并对它们进行某种操作的过程。但是我们应该怎么去做呢?
应该从树的顶端还是底端开始呢?从左开始还是从右开始呢?访问树的所有节点有三种方式:中序、先序和后序

8.3.1 中序遍历
以从最小到最大的顺序访问所有节点。中序遍历的一种应用就是对树进行排序操作。
具体实现:

class BinarySearchTree{
    // ......
    inOrderTraverse(callback) {
        Tool.inOrderTraverseNode(this.root, callback);
    }
    //......
}

class Tool {
    //......
    static inOrderTraverseNode(node, callback) {
        if(node !== null) {
            this.inOrderTraverseNode(node.left, callback);
            callback(node.key);
            this.inOrderTraverseNode(node.right, callback);
        }
    }
}

inOrderTraverse 方法接收一个回调函数作为参数。回调函数用来定义我们对遍历到的每个节点进行的操作由于我们在BST中最常实现的算法是递归,
这里使用了一个私有的辅助函数,来接收一个节点和对应的回调函数作为参数。

要通过中序遍历的方法遍历一棵树,首先要检查以参数形式传入的节点是否为 null

测试执行:

function printNode(value) {
    console.log(value)
}
tree.inOrderTraverse(printNode);

下面的图描绘了 inOrderTraverse 方法的访问路径:
8-06

8.3.2 先序遍历
先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。
具体实现:

class BinarySearchTree{
    // ......
    // 中序遍历
    inOrderTraverse(callback) {
        Tool.inOrderTraverseNode(this.root, callback);
    }
    //......
}

class Tool {
    //......
    static preOrderTraverseNode(node, callback) {
        if(node !== null) {
            callback(node.key);
            this.preOrderTraverseNode(node.left, callback);
            this.preOrderTraverseNode(node.right, callback);
        }
    }
}

下面的图描绘了 preOrderTraverseNode 方法的访问路径:
8-07

8.3.3 后序遍历
后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目录和它的子目录中所有文件所占空间的大小。
具体实现:

class BinarySearchTree{
    // ......
    // 后序遍历
    postOrderTraverse(callback) {
        Tool.postOrderTraverseNode(this.root, callback);
    }
    //......
}

class Tool {
    //......
    static postOrderTraverseNode(node, callback) {
        if(node !== null) {
            this.postOrderTraverseNode(node.left, callback);
            this.postOrderTraverseNode(node.right, callback);
            callback(node.key);
        }
    }
}

下面的图描绘了 postOrderTraverse 方法的访问路径:
8-08

8.4 搜索树中的值

在树中,有三种经常执行的搜索类型:
 最小值;
 最大值;
 搜索特定的值。

8.4.1 搜索最小值和最大值
我们使用下面的树作为示例:
8-09
看一眼树最后一层最左侧的节点,会发现它的值为3,这是这棵树中最小的键。如果你再看一眼树最右端的节点(同样是树的最后一层),
会发现它的值为25,这是这棵树中最大的键。这条信息在我们实现搜索树节点的最小值和最大值的方法时能给予我们很大的帮助。
具体实现:

class BinarySearchTree{
    // ......
    // 获取最小键
    min() {
        return Tool.minNode(this.root);
    }
    
    // 获取最大键
    max() {
        return Tool.maxNode(this.root);
    }
    //......
}

class Tool {
    //......
    static minNode(node) {
        if(node) {
            while (node && node.left !== null) {
                node = node.left
            }
            return node.key;
        }
        return null;
    }
    
    static maxNode(node) {
        if(node) {
            while (node && node.right !== null) {
                node = node.right;
            }  
            return node.key;
        }
        return null
    }
}

8.4.2 搜索一个特定的值
在之前的章节中,我们同样实现了 find 、 search 或 get 方法来查找数据结构中的一个特定的值(和之前章节中实现的 has 方法相似)。我们将同样在BST中实现搜索的方法,来看它的实现:
具体实现:

class BinarySearchTree{
    // ......
    // 搜索一个特定的值
    search(key) {
        return Tool.searchNode(this.root, key);
    }
    //......
}

class Tool {
    //......
    static searchNode(node, key) {
        if(node === null) {
            return false;
        }
        if(key < node.key) {
            return this.searchNode(node.left, key);
        } else if(key > node.key) {
            return this.searchNode(node.right, key);
        } else {
            return true;
        }
    }
}

对于结果的测试:

console.log(tree.search(1) ? 'Key 1 found.' : 'Key 1 not found.');
console.log(tree.search(8) ? 'Key 8 found.' : 'Key 8 not found.');

输出结果如下:
Value 1 not found.
Value 8 found.

8.4.3 移除一个节点
removeNode 方法的实现:

class BinarySearchTree{
     // ......
    // 移除一个节点
    remove(key) {
        root = Tool.removeNode(this.root, key);
    }
     //......
}
 
class Tool {
     //......
    static removeNode(node, key) {
        if(node === null) {
            return null;
        }
        if(key < node.key) {
            node.left = this.removeNode(node.left, key);
            return node;
        } else if(key > node.key) {
            node.right = this.removeNode(node.right, key);
            return node;
        } else {
            //键等于 node.key 的情况
            //第一种情况: 一个叶节点
            if(node.left === null && node.right === null) {
                node = null;
                return node
            }
    
            //第二种情况: 一个只有一个子节点的节点
            if(node.left === null) {
                node = node.right;
                return node;
            } else if(node.right === null) {
                node = node.left;
                return node;
            }
    
            // 第三种情况: 一个有两个子节点的节点
            let aux = this.findMinNode(node.right);
            node.key = aux.key;
            node.right = this.removeNode(ndoe.right, aux.key);
            return node;
        }
    }
    
    static findMinNode(node) {
        if(node) {
            while (node && node.left !== null) {
                node = node.left
            }
            return node;
        }
        return null;
    }
}

说明:

    1. 移除一个叶节点
      下图展现了移除一个叶节点的过程:
      8-10
    1. 移除有一个左侧或右侧子节点的节点
      下图展现了移除只有一个左侧子节点或右侧子节点的节点的过程:
      8-11
    1. 移除有两个子节点的节点
      现在是第三种情况,也是最复杂的情况,那就是要移除的节点有两个子节点——左侧子节点和右侧子节点。要移除有两个子节点的节点,需要执行四个步骤。
      (1) 当找到了需要移除的节点后,需要找到它右边子树中最小的节点(它的继承者——行{18} )。
      (2) 然后,用它右侧子树中最小节点的键去更新这个节点的值(行 {19} )。通过这一步,我们改变了这个节点的键,也就是说它被移除了。
      (3) 但是,这样在树中就有两个拥有相同键的节点了,这是不行的。要继续把右侧子树中的最小节点移除,毕竟它已经被移至要移除的节点的位置了(行 {20} )。
      (4) 最后,向它的父节点返回更新后节点的引用(行 {21} )。

下图展现了移除有两个子节点的节点的过程:
8-12

本节代码示例

09章、图

非线性数据结构

9.1 图的相关术语

图是网络结构的抽象模型。图是一组由边连接的节点(或顶点)。学习图是重要的,因为任何二元关系都可以用图来表示。
我们还可以使用图来表示道路、航班以及通信状态,如下图所示:
9-01

一个图G = (V, E)由以下元素组成
 V:一组顶点
 E:一组边,连接V中的顶点
9-02

先了解一下图的一些术语。
由一条边连接在一起的顶点称为相邻顶点。比如,A和B是相邻的,A和D是相邻的,A和C是相邻的,A和E不是相邻的。
一个顶点的度是其相邻顶点的数量。比如,A和其他三个顶点相连接,因此,A的度为3;E和其他两个顶点相连,因此,E的度为2。
路径是顶点v 1 , v 2 ,…,v k 的一个连续序列,其中v i 和v i+1 是相邻的。以上一示意图中的图为例,其中包含路径A B E I和A C D G。
简单路径要求不包含重复的顶点。举个例子,A D G是一条简单路径。除去最后一个顶点(因为它和第一个顶点是同一个顶点),环也是一个简单路径,比如A D C A(最后一个顶点重新回到A)。
如果图中不存在环,则称该图是无环的。如果图中每两个顶点间都存在路径,则该图是连通的。

有向图和无向图
图可以是无向的(边没有方向)或是有向的(有向图)。如下图所示,有向图的边有一个方向:
9-03
如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。例如,C和D是强连通的,而A和B不是强连通的。

图还可以是未加权的(目前为止我们看到的图都是未加权的)或是加权的。如下图所示,加权图的边被赋予了权值:
9-04

9.2 图的表示

从数据结构的角度来说,我们有多种方式来表示图。在所有的表示法中,不存在绝对正确的方式。图的正确表示法取决于待解决的问题和图的类型。

9.2.1 邻接矩阵
图最常见的实现是邻接矩阵。每个节点都和一个整数相关联,该整数将作为数组的索引。我们用一个二维数组来表示顶点之间的连接。
如果索引为i的节点和索引为j的节点相邻,则array[i][j]=== 1,否则array[i][j] === 0,如下图所示:
9-05
不是强连通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。
邻接矩阵表示法不够好的另一个理由是,图中顶点的数量可能会改变,而2维数组不太灵活。

9.2.2 邻接表
我们也可以使用一种叫作邻接表的动态数据结构来表示图。邻接表由图中每个顶点的相邻顶点列表所组成。存在好几种方式来表示这种数据结构。我们可以用列表(数组)、链表,甚至是散列表或是字典来表示相邻顶点列表。
下面的示意图展示了邻接表数据结构。
9-06
尽管邻接表可能对大多数问题来说都是更好的选择,但以上两种表示法都很有用,且它们有着不同的性质(例如,要找出顶点v和w是否相邻,使用邻接矩阵会比较快)。
在本书的示例中,我们将会使用邻接表表示法。

9.2.3 关联矩阵
我们还可以用关联矩阵来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。如下图所示,我们使用二维数组来表示两者之间的连通性,
如果顶点v是边e的入射点,则array[v][e] === 1;否则,array[v][e] === 0。
9-07

9.3 创建图类

具体实现:

let Dictionary = require('../07、字典和散列表/01、字典/index');

class Graph {
    constructor() {
        this.vertices = [];
        this.adjList = new Dictionary();
    }

    // 一个用来向图中添加一个新的顶点(因为图实例化后是空的)
    addVertex(v) {
        this.vertices.push(v);
        this.adjList.set(v, []);
    }

    // 用来添加顶点之间的边
    addEdge(v, w) {
        this.adjList.get(v).push(w);
        this.adjList.get(w).push(v);
    }

    toString() {
        let s = '';
        for (let i = 0; i < this.vertices.length; i++) {
            s += this.vertices[i] + ' -> ';
            let neighbors = this.adjList.get(this.vertices[i]);
            for (let j = 0; j < neighbors.length; j++) {
                s += neighbors[j] + ' ';
            }
            s += '\n';
        }
        return s;
    }
}

module.exports = Graph;

测试代码:

const Graph = require('./index');

let graph = new Graph();
let myVertices = ['A','B','C','D','E','F','G','H','I']; //{7}
for (let i=0; i<myVertices.length; i++){ //{8}
    graph.addVertex(myVertices[i]);
}
graph.addEdge('A', 'B'); //{9}
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');

console.log(graph.toString());

我们为邻接表表示法构建了一个字符串。首先,迭代 vertices 数组列表(行 {10} ),将顶点的名字加入字符串中。
接着,取得该顶点的邻接表(行 {11} ),同样也迭代该邻接表(行 {12} ),将相邻顶点加入我们的字符串。
邻接表迭代完成后,给我们的字符串添加一个换行符(行 {13} ),这样就可以在控制台看到一个漂亮的输出了。

9.4 图的遍历

有两种算法可以对图进行遍历:广度优先搜索(Breadth-First Search,BFS)和深度优先搜索(Depth-First Search,DFS)。
图遍历算法的**是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。
完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。
9-08

当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。
 白色:表示该顶点还没有被访问。
 灰色:表示该顶点被访问过,但并未被探索过。
 黑色:表示该顶点被访问过且被完全探索过。
这就是之前提到的务必访问每个顶点最多两次的原因。

9.4.1 广度优先搜索
广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深地访问顶点,如下图所示:
9-09

以下是从顶点v开始的广度优先搜索算法所遵循的步骤。
(1) 创建一个队列Q。
(2) 将v标注为被发现的(灰色),并将v入队列Q。
(3) 如果Q非空,则运行以下步骤:
(a) 将u从Q中出队列;
(b) 将标注u为被发现的(灰色);
(c) 将u所有未被访问过的邻点(白色)入队列;
(d) 将u标注为已被探索的(黑色)。

具体实现:

let Dictionary = require('../07、字典和散列表/01、字典/index');
let Queue = require('../04章、队列/02、优先队列/index');

class Graph {
    constructor() {
        this.vertices = [];
        this.adjList = new Dictionary();
    }
    // ......

    // 广度优先遍历算法
    bfs(v, callback) {
        let color = this.initializeColor(), queue = new Queue();
        queue.enqueue(v);
        while (!queue.isEmpty()) {
            let u = queue.dequeue(), neighbors = this.adjList.get(u);
            color[u] = 'grey';
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i];
                if (color[w] === 'white') {
                    color[w] = 'grey';
                    queue.enqueue(w);
                }
            }
            color[u] = 'black';
            if(callback) {
                callback(u);
            }
        }
    }

    toString() {
        let s = '';
        for (let i = 0; i < this.vertices.length; i++) {
            s += this.vertices[i] + ' -> ';
            let neighbors = this.adjList.get(this.vertices[i]);
            for (let j = 0; j < neighbors.length; j++) {
                s += neighbors[j] + ' ';
            }
            s += '\n';
        }
        return s;
    }

    initializeColor() {
        let color = [];
        for (let i = 0; i < this.vertices.length; i++) {
            color[this.vertices[i]] = 'white';
        }
        return color;
    }
}

module.exports = Graph;

广度优先搜索和深度优先搜索都需要标注被访问过的顶点。为此,我们将使用一个辅助数组color 。
由于当算法开始执行时,所有的顶点颜色都是白色(行 {1} ),所以我们可以创建一个辅助函数 initializeColor ,为这两个算法执行此初始化操作。

让我们深入学习广度优先搜索方法的实现。我们要做的第一件事情是用 initializeColor函数来将 color 数组初始化为 white (行 {2} )。
我们还需要声明和创建一个 Queue 实例(行 {3} ),它将会存储待访问和待探索的顶点。

照着本章开头解释过的步骤, bfs 方法接受一个顶点作为算法的起始点。起始顶点是必要的,我们将此顶点入队列(行 {4} )。

如果队列非空(行 {5} ),我们将通过出队列(行 {6} )操作从队列中移除一个顶点,并取得一个包含其所有邻点的邻接表(行 {7} )。
该顶点将被标注为 grey (行 {8} ),表示我们发现了它(但还未完成对其的探索)。

对于u(行 {9} )的每个邻点,我们取得其值(该顶点的名字——行 {10} ),如果它还未被访问过(颜色为 white ——行 {11} ),
则将其标注为我们已经发现了它(颜色设置为 grey ——行{12} ),并将这个顶点加入队列中(行 {13} ),这样当其从队列中出列的时候,我们可以完成对其的探索。

当完成探索该顶点和其相邻顶点后,我们将该顶点标注为已探索过的(颜色设置为black ——行 {14} )。

我们实现的这个 bfs 方法也接受一个回调(我们在第8章中遍历树时使用了一个相似的方法)。这个参数是可选的,如果我们传递了回调函数(行 {15} ),会用到它。

让我们执行下面这段代码来测试一下这个算法:

function printNode(value) {
    console.log('Visited vertex: ' + value);
}
graph.bfs(myVertices[0], printNode);

输出结果如下:

Visited vertex: A
Visited vertex: B
Visited vertex: C
Visited vertex: D
Visited vertex: E
Visited vertex: F
Visited vertex: G
Visited vertex: H
Visited vertex: I

1. 使用BFS寻找最短路径
到目前为止,我们只展示了BFS算法的工作原理。我们可以用该算法做更多事情,而不只是输出被访问顶点的顺序。

给定一个图G和源顶点v,找出对每个顶点u,u和v之间最短路径的距离(以边的数量计)。
对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推。所以,可以用广度优先算法来解这个问题。
我们可以修改 bfs 方法以返回给我们一些信息:
 从v到u的距离d[u];
 前溯点pred[u],用来推导出从v到其他每个顶点u的最短路径。
让我们来看看改进过的广度优先方法的实现:

具体实现:

let Dictionary = require('../07、字典和散列表/01、字典/index');
let Queue = require('../04章、队列/02、优先队列/index');

class Graph {
    constructor() {
        this.vertices = [];
        this.adjList = new Dictionary();
    }
    // ......
    // 使用BFS寻找最短路径
    BFS(v) {
        let color = initializeColor(),
            queue = new Queue(),
            d = [], //{1}
            pred = []; //{2}
        queue.enqueue(v);
        for (let i=0; i<this.vertices.length; i++){ //{3}
            d[this.vertices[i]] = 0; //{4}
            pred[this.vertices[i]] = null; //{5}
        }
        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u);
            color[u] = 'grey';
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i];
                if (color[w] === 'white') {
                    color[w] = 'grey';
                    d[w] = d[u] + 1; //{6}
                    pred[w] = u; //{7}
                    queue.enqueue(w);
                }
            }
            color[u] = 'black';
        }
        return { //{8}
            distances: d,
            predecessors: pred
        };
    }

}

module.exports = Graph;

我们还需要声明数组 d (行 {1} )来表示距离,以及 pred 数组来表示前溯点。下一步则是对图中的每一个顶点,用 0 来初始化数组 d (行 {4} ),用 null 来初始化数组 pred 。
当我们发现顶点 u 的邻点 w 时,则设置 w 的前溯点值为 u (行 {7} )。我们还通过给 d[u] 加1来设置 v 和 w 之间的距离( u 是 w 的前溯点, d[u] 的值已经有了)。
方法最后返回了一个包含 d 和 pred 的对象(行 {8} )。
现在,我们可以再次执行 BFS 方法,并将其返回值存在一个变量中:

let shortestPathA = graph.BFS(myVertices[0]);
console.log(shortestPathA);

对顶点 A 执行 BFS 方法,以下将会是输出:

distances: [A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2 , I: 3],
predecessors: [A: null, B: "A", C: "A", D: "A", E: "B", F: "B", G:"C", H: "D", I: "E"]

这意味着顶点 A 与顶点 B 、 C 和 D 的距离为 1 ;与顶点 E 、 F 、 G 和 H 的距离为 2 ;与顶点 I 的距离为 3 。

通过前溯点数组,我们可以用下面这段代码来构建从顶点 A 到其他顶点的路径:

let fromVertex = myVertices[0]; //{9}
for (let i=1; i<myVertices.length; i++){ //{10}
    let toVertex = myVertices[i], //{11}
        path = new Stack(); //{12}
    for (let v=toVertex; v!== fromVertex;
         v=shortestPathA.predecessors[v]) { //{13}
        path.push(v); //{14}
    }
    path.push(fromVertex); //{15}
    let s = path.pop(); //{16}
    while (!path.isEmpty()){ //{17}
        s += ' - ' + path.pop(); //{18}
    }
    console.log(s); //{19}
}

我们用顶点 A 作为源顶点(行 {9} )。对于每个其他顶点(除了顶点 A ——行 {10} ),我们会计算顶点 A 到它的路径。我们从顶点数组得到 toVertex (行 {11} ),然后会创建一个栈来存储路径值(行 {12} )。
接着,我们追溯 toVertex 到 fromVertex 的路径{行 {13} }。变量 v 被赋值为其前溯点的值,这样我们能够反向追溯这条路径。将变量 v 添加到栈中(行 {14} )。最后,源顶点也会被添加到栈中,以得到完整路径。
这之后,我们创建了一个 s 字符串,并将源顶点赋值给它(它是最后一个加入栈中的,所以它是第一个被弹出的项 ——行 {16} )。当栈是非空的,我们就从栈中移出一个项并将其拼接到字符串 s 的后面(行 {18} )。最后(行 {19} )在控制台上输出路径。
执行该代码段,我们会得到如下输出:

A - B
A - C
A - D
A - B - E
A - B - F
A - C - G
A - D - H
A - B - E - I

这里,我们得到了从顶点 A 到图中其他顶点的最短路径(衡量标准是边的数量)。

9.4.2 深度优先搜索
深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。
换句话说,它是先深度后广度地访问顶点,如下图所示:
9-10

深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未访问,则访问该顶点v。
要访问顶点v,照如下步骤做。
(1) 标注v为被发现的(灰色)。
(2) 对于v的所有未访问的邻点w:
(a) 访问顶点w。
(3) 标注v为已被探索的(黑色)。

具体算法实现:

class Graph {
    constructor() {
        this.vertices = [];
        this.adjList = new Dictionary();
    }
    
    // 深度优先算法实现
    dfs(callback) {
        let color = this.initializeColor();
        for(let i = 0;i<this.vertices.length; i++) {
            if(color[this.vertices[i]] === 'white') {
                this.dfsVisit(this.vertices[i], color, callback);
            }
        }
    }
    
    dfsVisit(u, color, callback) {
        color[u] = 'grey';
        if(callback) {
            callback(u);
        }
        let neighbors = this.adjList.get(u);
        for (let i = 0; i< neighbors.length; i++) {
            let w = neighbors[i];
            if(color[w] === 'white') {
                this.dfsVisit(w, color, callback);
            }
        }
        color[u] = 'black';
    }
}

其他省略
让我们执行下面的代码段来测试一下 dfs 方法:
graph.dfs(printNode);
输出如下:

Visited vertex: A
Visited vertex: B
Visited vertex: E
Visited vertex: I
Visited vertex: F
Visited vertex: C
Visited vertex: D
Visited vertex: G
Visited vertex: H

下面这个示意图展示了该算法每一步的执行过程:
9-11

1. 探索深度优先算法
到目前为止,我们只是展示了深度优先搜索算法的工作原理。我们可以用该算法做更多的事情,而不只是输出被访问顶点的顺序。

对于给定的图G,我们希望深度优先搜索算法遍历图G的所有节点,构建“森林”(有根树的一个集合)以及一组源顶点(根),
并输出两个数组:发现时间和完成探索时间。我们可以修改dfs 方法来返回给我们一些信息:

 顶点u的发现时间d[u];
 当顶点u被标注为黑色时,u的完成探索时间f[u];
 顶点u的前溯点p[u]。

算法函数的具体实现

// 深度优先算法的优化
DFS() {
    let color = this.initializeColor(), d = [], f = [], p = [];
    this.time = 0;
    for (let i = 0; i < this.vertices.length; i++) {
        f[this.vertices[i]] = 0;
        d[this.vertices[i]] = 0;
        p[this.vertices[i]] = null;
    }

    for (let i = 0; i < this.vertices.length; i++) {
        if (color[this.vertices[i]] === 'white') {
            this.DFSVisit(this.vertices[i], color, d, f, p);
        }
    }

    return {
        discovery: d,
        finished: f,
        predecessors: p
    }
}

DFSVisit(u, color, d, f, p) {
    console.log('discovered ' + u);
    color[u] = 'grey';
    d[u] = ++this.time;
    let neighbors = this.adjList.get(u);
    for (let i = 0; i < neighbors.length; i++) {
        let w = neighbors[i];
        if (color[w] === 'white') {
            p[w] = u;
            this.DFSVisit(w, color, d, f, p);
        }
    }
    color[u] = 'black';
    f[u] = ++this.time;
    console.log('explored ' + u);
}

对于改进过的深度优先搜索,有两点需要我们注意:
 时间( time )变量值的范围只可能在图顶点数量的一倍到两倍之间;
 对于所有的顶点 u ,d[u]<f[u](意味着,发现时间的值比完成时间的值小,完成时间意思是所有顶点都已经被探索过了)。
在这两个假设下,我们有如下的规则: 1 ≤ d[u] < f[u] ≤ 2|V|
9-12

具体代码请见本节示例

10章、排序和搜索算法

10.1 排序算法

在开始排序算法之前,我们先创建一个数组(列表)来表示待排序和搜索的数据结构。

class ArrayList {
    constructor() {
        this.array = [];
    }
    
    insert(item) {
        this.array.push(item);
    }
    
    toString() {
        return this.array.join();
    }
}

10.1.1 冒泡排序

人们开始学习排序算法时,通常都先学冒泡算法,因为它在所有排序算法中最简单。然而,从运行时间的角度来看,冒泡排序是最差的一个。
冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们。

//冒泡法排序
bubbleSort() {
    let length = this.array.length;
    for(let i = 0; i < length; i++) {
        for (let j = 0; j < length;j++) {
            if(this.array[j]> this.array[j+1]) {
                let temp = this.array[j];
                this.array[j] = this.array[j+1];
                this.array[j+1] = temp;
            }
        }
    }
}

下面这个示意图展示了冒泡排序的工作过程:
10-1

测试代码如下:

const ArrayList = require('./index');

function createNonSortedArray(size){
    let array = new ArrayList();
    for (let i = size; i> 0; i--){
        array.insert(i);
    }
    return array;
}
let array = createNonSortedArray(5);
console.log(array.toString());
array.bubbleSort();
console.log(array.toString());

改进后的冒泡排序
如果从内循环减去外循环中已跑过的轮数,就可以避免内循环中所有不必要的比较
代码示例如下:

// 改进后的冒泡法排序
modifiedBubbleSort() {
    let length = this.array.length;
    for(let i = 0; i < length; i++) {
        for (let j = 0; j < length-1-i; j++) {
            if(this.array[j]> this.array[j+1]) {
                let temp = this.array[j];
                this.array[j] = this.array[j+1];
                this.array[j+1] = temp;
            }
        }
    }
}

下面这个示意图展示了改进后的冒泡排序算法是如何执行的:
10-2

10.1.2 选择排序

选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
具体代码实现示例:

// 选择排序
selectionSort() {
    let length = this.array.length, indexMin;
    for(let i = 0; i < length -1; i++) {
        indexMin = i;
        for(let j = i; j < length; j++) {
            if(this.array[indexMin] > this.array[j]) {
                indexMin = j;
            }
        }
        if(i !== indexMin) {
            this.swap(i, indexMin);
        }
    }
}

swap(index1, index2) {
    let temp = this.array[index1];
    this.array[index1] = this.array[index2];
    this.array[index2] = temp;
}

用以下代码段来测试选择排序算法:

array = createNonSortedArray(5);
console.log(array.toString());
array.selectionSort();
console.log(array.toString());

选择排序同样也是一个复杂度为O(n 2 )的算法。和冒泡排序一样,它包含有嵌套的两个循环,这导致了二次方的复杂度。
算法执行图如下
10-3

10.1.3 插入排序

插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了,接着,它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?
这样,头两项就已正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢?),以此类推。

具体代码实现:

// 插入法排序
insertionSort() {
    let length = this.array.length, j, temp;
    for(let i = 1; i < length; i++) {
        j = i ;
        temp = this.array[i];
        while (j > 0 && this.array[j -1] > temp) {
            this.array[j] = this.array[j - 1];
            j--;
        }
        this.array[j] = temp;
    }
}

下面的示意图展示了一个插入排序的实例:
10-4

10.1.4 归并排序

归并排序是第一个可以被实际使用的排序算法。你在本书中学到的前三个排序算法性能不好,但归并排序性能不错,其复杂度为O(nlog n )。

归并排序是一种分治算法。其**是将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。

由于是分治法,归并排序也是递归的:

// 归并排序
mergeSort() {
    this.array = this.mergeSortRec(this.array);
}
mergeSortRec(array) {
    let length = this.array.length;
    if(length === 1) {
        return this.array;
    }
    let mid = Math.floor(length/2),
        left = this.array.slice(0, mid),
        right = this.array.slice(mid, length);
    return this.merge(this.mergeSortRec(left), this.mergeSortRec(right))
}
merge(left, right) {
    let result = [], il = 0, ir = 0;
    while (il < left.length && ir < right.length) {
        if(left[il] < right[ir]) {
            result.push(left[il++]);
        }else {
            result.push(right[ir++]);
        }
    }
    while (il < left.length) {
        result.push(left[il++]);
    }
    while (ir <right.length) {
        result.push(right[ir++]);
    }
    return result;
}

如果执行 mergeSort 函数,下图是具体的执行过程:
10-5

10.1.5 快速排序

快速排序也许是最常用的排序算法了。它的复杂度为O(nlog n ),且它的性能通常比其他的复杂度为O(nlog n )的排序算法要好。
和归并排序一样,快速排序也使用分治的方法,将原始数组分为较小的数组(但它没有像归并排序那样将它们分割开)。
快速排序比到目前为止你学过的其他排序算法要复杂一些。
(1) 首先,从数组中选择中间一项作为主元。
(2) 创建两个指针,左边一个指向数组第一个项,右边一个指向数组最后一个项。移动左指针直到我们找到一个比主元大的元素,接着,
移动右指针直到找到一个比主元小的元素,然后交换它们,重复这个过程,直到左指针超过了右指针。
这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后。这一步叫作划分操作。
(3) 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的两个步骤,直至数组已完全排序。
具体实现过程:

//快速排序
quickSort() {
    this.quick(this.array, 0, this.array.length - 1);
}
quick(array, left, right) {
    let index;
    if(array.length > 1) {
        index = this.partition(array, left, right);
        if(left < index -1) {
            this.quick(array, left, index - 1);
        }
        if(index < right) {
            this.quick(array, index, right);
        }
    }
}
//划分过程
partition(array, left, right) {
    let pivot = array[Math.floor((right + left) / 2)], i = left, j = right;
    while (i <= j) {
        while (array[i] < pivot) {
            i++;
        }
        while (array[j] > pivot) {
            j++;
        }
        if(i <= j) {
            this.swapQuickStort(array, i, j);
            i++;
            j++;
        }
    }
    return i;
}
swapQuickStort(array, index1, index2) {
    let temp = array[index1];
    array[index1] = array[index2];
    array[index2] = temp;
}

让我来一步步地看一个快速排序的实际例子:
10-6
给定数组 [3, 5, 1, 6, 4, 7, 2] ,前面的示意图展示了划分操作的第一次执行。
下面的示意图展示了对有较小值的子数组执行的划分操作(注意7和6不包含在子数组之内):
10-7
接着,我们继续创建子数组,请看下图,但是这次操作是针对上图中有较大值的子数组(有1的那个较小子数组不用再划分了,因为它仅含有一个项)。
10-8
子数组( [2, 3, 5, 4] )中的较小子数组( [2, 3] )继续进行划分(算法代码中的行 {5} ):
10-9
然后子数组( [2, 3, 5, 4] )中的较大子数组( [5, 4] )也继续进行划分(算法中的行),示意图如下:
10-10
最终,较大子数组 [6, 7] 也会进行划分( partition )操作,快速排序算法的操作执行完成。

10.2 搜索算法

10.2.1 顺序搜索

顺序或线性搜索是最基本的搜索算法。它的机制是,将每一个数据结构中的元素和我们要找的元素做比较。顺序搜索是最低效的一种搜索算法。

// 顺序搜索法
sequentialSearch(item) {
    for(let i = 0; i < this.array.length, i++) {
        if(item === this.array[i]) {
            return i;
        }
    }
    return -1;
}

假定有数组 [5, 4, 3, 2, 1] 和待搜索值3,下图展示了顺序搜索的示意图:
10-11

二分搜索算法的原理和猜数字游戏类似,就是那个有人说“我正想着一个1到100的数字”的
游戏。我们每回应一个数字,那个人就会说这个数字是高了、低了还是对了。

10.2.2 二分搜索

二分搜索算法的原理和猜数字游戏类似,就是那个有人说“我正想着一个1到100的数字”的游戏。我们每回应一个数字,那个人就会说这个数字是高了、低了还是对了。
这个算法要求被搜索的数据结构已排序。以下是该算法遵循的步骤。
(1) 选择数组的中间值。
(2) 如果选中值是待搜索值,那么算法执行完毕(值找到了)。
(3) 如果待搜索值比选中值要小,则返回步骤1并在选中值左边的子数组中寻找。
(4) 如果待搜索值比选中值要大,则返回步骤1并在选种值右边的子数组中寻找。
具体代码实现:

//二分搜索
binarySearch(item) {
    this.quickSort();
    let low = 0, height = this.array.length -1, mid, element;

    while (low <= height) {
        mid = Math.floor((low + height) / 2);
        element = this.array[mid];
        if(item > element) {
            low = mid + 1;
        } else if(item < element) {
            height = mid - 1;
        } else {
            return mid;
        }
    }
    return -1;
}

给定下图所示数组,让我们试试看搜索2。这些是算法将会执行的步骤:
10-12

Profiler 用法介绍

Profiler

Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。
它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

用法

Profiler 能添加在 React 树中的任何地方来测量树中这部分渲染所带来的开销。
它需要两个 prop :一个是 id(string),一个是当组件树中的组件“提交”更新的时候被React调用的回调函数 onRender(function)。

import React, { FC, useState, Profiler as ReactProfiler, ProfilerOnRenderCallback } from 'react';

const Profiler: FC = () => {
  const [value, setValue] = useState('');

  const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime, interactions) => {
    console.group('info');
    console.log('id', id);
    console.log('phase', phase);
    console.log('actualDuration',actualDuration);
    console.log('baseDuration', baseDuration);
    console.log('startTime', startTime);
    console.log('commitTime', commitTime);
    console.log('interactions', interactions);
    console.groupEnd();
  }

  return (
    <div>
      <ReactProfiler id="profiler" onRender={onRender}>
        <input type="text" onChange={event => setValue(event.target.value)} /> <br />
        <p>输出内容:{value}</p>
      </ReactProfiler>
    </div>
  );
};

export default Profiler;

onRender

参数:

  • id: string - 发生提交的 Profiler 树的 id。 如果有多个 profiler,它能用来分辨树的哪一部分发生了“提交”。
  • phase: "mount" | "update" - 判断是组件树的第一次装载引起的重渲染,还是由 props、state 或是 hooks 改变引起的重渲染。
  • actualDuration: number - 本次更新在渲染 Profiler 和它的子代上花费的时间。 这个数值表明使用 memoization 之后能表现得多好。(例如 React.memo,useMemo,shouldComponentUpdate)。 理想情况下,由于子代只会因特定的 prop 改变而重渲染,因此这个值应该在第一次装载之后显著下降。
  • baseDuration: number - 在 Profiler 树中最近一次每一个组件 render 的持续时间。 这个值估计了最差的渲染时间。(例如当它是第一次加载或者组件树没有使用 memoization)。
  • startTime: number - 本次更新中 React 开始渲染的时间戳。
  • commitTime: number - 本次更新中 React commit 阶段结束的时间戳。 在一次 commit 中这个值在所有的 profiler 之间是共享的,可以将它们按需分组。
  • interactions: Set - 当更新被制定时,“interactions” 的集合会被追踪。(例如当 render 或者 setState 被调用时)。

[设计模式] 02-创建型设计模式

第二篇、创建型设计模式

第三章-简单工程模式

简单工厂的最简单示例

所有的类都封装到一个函数里面,这样在模块调用的时候只需要记住这个函数,通过这个函数创建用户需要的对象就可以了。
这样用户可以不再关注创建这些对象依赖了那些基类,只要知道这个函数就可以了。这个函数就是被称为工厂函数,这种设计模式就被称为简单工厂设计模式。
一个最简单的示例如下:

//篮球基类
class BasketBall {
    constructor() {
        this.intro = '篮球盛行于美国'
    }

    getMember() {
        console.log('每一个队伍需要五个球员');
    }

    getBallSize() {
        console.log('篮球很大')
    }
}

// 足球基类
class FootBall {
    constructor() {
        this.intro = '足球在全世界范围都很流行'
    }

    getMember() {
        console.log('每一个队伍需要11个球员');
    }

    getBallSize() {
        console.log('足球很大')
    }
}

let SportsFactory = function(name) {
    switch (name) {
        case 'NBA':
            return new BasketBall();
        case 'wordCup':
            return new FootBall();
    }
};

// 为直接被创建一个足球,只需要记住工厂,并且调用就可以了
let footNall  = SportsFactory('wordCup');
console.log(footNall);
console.log(footNall.intro);
footNall.getMember();

一个对象也可以替代许多类

不同的类有相同的地方,是可以抽取出来公用的。
简单工厂设计模式的理念是创建对象,上面的方式是不同的类实例化而已,不过除此之外,简单工厂模式还可以用来创建对象。
举一个例子:
比如创建一些书,书有一些相似的地方,比如目录,页码等,也有一些不同的地方,比如书名,出版时间,书的类型等,对于创建的对象想死的属性处理好,不同的属性针对想处理。
比如我们将不同的属性作为参数传递进来处理。

function createBook(name, time, type) {
    let o  = new Object();
    o.name = name;
    o.time = time;
    o.type = type;
    o.getName = function() {
        console.log(this.name);
    };
    return o;
}

let book1 = createBook('js book', 2018, 'js');
let book2 = createBook('css book', 2017, 'css');

book1.getName();
book2.getName();

比如本书里面的三个雷,抽象共同点,比如属性this.content, 公用方法show,
不同点,确认框和提示框的确认按钮, 比如提示框的用户输入框等, 所以你就可以像下面这样创建了。

function createPop(type, text) {
    let o = new Object();
    o.content = text;
    o.show = function() {
        // 显示方法
    };

    if(type === 'alert') {
        // 警告框差异部分
    }
    if(type === 'prompt') {
        // 提示框差异部分
    }
    if(type === 'confirm') {
        //确认框差异部分
    }
    return o;
}
let userNameAlert = createPop('alert', '用户名只能是26个字母和数字');

第四章-工厂方法模式

将实现创建对象的工作推迟到子类当中。这样核心类就成了抽象类,我们可以将工厂方法看做一个实例化对象的工厂类。
将创建对象的基类放在工厂方法类的原型就可以了。

用简单方法实现的一个广告展现的示例

let Java = function(content) {
    this.content = content;
    (function(content) {
        console.log(content)
    })(content);
};

let Php = function(content) {
    this.content = content;
    (function(content) {
        console.log(content)
    })(content);
};

let JavaScript = function(content) {
    this.content = content;
    (function(content) {
        console.log(content)
    })(content);
};

// 学科工厂
function JobFactory(type, content) {
    switch (type) {
        case 'java':
            return new Java(content);
        case 'php':
            return new Php(content);
        case 'javascript':
            return new JavaScript(content);
    }
}
let java = JobFactory('javascript', '我是js书籍');

缺点,如果有后续的更多的需求,需要添加类,又要修改简单工厂函数。

安全模式类

安全模式类,可以屏蔽调用错误,没有new 关键字的时候,直接调用类,是会报错的。安全类就是可以屏蔽这个错误的类。

let Demo = function(){

};
Demo.proptotype = {
    show() {
        console.log('获取成功');
    }
};
let d = new Demo();
d.show();
let d = Demo();
d.show();   // 这个地方就会报错: TypeError: Cannot read property 'show' of undefined

安全模式就是为了解决上面问题的。

解决办法:

let Demo = function(){
    if(!(this instanceof Demo)) {
        return new Demo();
    }
};
Demo.proptotype = {
    show() {
        console.log('获取成功');
    }
};
let d = new Demo();
d.show();
let d = Demo();
d.show();

安全工厂方法

实际上的本质还是把类的实例化移动到了原型中去,本质并没有发生变化。之前的简单工厂方法,是把对象的实例化放置在工厂方法本身上的。
这个方法就实现了所有的类都写在了原型里面,动态判断了是否是工厂函数本身,根据不同的装填添加不同的方法;

let Factory = function (type, content) {
    if (this instanceof Factory) {
        return new this[type](content);
    } else {
        return new Factory(type, content)
    }
};

Factory.prototype = {
    Java(content) {
        this.content = content;
        (function (content) {
            console.log(content)
        })(content);
    },
    Php(content) {
        this.content = content;
        (function (content) {
            console.log(content)
        })(content);
    },
    JavaScript(content) {
        this.content = content;
        (function (content) {
            console.log(content)
        })(content);
    }
};

第五章-抽象工厂模式

js中abstract是保留字段,抽象类是一种申明但是不能使用的类,当你使用的时候就会报错。我们可以在类的方法中手动排除错误来模拟抽象类。

let Car = function() {};
Car.prototype = {
    getPrice() {
        return new Error('抽象方法不能被调用');
    },
    getSpeed() {
        return new Error('抽象方法不能被调用');
    }
};

这个car类什么都不能做,不能使用,但是在继承上却很有用,因为定义类一个类,这给类具备必要的方法,如果在子类没有重写这些方法,那么调用的时候就会报错。
在大型程序中,总会有一些子类去继承另外一些父类,这些父类经常会定义一些必要的方法,却没有具体实现。这种写法是忘记重写子类的这些错误遗漏的避免是很有帮助的。

抽象工厂模式

//抽象工厂
let VehicleFactory = function(subType, superType) {
    if(typeof VehicleFactory[superType] === 'function') {
        // 缓存类
        function F(){}
        // 集成父类的属性和方法
        F.prototype = new VehicleFactory[superType]();
        // 将子类的constructor 指向子类
        superType.constructor = subType;
        // 子类原型继承 父类
        subType.prototype = new F();
    } else {
        throw new Error('没有创建该类抽象对象');
    }
};
// 小汽车抽象类
VehicleFactory.Car = function() {
    this.type = 'car'
};
VehicleFactory.Car.prototype = {
    getPrice() {
        return new Error('抽象方法不能被调用')
    },
    getSpeed() {
        return new Error('抽象方法不能被调用')
    }
};

// 公交车
VehicleFactory.Bus = function() {
    this.type = 'bus'
};
VehicleFactory.Bus.prototype = {
    getPrice() {
        return new Error('抽象方法不能被调用')
    },
    getSpeed() {
        return new Error('抽象方法不能被调用')
    }
};

// 货车抽象类
VehicleFactory.Truck = function() {
    this.type = 'truck'
};
VehicleFactory.Truck.prototype = {
    getPrice() {
        return new Error('抽象方法不能被调用')
    },
    getSpeed() {
        return new Error('抽象方法不能被调用')
    }
};

源码请见02、抽象工厂的实现

抽象工厂实际上是一个实现一个子类继承一个抽象父类的方法。

抽象与实现

抽象工厂是创建子类的,所以我们需要一些子类产品,让子类继承父类就可以了。具体实现如下:

/*具体实现*/
//宝马汽车子类
let BMW = function(price, speed) {
    this.price = price;
    this.speed = speed;
};
VehicleFactory(BMW, 'Car');
BMW.prototype.getPrice = function() {
    return this.price;
};
BMW.prototype.getSpeed = function() {
    return this.speed;
};
//兰博基尼汽车子类
let Lamborghini = function(price, speed) {
    this.price = price;
    this.speed = speed;
};
VehicleFactory(Lamborghini, 'Car');
Lamborghini.prototype.getPrice = function() {
    return this.price;
};
Lamborghini.prototype.getSpeed = function() {
    return this.speed;
};

//宇通汽车子类
let YUTONG = function(price, speed) {
    this.price = price;
    this.speed = speed;
};
VehicleFactory(YUTONG, 'Bus');
YUTONG.prototype.getPrice = function() {
    return this.price;
};
YUTONG.prototype.getSpeed = function() {
    return this.speed;
};

//奔驰汽车子类
let BenzTruck = function(price, speed) {
    this.price = price;
    this.speed = speed;
};
VehicleFactory(BenzTruck, 'Truck');
BenzTruck.prototype.getPrice = function() {
    return this.price;
};
BenzTruck.prototype.getSpeed = function() {
    return this.speed;
};

let truck = new BenzTruck(10000, 100);
console.log(truck.getPrice());
console.log(truck.type);
console.log(truck.getSpeed());

源码请见:03、抽象与实现

第六章-建造者模式

将一个复杂对象的构建层与其表现层相互分离

创建对象的另外一种形式

工厂模式主要是为了创建对象和抽象工厂,关心的是最终产出(创建)的是什么。不关心你创建的过程,仅仅需要你知道最终创建的结果是什么就可以了。
建造者设计模式目的也是为了创建对象,但是更加关心的是创建这个对象的过程。比如创建一个人,不仅仅要得到人的实例,还要关心创建人的时候,这个人穿什么衣服,是男是女,兴趣爱好是什么。

//应聘者类
let Human = function(param) {
    this.skill = param && param.skill || '保密';
    this.hobby = param && param.hobby || '保密';
};
Human.prototype = {
    getSkill() {
        return this.skill;
    },
    getHobby() {
        return this.hobby
    }
};
//实例化姓名类
let Named = function(name) {
    let that = this;
    (function(name, that) {
        that.whileName = name;
        if(name.indexOf(' ') > -1) {
            that.firstName = name.slice(0, name.indexOf(' '));
            that.secondName = name.slice(name.indexOf(' '));
        }
    })(name, that)
};
// 实例化工作职位类
let Work = function(work) {
    let that = this;
    (function(work, thar) {
        switch (work) {
            case 'code':
                that.work = '工程师';
                that.workDescript = '写代码';
                break;
            case 'UI':
            case 'UE':
                that.work = '设计师';
                that.workDescript = '艺术工作';
                break;
            case 'teach':
                that.work = '教师';
                thar.workDescript = '教书育人';
                break;
            default:
                that.work = work;
                that.workDescript = '没有你描述的职位'
        }
    })(work, that)
};
//期望的职位
Work.prototype.changeWork = function(work) {
    this.work = work;
};
//添加职位描述
Work.prototype.changeDescript = function(setence) {
    this.workDescript = setence;
};

创建一位应聘者

上面我们创建了三个类 - 应聘者类、姓名解析类与期望职位类。最终目的是创建一个应聘者,所以需要抽象上面三个类。写一个建造者类,在建造者类中,我们要通过对这三个类组合调用,创建一个完整的应聘者对象出来。
核心代码如下:

let Person = function(name, work) {
    // 应聘者缓存对象
    let _person = new Human();
    // 姓名解析
    _person.name = new Named(name);
    // 应聘者职位
    _person.work = new Work(work);
    return _person;
}

在创造者中,我们分为三个部分来创建一个应聘者的,首先创建一个应聘者缓存对象,缓存对象添加姓名和职位,最终得到一个完整的应聘者了。

源码请见:02、创建一位应聘者

建造者模式不仅仅要得到穿件的结果,还要参与到创建的过程,对于创建的具体实现的细节也参与了干涉。这种创建模式创建的对象是一个复杂的符合对象。

第七章-原型链模式

原型链模式就是讲原型对象只想创建对象的类,使这类共享原型对象的方法与属性。

创建一个轮播图

创建轮播图最好的方式就是通过创建对象来一一实现:

let LoopImages = function(imgArr, container) {
    this.imagesArray = imgArr;              // 轮播图片数组
    this.container = container;             // 轮播图片容器
    this.createImage = function(){};        // 创建轮播图片
    this.changeImage = function(){};        // 切换下一张图片
}

如果一个页面有多个这类轮播图,切换动画也是变化多样,有的上下切换,有的左右切换,有的渐隐渐出,有的缩放切换等等。
这种情况下我们就需要抽象出来一个基类,让不同的特效类去继承这个基类,对于差异化的需求,通过重写这些基类下面的属性或者方法来解决。

// 上下滑动切换类
let SlideLoopImg = function(imgArr, container) {
    //继承
    LoopImages.call(this, imgArr, container);

    // 重写方法
    this.changeImage = function() {
        console.log('上下滑动切换')
    }
};

// 隐藏出现切换类
let FadeLoopImg = function(imgArr, container, arrow) {
    // 继承
    LoopImages.call(this, imgArr, container);
    this.arrow = arrow;
    this.changeImage = function() {
        console.log('隐藏出现切换类');
    }
};

//实例化一个对象
let fadeImg = new FadeLoopImg([
    '1.jpg',
    '2.jpg',
    '3.jpg',
], 'slide', [
    'left.jpg',
    'right.jpg'
]);

优化的解决方案

上面的缺点:每次子类继承父类都要创建一次父类。所以我们需要一种共享机制,每次创建基类的时候,对于每次创建的一些简单又差异化的属性,我们可以放在构造函数中,将一些消耗资源比较大的方法放在基类的原型中。

// 图片轮播图
let LoopImages = function(imgArr, container) {
    this.imagesArray = imgArr;
    this.container = container;
};
LoopImages.prototype = {
    // 创建轮播图片
    crateImage() {
        console.log('创建轮播图片')
    },

    // 切换下一个图片
    changeImage() {
        console.log('切换下一个图片')
    }
};

// 上下滑动切换类
let SlideLoopImg = function(imgArr, container) {
    //继承
    LoopImages.call(this, imgArr, container);
};
SlideLoopImg.prototype = new LoopImages();
SlideLoopImg.prototype.changeImage = function () {
    console.log('上下滑动切换')
};


// 隐藏出现切换类
let FadeLoopImg = function(imgArr, container, arrow) {
    // 继承
    LoopImages.call(this, imgArr, container);
    this.arrow = arrow;
};
FadeLoopImg.prototype = new LoopImages();
FadeLoopImg.prototype.changeImage = function () {
    console.log('隐藏出现切换类')
};

//实例化一个对象
let fadeImg = new FadeLoopImg([
    '1.jpg',
    '2.jpg',
    '3.jpg',
], 'slide', [
    'left.jpg',
    'right.jpg'
]);
console.log(fadeImg.container);
fadeImg.changeImage();

在原型链设计模式中,父子类的具体实现,都可以放在原型中去;

原型的扩展

原型对象是一个共享对象,无论父类的实例还是子类的继承,都是对他的一个指向引用。所以对于原型的扩展,无论是子类还是父类的示例,都会被继承下来。

备注

上面实际上只是一个原型链的一个继承方式, 其实现在又es6之后, 这种方式渐渐已经可以用es6 的面向对象class 去掉掉。

第八章-单例模式

单例模式(Singleton) - 只允许实例化一次的对象类。

做一个滑动特效的示例

function g(id) {
    return document.getElementById(id)
}

function css(id, key, value) {
    g(id).style[key] = value;
}
function attr(id, key, value) {
    g(id)[key] = value;
}
function html(id, value) {
    g(id).innerHeight = value;
}

这些代码存在不妥的地方:如果以后有人要在页面添加新的需求,添加代码的时候定义了一个on变量,或者重写了on方法,这样就会出现代码冲突了。

命名空间

用命名空间可以约束每个人定义的变量来解决上面的问题

let Le = {
    g(id) {
        return document.getElementById(id)
    },
    css(id, key, value) {
        this.g(id).style[key] = value;
    },
    attr(id, key, value) {
        this.g(id)[key] = value;
    },
    html(id, value) {
        this.g(id).innerHeight = value;
    }
};

无法改变的静态变量

将变量放在一个函数内部,只有通过特权方法访问,如果不提供赋值变量的方法,只提供获取变量的方法,就可以做到限制变量的修改而且可以提供外界访问的需求。
为了实现创建后就能使用,我们需要让穿件的函数执行一次。最后将这个对象最为一个单利放在全局空间里面,作为静态变量提供给他人使用。

let Conf = (function () {
    // 私有变量
    let conf = {
        MAX_NUM: 100,
        MIN_NUM:1,
        COUNT: 1000
    };
    return {
        get(name) {
            return conf[name] ? conf[name] : null
        }
    }
})();
let count = Conf.get('COUNT');
console.log(count);

惰性单例

有的时候对于单例对象需要延迟创建,所以单例中还存在一种延迟创建的形式,被称为惰性创建。

let LazySingle = (function () {
    let _instance = null;
    function single() {
        return {
            publicMath() {},
            publicConst: 100
        }
    }
    return function() {
        if(!_instance) {
            _instance = single();
        }
        //返回单例
        return _instance;
    }
})();

console.log(LazySingle().publicConst);

数组的扩展

数组的扩展

1、Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象( array-like object )和可遍历( iterable )的对象(包括 ES6 新增的数据结构 Set 和Map )。

实例1:

    let arrayLike = {
        '0': 'a',
        '1': 'b',
        '2': 'c',
        length: 3
    };
    // ES6 的写法
    let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

实例2:实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。

    // NodeList 对象
    let ps = document.querySelectorAll('p');
    Array.from(ps).forEach(function (p) {
        console.log(p);
    });
    // arguments 对象
    function foo() {
        var args = Array.from(arguments);
    // ...
    }

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
实例3:

    Array.from(arrayLike, x => x * x);
    //  等同于
    Array.from(arrayLike).map(x => x * x);
    Array.from([1, 2, 3], (x) => x * x)
    // [1, 4, 9]

2、Array.of()

Array.of方法用于将一组值,转换为数组。

    Array.of(3, 11, 8) // [3,11,8]
    Array.of(3) // [3]
    Array.of(3).length // 1

3、数组实例的 copyWithin()

数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

语法:Array.prototype.copyWithin(target, start = 0, end = this.length)             

target (必需):从该位置开始替换数据的下标位置。
start (可选):从该位置开始读取数据,默认为 0 。如果为负值,表示倒数。
end (可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。

实例:

    [1, 2, 3, 4, 5].copyWithin(0, 3)
    // [4, 5, 3, 4, 5]
    
    //  将 3 号位复制到 0 号位
    [1, 2, 3, 4, 5].copyWithin(0, 3, 4)
    // [4, 2, 3, 4, 5]
    // -2 相当于 3 号位, -1 相当于 4 号位
    [1, 2, 3, 4, 5].copyWithin(0, -2, -1)
    // [4, 2, 3, 4, 5]
    //  将 3 号位复制到 0 号位
    [].copyWithin.call({length: 5, 3: 1}, 0, 3)
    // {0: 1, 3: 1, length: 5}
    //  将 2 号位到数组结束,复制到 0 号位
    var i32a = new Int32Array([1, 2, 3, 4, 5]);
    i32a.copyWithin(0, 2);
    // Int32Array [3, 4, 5, 4, 5]
    //  对于没有部署 TypedArray 的 copyWithin 方法的平台
    //  需要采用下面的写法
    [].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
    // Int32Array [4, 2, 3, 4, 5]

4、数组实例的 find() 和 findIndex() - 非常重要

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
实例1:

    [1, 5, 10, 15].find(function(value, index, arr) {
        return value > 9;
    }) // 10

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
实例2:

    [1, 5, 10, 15].findIndex(function(value, index, arr) {
        return value > 9;
    }) // 2

5、数组实例的 fill()

fill方法使用给定值,填充一个数组。
fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
实例:

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

6、 数组实例的 entries() , keys() 和 values()

ES6 提供三个新的方法 —— entries(),keys()和values() —— 用于遍历数组。它们都返回一个遍历器对象(详见《 Iterator 》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
实例:

for (let index of ['a', 'b'].keys()) {
    console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
    console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
    console.log(index, elem);
}
// 0 "a"
// 1 "b"

7、数组实例的 includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于 ES7 ,但 Babel 转码器已经支持。
实例:

[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true

Generator 总结

Generator

目录

1.Generator

从计算机角度看,生成器是一种类协程或半协程,它提供了一种可以通过特定语句或方法使其执行对象暂停的功能。
​Generator函数,返回一个部署了Iterator接口的遍历器对象,用来操作内部指针。
以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。
value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

2.yield [[expression]]

yield 关键字使生成器函数暂停执行,并返回跟在它后面的表达式的当前值。
可以把它想成是return关键字的一个基于生成器的版本,但其并非退出函数体,而是切出当前函数的运行时,
与此同时可以将一个值带到主线程中。yield语句是暂停执行的标记,而next方法可以恢复执行。

function* gen(){
  yield 'li';
  yield 'gang'; // 有误!!!
  return '!';
}
var g = gen();
g.next(); // {value: 'li', done: false}
g.next(); // {value: 'gang', done: false}
g.next(); // {value: '!', done: true}

(1)遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值;
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句;
(3)如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值;
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,
因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

function* gen() {
  yield  123 + 456;
}

上述示例中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。
Generator函数也可以不用yield语句,这时就变成了一个单纯的暂缓执行函数。

function* f() {
  console.log('执行了!')
}
let gen = f();
setTimeout(function () {
  gen.next()
}, 2000);

3.next方法的参数

注意: yield句本身没有返回值(返回undefined)。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next();  // Object{value:6, done:false}
a.next();  // Object{value:NaN, done:false}
a.next();  // Object{value:NaN, done:true}

var b = foo(5);
b.next();   // { value:6, done:false }
b.next(12); // { value:8, done:false }
b.next(13); // { value:42, done:true }

next方法不带参数,导致y的值等于2 * undefined(即NaN),除以3以后还是NaN;
next方法提供参数,第一次调用b的next方法时,返回x+1的值6;
第二次调用next方法,将上一次yield语句的值设为12,因此y等于24,返回y/3的值8。

注意:这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。
通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。
也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

function* f() {
  for(let i=0; true; i++) {
    let reset = yield i;
    if(reset) { i = -1; }
  }
}

let g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

4.for…of循环

for...of循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next方法。

function *foo() {
  yield 1;
  yield 2;
  return 3;
}
for (let v of foo()) {
  console.log(v);
}

利用Generator函数和for...of循环,实现斐波那契数列

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}
for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

yield [[expression]]*
yield* 一个可迭代对象,就相当于把这个可迭代对象的所有迭代值分次 yield 出去。
表达式本身的值就是当前可迭代对象迭代完毕(当done为true时)时的返回值。

function* gen(){
  yield [1, 2];
  yield* [3, 4];
}
var g = gen();
g.next(); // {value: Array[2], done: false}
g.next(); // {value: 3, done: false}
g.next(); // {value: 4, done: false}
g.next(); // {value: undefined, done: true}

判断是否为Generator函数

function isGenerator(fn){
  // 生成器示例必带@@toStringTag属性
  if(Symbol && Symbol.toStringTag) {
    return fn[Symbol.toStringTag] === 'GeneratorFunction';
  }
}

5.async/await

async函数可以理解为Generator函数的语法糖,使用async内置了执行器,无需调用next方法进行逐步调用。且其返回值为Promise。

基本用法
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

正常情况下,await命令后是一个Promise对象。如果不是,会被转成一个立即resolve的Promise对象。
await只能用在async函数中,不能用在普通函数中

await后面可能存在reject,需要进行try…catch代码块中
多个异步操作,如果没有继承关系,最好同时触发

6.Promise和async使用场景

一个过程中同时存在异步、同步情况,请使用Promise

/*常规方式,错误!不能实现*/
function test(bool) {
  // bool为true,直接返回"hello"
  // bool为false,进行异步请求,这里使用setTimeout代替异步过程
  if(bool) {
    return "hello";
  } else {
    setTimeout(() => {
      return "world";
    }, 5000); 
  }
}
test(true);  // "hello"
test(false); // 无任何输出内容

/*Promise正确方式*/
function test(bool) {
  // bool为true,直接返回"hello"
  // bool为false,进行异步请求,这里使用setTimeout代替异步过程
  return new Promise((resolve, reject) => {
    if(bool) {
        return resolve("hello");
    } else {
        setTimeout(() => {
          return resolve("world");
        }, 5000); 
    }
  });
}
test(true).then(v => console.log(v));   // 'hello'
test(false).then(v => console.log(v));  // 大约5s后输出 'world'

包裹本身不支持async的函数,且hold住当前请求

import fs from "fs";

async function readFile(filepath) {
    return await new Promise((resolve, reject) => {
        fs.stat(filepath, (error) => {
            if(error) {
                return reject("文件不存在!");
            }
            let content = fs.readFileSync(filepath, "utf8");
            return resolve(content);
        })
    })
}
// 测试
readFile(__filename).then((content) => {
    console.log(content)
}).catch((error) => {
    console.error(error);
});

[shell] 入门 - 5 函数

函数

函数定义

function function_name () {
    list of commands
    [ return value ]
}

示例: 其中function关键字是可选的。

#!/bin/bash

hello(){
	echo 'hello';
}

hello

Shell 函数返回值只能是整数,一般用来表示函数执行成功与否,0表示成功,其他值表示失败。如果 return 其他数据,比如一个字符串,往往会得到错误提示:numeric argument required。

如果一定要让函数返回字符串,那么可以先定义一个变量,用来接收函数的计算结果,脚本在需要的时候访问这个变量来获得函数返回值。

#!/bin/bash

function hello(){
	return 'hello';
}

str=hello

echo $str

删除函数也可以使用 unset 命令,不过要加上 .f 选项 $unset .f function_name

函数参数

过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数...这就是前面讲的特殊变量。

function sum(){
	case $# in 
		0) echo "no param";;
		1) echo $1;;
		2) echo `expr $1 + $2`;;
		3) echo `expr $1 + $2 + $3`;;
		*) echo "$# params! It's too much!";;
	esac
}

sum 1 3 5 6

注意,$10 不能获取第十个参数,获取第十个参数需要${10}。当n>=10时,需要使用${n}来获取参数。

特殊变量说明

特殊变量 说明
$# 传递给函数的参数个数。
$* 显示所有传递给函数的参数。
$@ 与$*相同,但是略有区别,请查看Shell特殊变量。
$? 函数的返回值。

如何获取函数返回值

#!/bin/bash

function sum()
{
	echo `expr 1+2+3`
}

num=$(sum)

[shell] 入门 - 6 其他补充

其它

Shell输入输出重定向

Unix 命令默认从标准输入设备(stdin)获取输入,将结果输出到标准输出设备(stdout)显示。一般情况下,标准输入设备就是键盘,标准输出设备就是终端,即显示器。
输出重定向

命令的输出不仅可以是显示器,还可以很容易的转移向到文件,这被称为输出重定向。

命令输出重定向的语法为:

command > file

这样,输出到显示器的内容就可以被重定向到文件。

例如,下面的命令在显示器上不会看到任何输出:

who > users

打开 users 文件,可以看到下面的内容:

cat users

oko         tty01   Sep 12 07:30
ai          tty15   Sep 12 13:32
ruth        tty21   Sep 12 10:10
pat         tty24   Sep 12 13:07
steve       tty25   Sep 12 13:03

输出重定向会覆盖文件内容,请看下面的例子:

echo line 1 > users

cat users
line 1

如果不希望文件内容被覆盖,可以使用 >> 追加到文件末尾,例如:

echo line 2 >> users

cat users
line 1
line 2

输入重定向

和输出重定向一样,Unix 命令也可以从文件获取输入,语法为:

command < file

这样,本来需要从键盘获取输入的命令会转移到文件读取内容。

注意:输出重定向是大于号(>),输入重定向是小于号(<)。

例如,计算 users 文件中的行数,可以使用下面的命令:

wc -l users
2 users

也可以将输入重定向到 users 文件:

wc -l < users
2

注意:上面两个例子的结果不同:第一个例子,会输出文件名;第二个不会,因为它仅仅知道从标准输入读取内容。

重定向深入讲解

一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:

  • 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
  • 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
  • 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。

默认情况下,command > file 将 stdout 重定向到 file,command < file 将stdin 重定向到 file。

如果希望 stderr 重定向到 file,可以这样写:

command 2 > file

如果希望 stderr 追加到 file 文件末尾,可以这样写:

command 2 >> file

2 表示标准错误文件(stderr)。

如果希望将 stdout 和 stderr 合并后重定向到 file,可以这样写:

command > file 2>&1

如果希望对 stdin 和 stdout 都重定向,可以这样写:

command < file1 >file2

command 命令将 stdin 重定向到 file1,将 stdout 重定向到 file2。

全部可用的重定向命令列表:

命令	说明
command > file	将输出重定向到 file。
command < file	将输入重定向到 file。
command >> file	将输出以追加的方式重定向到 file。
n > file	将文件描述符为 n 的文件重定向到 file。
n >> file	将文件描述符为 n 的文件以追加的方式重定向到 file。
n >& m	将输出文件 m 和 n 合并。
n <& m	将输入文件 m 和 n 合并。
<< tag	将开始标记 tag 和结束标记 tag 之间的内容作为输入。

Here Document

Here Document 目前没有统一的翻译,这里暂译为嵌入文档。Here Document 是 Shell 中的一种特殊的重定向方式,它的基本的形式如下:

command << delimiter
    document
delimiter

它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command。

注意:
结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。

开始的delimiter前后的空格会被忽略掉。

下面的例子,通过 wc -l 命令计算 document 的行数:

wc -l << EOF
    This is a simple lookup program
    for good (and bad) restaurants
    in Cape Town.
EOF

输出: 3

也可以 将 Here Document 用在脚本中,例如:

#!/bin/bash
cat << EOF
This is a simple lookup program
for good (and bad) restaurants
in Cape Town.
EOF

运行结果:

This is a simple lookup program
for good (and bad) restaurants
in Cape Town.

/dev/null 文件

如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null

command > /dev/null

/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到禁止输出的效果。

如果希望屏蔽 stdout 和 stderr,可以这样写:

command > /dev/null 2>&1

这样不会在屏幕打印任何信息。

Shell文件包含

像其他语言一样,Shell 也可以包含外部脚本,将外部脚本的内容合并到当前脚本。

Shell 中包含脚本可以使用 . filenamesource filename

两种方式的效果相同,简单起见,一般使用点号(.),但是注意点号(.)和文件名中间有一空格。

示例:
被包含文件:sub.sh

name="yjc"

主文件:test.sh

. ./sub.sh
echo $name

运行结果:

yjc

获取当前正在执行脚本的绝对路径

正确的命令是:

basepath=$(cd `dirname $0`; pwd)

直接使用pwd或者dirname $0是不对的。

按特定字符串截取字符串

示例:截取/www/html/php/myapp/里的myapp。

方案:

str=/www/html/php/myapp/
b=($(echo $str|sed 's#/# #g'))
b_len=`expr ${#b[*]} - 1`
app_name=${b[$b_len]}
echo $app_name

这里利用sed将字符串按指定字符截成数组,然后取最后一个。

计算数组长度:${#arr[*]}
计算则需要使用expr命令

awk

awk简介

awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件(或其他方式的输入流, 如重定向输入)逐行的读入(看作一个记录集), 把每一行看作一条记录,以空格(或\t,或用户自己指定的分隔符)为默认分隔符将每行切片(类似字段),切开的部分再进行各种分析处理。

awk有3个不同版本: awk、nawk和gawk,未作特别说明,一般指gawk,gawk 是 AWK 的 GNU 版本。

Awk基本语法: 

awk 'pattern1 {command1;command 2…; command 3}  pattern2 { command …}'

pattern表示用来过滤记录的模式,可是是正则表达式,关系运算表达式,也可以什么也没有(表示选中所有记录)。

每个pattern选中的行记录会被花括号括起来的命令command操作一遍, command之间用;分割。 花括号里面可以什么也没有, 则默认为print输出整行记录。 Comamnd可以是输出, 可以是算术运算,逻辑运算,循环控制等等。

示例

s.txt

zhangsan 1977 male computer 83
lisi 1989 male math 99
wanglijiang 1990 female chinese 78
xuliang 1977 male economic 89
xuxin 1986 female english 99
wangxuebing 1978 male math 89
lichang 1989 male math 99
wanglijiang 1990 female chinese 78
zhangsansan 1977 male computer 83 
langxuebing 1978 male math 89
lisibao 1989 male math 99
xiaobao 1990 female chinese 78

一行中的5个字段分别表示姓名, 出生年, 性别,科目,分数, 是一个很传统很典型的报表文件。

现在演示awk是如何查找的:

1)直接输出1990年出生的同学:

$ awk '/1990/' s.txt

wanglijiang 1990 female chinese 78
wanglijiang 1990 female chinese 78
xiaobao 1990 female chinese 78
 

或者:

$ awk '/1990/{print $0}' s.txt

awk默认把输入的内容以空格拆分出每列。$0表示匹配所有列,print $0将输出所有列,每列分隔符是空格。

2)对chinese的课程的行输出"语文":

$ awk '/chinese/{print "语文"}' s.txt

语文
语文
语文

3)记录的头部和结尾加上一段说明:

$ awk 'BEGIN{print "Result of the quiz:\n"}{print $0}END{print "------"}' s.txt
Result of the quiz:

zhangsan 1977 male computer 83
lisi 1989 male math 99
wanglijiang 1990 female chinese 78
xuliang 1977 male economic 89
xuxin 1986 female english 99
wangxuebing 1978 male math 89
lichang 1989 male math 99
wanglijiang 1990 female chinese 78
zhangsansan 1977 male computer 83
langxuebing 1978 male math 89
lisibao 1989 male math 99
xiaobao 1990 female chinese 78
------

AWK工作流程:逐行扫描文件,从第一行到最后一行,寻找匹配特定模式的行,并在这些行上进行用户想要到的操作

BEGIN只会在最开始执行;END只会在扫描所有行数之后执行。BEGIN和END之间的花括号的内容每扫描一行都会执行。

4)查找女生的成绩且只输出姓名、学科、成绩:

$ awk '$3=="female"{print $1,$4,$5}' s.txt
wanglijiang chinese 78
xuxin english 99
wanglijiang chinese 78
xiaobao chinese 78

$1表示第1列,$n类推。这里条件是表达式,而不是正则。print里,表示空格分隔符。

5)找出1990年出生的学生姓名,并要求匹配正则:

$ awk '$2~/1990/{print $1}' s.txt
wanglijiang
wanglijiang
xiaobao

这里~表示匹配正则表达式。!~表示不匹配正则表达式。

如果需要多选,则改成:

$ awk '$2~/(1990|1991)/{print $1}' s.txt

向大佬,请教学习经验

和你差不多一样的工作时间,你的执行力很强!你是通过什么方法来确保,计划的执行?希望向你多学习,大佬也多分享下经验,看到你的那么多项目真实惭愧。

函数的扩展

6、函数的扩展

1、函数参数的默认值

1.1、 基本用法

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
实例:基本使用

    function log(x, y = 'World') {
        console.log(x, y);
    }
    log('Hello') // Hello World
    log('Hello', 'China') // Hello China
    log('Hello', '') // Hello

1.2、与解构赋值默认值结合使用

实例1:基本使用方式

    function foo({x, y = 5}) {
        console.log(x, y);
    }
    foo({}) // undefined, 5
    foo({x: 1}) // 1, 5
    foo({x: 1, y: 2}) // 1, 2
    foo() // TypeError: Cannot read property 'x' of undefined

实例2:高级使用

    //  写法一(采用第一种赋值方式)
    function m1({x = 0, y = 0} = {}) {
        return [x, y];
    }
    //  写法二
    function m2({x, y} = { x: 0, y: 0 }) {
        return [x, y];
    }   

上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

    //  函数没有参数的情况
    m1() // [0, 0]
    m2() // [0, 0]
    
    // x 和 y 都有值的情况
    m1({x: 3, y: 8}) // [3, 8]
    m2({x: 3, y: 8}) // [3, 8]
    
    // x 有值, y 无值的情况
    m1({x: 3}) // [3, 0]
    m2({x: 3}) // [3, undefined]
    
    // x 和 y 都无值的情况
    m1({}) // [0, 0];
    m2({}) // [undefined, undefined]
    
    m1({z: 3}) // [0, 0]
    m2({z: 3}) // [undefined, undefined]

1.3、函数的 length 属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。如果参数有赋值默认值,就要用这个属性了
实例1:

    (function (a) {}).length // 1
    (function (a = 5) {}).length // 0
    (function (a, b, c = 5) {}).length // 2

1.4、使用

实例1:利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

    function throwIfMissing() {
        throw new Error('Missing parameter');
    }
    function foo(mustBeProvided = throwIfMissing()) {
        return mustBeProvided;
    }
    foo()
    // Error: Missing parameter

2、rest参数

ES6 引入 rest 参数(形式为 “... 变量名 ” ),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。 rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
实例1:基本使用

    function add(...values) {
        let sum = 0;
        for (var val of values) {
            sum += val;
        }
        return sum;
    }
    add(2, 5, 3) // 10

3、扩展运算符

3.1、含义:扩展运算符( spread )是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

实例1:

    console.log(...[1, 2, 3])
    // 1 2 3
    console.log(1, ...[2, 3, 4], 5)
    // 1 2 3 4 5
    [...document.querySelectorAll('div')]
    // [<div>, <div>, <div>]

3.2、替代数组的 apply 方法

    // ES6 的写法
    function f(x, y, z) {
    // ...
    }
    var args = [0, 1, 2];
    f(...args);

3.3、扩展运算符的应用

3.3.1、合并数组

    [...arr1, ...arr2, ...arr3]
    // [ 'a', 'b', 'c', 'd', 'e' ]

3.3.2、与解构赋值结合

实例:

    const [first, ...rest] = [1, 2, 3, 4, 5];
    first // 1
    rest // [2, 3, 4, 5]
    
    const [first, ...rest] = [];
    first // undefined
    rest // []:
    
    const [first, ...rest] = ["foo"];
    first // "foo"
    rest // []

实例2:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

    const [...butLast, last] = [1, 2, 3, 4, 5];
    //  报错
    
    const [first, ...middle, last] = [1, 2, 3, 4, 5];
    //  报错

3.4、实现了 Iterator 接口的对象

实例:

    var nodeList = document.querySelectorAll('div');
    var array = [...nodeList];

3.5、Map 和 Set 结构, Generator 函数

实例:

    let map = new Map([
        [1, 'one'],
        [2, 'two'],
        [3, 'three'],
    ]);
    let arr = [...map.keys()]; // [1, 2, 3]

Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。
实例2:

    var go = function*(){
        yield 1;
        yield 2;
        yield 3;
    };
    [...go()] // [1, 2, 3]:

4、name属性

实例:函数的name属性,返回该函数的函数名

    function foo() {}
    foo.name // "foo"
    
    var func1 = function () {};
    
    // ES5
    func1.name // ""
    
    // ES6
    func1.name // "func1"

5、箭头函数


箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。

6、函数绑定

箭头函数并不适用于所有场合,所以 ES7 提出了 “ 函数绑定 ” ( function bind )运算符,用来取代call、apply、bind调用。虽然该语法还是 ES7 的一个提案,但是 Babel 转码器已经支持。
实例1:

    foo::bar;
    //  等同于
    bar.bind(foo);
    
    foo::bar(...arguments);
    //  等同于
    bar.apply(foo, arguments);
    
    const hasOwnProperty = Object.prototype.hasOwnProperty;
    function hasOwn(obj, key) {
        return obj::hasOwnProperty(key);
    }

7、尾调优化(略)

8、参数尾逗号(略)

[shell] 入门 - 1 基础

第一节-入门

授权可执行shell 文件: chmod +x test.sh

打印输出

echo: 是Shell的一个内部指令,用于在屏幕上打印出指定的字符串。

echo arg 
echo -e arg #执行arg里的转义字符。echo加了-e默认会换行
echo arg > myfile #显示结果重定向至文件,会生成myfile文件

printf:格式化输出语句。 printf 命令用于格式化输出, 是echo命令的增强版。它是C语言printf()库函数的一个有限的变形,并且在语法上有些不同。

printf  format-string  [arguments...]
#format-string 为格式控制字符串,arguments 为参数列表。功能和用法与c语言的 printf 命令类似。

# 双引号
printf "%d %s\n" 10 "abc"
10 abc
# 单引号与双引号效果一样 
printf '%d %s\n' 10 "abc" 
10 abc

# 没有引号也可以输出
printf %s abc
abc

# 但是下面的会出错:
printf %d %s 10 abc 
#因为系统分不清楚哪个是参数,这时候最好加引号了。


# 格式只指定了一个参数,但多出的参数仍然会按照该格式输出,format-string 被重用
$ printf %s a b c
abc
$ printf "%s\n" a b c
a
b
c

# 如果没有 arguments,那么 %s 用NULL代替,%d 用 0 代替
$ printf "%s and %d \n" 
and 0

read: 命令行从输入设备读入内容

#!/bin/bash

# Author : lalal

echo "What is your name?"
read NAME #输入
echo "Hello, $NAME"

定义变量

定义变量variableName="value" 变量名和等号之间不能有空格

使用一个定义过的变量,只要在变量名前面加美元符号($)即可,如:

your_name="lalal"
echo $your_name
echo ${your_name}

for skill in C PHP Python Java 
do
    echo "I am good at ${skill}Script"
done

建议最好加上花括号

readonly
在变量前面加readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

url="http://www.baidu.com"
readonly url
url="http://www.baidu.com"

特殊变量

变量 含义
$0 当前脚本的文件名
$n 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2。
$# 传递给脚本或函数的参数个数。
$* 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。被双引号(" ")包含时,与 $* 稍有不同
$? 上个命令的退出状态,或函数的返回值。
$$ 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。

示例

#!/bin/bash
echo "File Name: $0"
echo "First Parameter : $1"
echo "First Parameter : $2"
echo "Quoted Values: $@"
echo "Quoted Values: $*"
echo "Total Number of Parameters : $#"

结果

$./test.sh Zara Ali
File Name : ./test.sh
First Parameter : Zara
Second Parameter : Ali
Quoted Values: Zara Ali
Quoted Values: Zara Ali
Total Number of Parameters : 2

$* 和 $@ 的区别

$* 和 $@ 都表示传递给函数或脚本的所有参数,不被双引号(" ")包含时,都以"$1" "$2" … "$n" 的形式输出所有参数。

但是当它们被双引号(" ")包含时,"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数;"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数。

退出状态

$? 可以获取上一个命令的退出状态。所谓退出状态,就是上一个命令执行后的返回结果。

if [[ $? != 0 ]];then
  echo "error"
  exit 1;
fi

退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回 1

转义字符

转义字符	含义
\\	反斜杠
\a	警报,响铃
\b	退格(删除键)
\f	换页(FF),将当前位置移到下页开头
\n	换行
\r	回车
\t	水平制表符(tab键) 
\v	垂直制表符

shell默认是不转义上面的字符的。需要加-e选项。

举个例子:

#!/bin/bash
a=11
echo -e "a is $a \n"

运行结果:Value of a is 11
这里 -e 表示对转义字符进行替换。如果不使用 -e 选项,将会原样输出:Value of a is 11\n

命令替换

命令替换是指Shell可以先执行命令,将输出结果暂时保存,在适当的地方输出。语法: command
举例:

#!/bin/bash
DATE=`date`
echo "Date is $DATE"

变量替换

形式 说明
${var} 变量本来的值
${var:-word} 如果变量 var 为空或已被删除(unset),那么返回 word,但不改变 var 的值。
${var:=word} 如果变量 var 为空或已被删除(unset),那么返回 word,并将 var 的值设置为 word。
${var:?message} 如果变量 var 为空或已被删除(unset),那么将消息 message 送到标准错误输出,可以用来检测变量 var 是否可以被正常赋值。若此替换出现在Shell脚本中,那么脚本将停止运行。
${var:+word} 如果变量 var 被定义,那么返回 word,但不改变 var 的值。

完整的shell示例

#!/bin/bash
#zip install

if [ -d php-5.4.25/ext/zip ];then
	cd php-5.4.25/ext/zip
else
	tar zxvf php-5.4.25.tar.gz
	cd php-5.4.25/ext/zip
fi
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config
make
[ $? != 0 ] && exit
make install
echo 
grep 'no-debug-zts-20100525' /usr/local/php/etc/php.ini
if [ $? != 0 ];then
        echo '' >> /usr/local/php/etc/php.ini
        echo 'extension_dir=/usr/local/php/lib/php/extensions/no-debug-zts-20100525' >> /usr/local/php/etc/php.ini
fi
grep 'zip.so' /usr/local/php/etc/php.ini
if [ $? != 0 ];then
	echo 'extension=zip.so' >> /usr/local/php/etc/php.ini
fi
echo "zip install is OK"


/usr/local/apache2/bin/apachectl restart
cd -
rm -rf php-5.4.25
echo "all ok!"
ls /usr/local/php/lib/php/extensions/no-debug-zts-20100525/

useImperativeHandle 使用

useImperativeHandle

useImperativeHandle 是 hook 中提供的允许我们 ref 一个function component 的方案,也是 Hook 在 TypeScript 中使用最复杂的场景。
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染 的父组件可以调用 fancyInputRef.current.focus()。

来一段有生命力的例子

parent:

import React, { FC, forwardRef, useEffect, useRef } from 'react';
import Child, { ChildRef } from './Child';

const ChildRefComponent = forwardRef(Child);

const UseImperativeHandle: FC = () => {
  const childRef = useRef<ChildRef>(null);

  useEffect(() => {
    console.log(`<${'='.repeat(50)}${'='.repeat(50)}>`);
    console.log(childRef.current);
    console.log(`<${'='.repeat(50)}${'='.repeat(50)}>`);
  }, []);

  return (
    <>
      <ChildRefComponent ref={childRef} parent="name" />
    </>
  );
};

export default UseImperativeHandle;

Child.tsx:

import React, { RefForwardingComponent, useImperativeHandle } from 'react';

interface Props {
  parent: string;
}

export interface ChildRef {
  name: string;
  age: number;
}

const Child: RefForwardingComponent<ChildRef, Props> = (props, ref) => {
  console.log(props);

  useImperativeHandle(ref, () => ({
    name: 'yanle',
    age: 27,
  }));

  return <div>my child component</div>;
};

export default Child;

参考文章

createPortal 用法介绍

createPortal

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。 ReactDOM.createPortal(child, container)
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

最基本用法

将子元素插入到 DOM 节点中的不同位置

render() {
  // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一个可以在任何位置的有效 DOM 节点。
  return ReactDOM.createPortal(
    this.props.children,
    domNode
  );
}

通过 Portal 进行事件冒泡

设存在如下 HTML 结构:

<html>
  <body>
    <div id="app-root"></div>
    <div id="modal-root"></div>
  </body>
</html>

在 #app-root 里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #modal-root 冒泡上来的事件。
在父组件里捕获一个来自 portal 冒泡上来的事件,使之能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。
例如,如果你在渲染一个 组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。

// 在 DOM 中有两个容器是兄弟级 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    // 在 Modal 的所有子元素被挂载后,
    // 这个 portal 元素会被嵌入到 DOM 树中,
    // 这意味着子元素将被挂载到一个分离的 DOM 节点中。
    // 如果要求子组件在挂载时可以立刻接入 DOM 树,
    // 例如衡量一个 DOM 节点,
    // 或者在后代节点中使用 ‘autoFocus’,
    // 则需添加 state 到 Modal 中,
    // 仅当 Modal 被插入 DOM 树中才能渲染子元素。
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 当子元素里的按钮被点击时,
    // 这个将会被触发更新父元素的 state,
    // 即使这个按钮在 DOM 中不是直接关联的后代
    this.setState(state => ({
      clicks: state.clicks + 1
    }));
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        <p>Number of clicks: {this.state.clicks}</p>
        <p>
          Open up the browser DevTools
          to observe that the button
          is not a child of the div
          with the onClick handler.
        </p>
        <Modal>
          <Child />
        </Modal>
      </div>
    );
  }
}

function Child() {
  // 这个按钮的点击事件会冒泡到父元素
  // 因为这里没有定义 'onClick' 属性
  return (
    <div className="modal">
      <button>Click</button>
    </div>
  );
}

ReactDOM.render(<Parent />, appRoot);

[docker] 入门 - 05 Docker Compose 多容器部署

Docker Compose 多容器部署

01、部署一个wordPress

启动一个mysql 数据容器: docker run -d --name=mysql -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=wordpress mysql:5.6

启动WordPress: docker run -d -e WORDPRESS_DB_HOST=mysql:3306 --link mysql -p 8080:80 wordpress

然后直接去宿主机访问 ip:8080 就可以了

然后依次安装就OK了

02、Docker Compose 是什么

一个应用需要多个容器, 统一管理容器就显得很痛苦, Docker Compose 就是做这个事儿的 。

是一个基于Docker 的一个工具, 可以定义yml文件来定义多个容器。

默认文件名: docker-compose.yml

三个基本概念: Services 、Networks、 Volumes

Services

一个service代表一个container, 可以从dockerHub拉取, 也可以从本地image 创建。
service启动类似于 docker run , 可以指定network和volume, 也可以直接指定其应用。
例如:

services:
    db:
      image: postgres: 9.4
      volumes: 
        - "db-data:/var/lib/postgresql/data"
      networks:
        - back-tier

上面这个实际上就是这样: docker run -d --network back-tier -v db-data:/var/lib/postgresql/data postgres:9.4

servies: 
    worker:
      build: ./worker
      links:
        - db
        - redis
      networks:
        - back-tier

把第一节的 部署wordpress 用docker-compose 的方式部署, 就是这样子的

version: '3'

services:

  wordpress:
    image: wordpress
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: mysql
      WORDPRESS_DB_PASSWORD: root
    networks:
      - my-bridge

  mysql:
    image: mysql:5.6
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - my-bridge

volumes:
  mysql-data:

networks:
  my-bridge:
    driver: bridge

03、docker-compose的安装和基本使用

安装详情可以参考这里: https://docs.docker.com/compose/install/

具体步骤:
1、 sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
2、 sudo chmod +x /usr/local/bin/docker-compose

重要命令:

命令行 作用
docker-compose up 默认启动docker-compose.yml 文件, 可以看到日志
docker-compose -f docker-compose.yml up 启动指定的yml 配置文件
docker-compose ps 查看启动的容器
docker-compose stop 停止容器
docker-compose start 启动容器
docker-compose down stop and remove
docker-compose up -d 后台执行, 不会看到日志
docker-compose images 可以列出创建容器所需要的image
dcoker-compose exec [service name] bash 进入指定容器的bash

04、水平扩展和均衡负载

docker-compose up --scale web=3 -d

比如有一个这样的 docker-compose.yml 文件:

version: "3"

services:

  redis:
    image: redis

  web:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      REDIS_HOST: redis

  lb:
    image: dockercloud/haproxy
    links:
      - web
    ports:
      - 8080:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock 

web 服务都是链接到 redis 上面的,如果想启动多个web 服务器: docker-compose up --scale web=3 -d

这个时候, 所有的web 服务都是均衡的链接到 redis 上面的。

部署一个复杂应用

03

直接把 code/chapter6/labs/example-voting-app/ 目录下面的内容拷贝到虚拟机上 /home/vagrant/labs/
进入虚拟机之后 运行 docker-compose up 这个过程非常慢, 因为需要拉取基础镜像、生成源码 image;

如果有一些 docker-compose 里面定义的service 有些是需要dockerFile build 生成的, 就可以直接通过 docker-compose build 直接生成就可以了。
然后在执行docker-compose up 就可以了。 如果直接执行 docker-compose build 会直接先build 在 up.

说明 docker-compose 一般来说是用于本地开发的一个工具, 并不适合用于 生产环境

参考文章

canvas 初级

canvas 初级

目录:

No.1 基础使用

1.1 元素

<canvas id="tutorial" width="300" height="300"></canvas>

看起来和标签一样,只是 只有两个可选的属性 width、heigth 属性,而没有 src、alt 属性。
​ 如果不给设置widht、height属性时,则默认 width为300、height为150,单位都是px。也可以使用css属性来设置宽高,但是如宽高属性和初始比例不一致,他会出现扭曲。所以,建议永远不要使用css属性来设置的宽高。

内容替换:
由于某些较老的浏览器(尤其是IE9之前的IE浏览器)或者浏览器不支持HTML元素,在这些浏览器上你应该总是能展示替代内容。
​支持的浏览器会只渲染标签,而忽略其中的替代内容。不支持 的浏览器则 会直接渲染替代内容。

文本替换:

<canvas>
    你的浏览器不支持canvas,请升级你的浏览器
</canvas>

图片替换:

<canvas>
    <img src="./美女.jpg" alt=""> 
</canvas>

结束标签不可省

1.2、渲染上下文(Thre Rending Context)

var canvas = document.getElementById('tutorial');
//获得 2d 上下文对象
var ctx = canvas.getContext('2d');

1.3、检测支持性

var canvas = document.getElementById('tutorial');

if (canvas.getContext){
  var ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here
}

1.4、代码模板

    <html>
    <head>
        <title>Canvas tutorial</title>
        <style type="text/css">
            canvas {
                border: 1px solid black;
            }
        </style>
    </head>
    <canvas id="tutorial" width="300" height="300"></canvas>
    </body>
    <script type="text/javascript">
        function draw(){
            var canvas = document.getElementById('tutorial');
            if(!canvas.getContext) return;
            var ctx = canvas.getContext("2d");
            //开始代码
    
        }
        draw();
    </script>
    </html>

1.5、一个简单的例子

绘制两个长方形:

<html>
<head>
    <title>Canvas tutorial</title>
    <style type="text/css">
        canvas {
            border: 1px solid black;
        }
    </style>
</head>
<canvas id="tutorial" width="300" height="300"></canvas>
</body>
<script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        var ctx = canvas.getContext("2d");
        ctx.fillStyle = "rgb(200,0,0)";
        //绘制矩形
        ctx.fillRect (10, 10, 55, 50);

        ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
        ctx.fillRect (30, 30, 55, 50);
    }
    draw();
</script>
</html>

详情请见示例02

No.02 绘制形状

只支持一种原生的 图形绘制:矩形。所有其他图形都至少需要生成一种路径(path)。不过,我们拥有众多路径生成的方法让复杂图形的绘制成为了可能。
canvast 提供了三种方法绘制矩形:
fillRect(x, y, width, height) 绘制一个填充的矩形
strockRect(x, y, width, height) 绘制一个矩形的边框
clearRect(x, y, widh, height) 清除指定的矩形区域,然后这块区域会变的完全透明。

说明:
这3个方法具有相同的参数。
​x, y:指的是矩形的左上角的坐标。(相对于canvas的坐标原点)
​width, height:指的是绘制的矩形的宽和高。

示例:

function draw(){
    var canvas = document.getElementById('tutorial');
    if(!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.fillRect(10, 10, 100, 50);  //绘制矩形,填充的默认颜色为黑色
    ctx.strokeRect(10, 70, 100, 50);  //绘制矩形边框
    ctx.clearRect(15, 15, 50, 25);
}
draw();

详情请见示例3

No.03 绘制路径

路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。

使用路径绘制图形需要一些额外的步骤:

1、创建路径起始点
2、调用绘制方法去绘制出路径
3、把路径封闭
4、一旦路径生成,通过描边或填充路径区域来渲染图形。

下面是需要用到的方法:

1、beginPath()
新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径
2、moveTo(x, y)
把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。
3、closePath()
闭合路径之后,图形绘制命令又重新指向到上下文中
4、stroke()
通过线条来绘制图形轮廓
5、fill()
通过填充路径的内容区域生成实心的图形

3.1、绘制线段

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.beginPath(); //新建一条path
        ctx.moveTo(50, 50);
        ctx.lineTo(200, 50);
        ctx.closePath();
        ctx.stroke();
    }
    draw();

请见示例4

3.2、绘制一个三角形

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.beginPath();
        ctx.moveTo(50, 50);
        ctx.lineTo(200, 50);
        ctx.lineTo(200, 200);
        ctx.closePath();
        ctx.stroke();
    }
    draw();

请见示例5

3.3、填充一个三角形

    function draw() {
        let canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.moveTo(50, 50);
        ctx.lineTo(200, 50);
        ctx.lineTo(200, 200);
        ctx.fill();
    }
    draw();

示例6

3.4、绘制圆弧

arc(x, y, r, startAngle, endAngle, anticlockwise):

以(x, y)为圆心,以r为半径,从 startAngle弧度开始到endAngle弧度结束。anticlosewise是布尔值,true表示逆时针,false表示顺时针。(默认是顺时针)
注意:
这里的度数都是弧度。
0弧度是指的x轴正方形
radians=(Math.PI/180)*degrees //角度转换成弧度

arcTo(x1, y1, x2, y2, radius):

根据给定的控制点和半径画一段圆弧,最后再以直线连接两个控制点。

圆弧案例1:

    function draw() {
        let canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.beginPath();
        ctx.arc(110, 110, 40, 0, Math.PI / 2, false);
        ctx.stroke()
    }

    draw();

示例7

圆弧案例2:

    function draw() {
        let canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.beginPath();
        ctx.arc(50, 50, 40, 0, Math.PI / 2, false);
        ctx.stroke();

        ctx.beginPath();
        ctx.arc(150, 50, 40, 0, -Math.PI / 2, true);
        ctx.closePath();
        ctx.stroke();

        ctx.beginPath();
        ctx.arc(50, 150, 40, -Math.PI / 2, Math.PI / 2, false);
        ctx.fill();

        ctx.beginPath();
        ctx.arc(150, 150, 40, 0, Math.PI, false);
        ctx.fill();


    }
    draw();

示例8

圆弧示例3:

    function draw(){
        let canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        ctx.beginPath();
        ctx.moveTo(50, 50);
        //参数1、2:控制点1坐标   参数3、4:控制点2坐标  参数5:圆弧半径
        ctx.arcTo(200, 50, 200, 200, 100);
        ctx.lineTo(200, 200);
        ctx.stroke();
    
        ctx.beginPath();
        ctx.rect(50, 50, 10, 10);
        ctx.rect(200, 50, 10, 10);
        ctx.rect(200, 200, 10, 10);
        ctx.fill()
    }
    draw();

arcTo方法的说明:
​这个方法可以这样理解。绘制的弧形是由两条切线所决定。
​第 1 条切线:起始点和控制点1决定的直线。
​第 2 条切线:控制点1 和控制点2决定的直线。
其实绘制的圆弧就是与这两条直线相切的圆弧。
示例9

3.5、绘制贝塞尔曲线

绘制二次贝塞尔曲线

quadraticCurveTo(cp1x, cp1y, x, y):
说明:
​参数1和2:控制点坐标
​参数3和4:结束点坐标

    function draw() {
        let canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.beginPath();
        ctx.moveTo(10, 200);
        let cp1x = 40, cp1y = 100;
        let x = 200, y = 200;
        ctx.quadraticCurveTo(cp1x, cp1y, x, y);
        ctx.stroke();

        ctx.beginPath();
        ctx.rect(10, 200, 10, 10);
        ctx.rect(cp1x, cp1y, 10, 10);
        ctx.rect(x, y, 10, 10);
        ctx.fill();
    }
    draw();

示例10

绘制三次贝塞尔曲线

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y):
说明:
​参数1和2:控制点1的坐标
​参数3和4:控制点2的坐标
参数5和6:结束点的坐标

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码

        ctx.beginPath();
        ctx.moveTo(40, 200); //起始点
        let cp1x = 20, cp1y = 100;  //控制点1
        let cp2x = 100, cp2y = 120;  //控制点2
        let x = 200, y = 200; // 结束点
        //绘制二次贝塞尔曲线
        ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        ctx.stroke();

        ctx.beginPath();
        ctx.rect(40, 200, 10, 10);
        ctx.rect(cp1x, cp1y, 10, 10);
        ctx.rect(cp2x, cp2y, 10, 10);
        ctx.rect(x, y, 10, 10);
        ctx.fill();
    }
    draw();

示例11

No.04 添加样式和颜色

在前面的绘制矩形章节中,只用到了默认的线条和颜色。
​如果想要给图形上色,有两个重要的属性可以做到。

基本使用方式

1、fillStyle = color 设置图形的填充颜色
2、 strokeStyle = color 设置图形轮廓的颜色

备注:

  1. color 可以是表示 css 颜色值的字符串、渐变对象或者图案对象。
  2. 默认情况下,线条和填充颜色都是黑色。
  3. 一旦您设置了 strokeStyle 或者 fillStyle 的值,那么这个新值就会成为新绘制的图形的默认值。如果你要给每个图形上不同的颜色,你需要重新设置 fillStylestrokeStyle 的值。

fillStyle示例:

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        for (let i = 0; i < 6; i++){
            for (let j = 0; j < 6; j++){
                ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' +
                    Math.floor(255 - 42.5 * j) + ',0)';
                ctx.fillRect(j * 50, i * 50, 50, 50);
            }
        }
    }
    draw();

示例12

strokeStyle示例:

    function randomInt(from, to){
        return parseInt(Math.random() * (to - from + 1) + from);
    }
    function draw(){
        let canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        for (let i = 0; i < 6; i++){
            for (let j = 0; j < 6; j++){
                ctx.strokeStyle = `rgb(${randomInt(0, 255)},${randomInt(0, 255)},${randomInt(0, 255)})`;
                ctx.strokeRect(j * 50, i * 50, 40, 40);
            }
        }
    }
    draw();

示例13

Transparency(透明度)

globalAlpha = transparencyValue
这个属性影响到 canvas 里所有图形的透明度,有效的值范围是 0.0 (完全透明)到 1.0(完全不透明),默认是 1.0。
globalAlpha 属性在需要绘制大量拥有相同透明度的图形时候相当高效。不过,我认为使用rgba()设置透明度更加好一些。

line style

1、lineWidth = value
线宽。只能是正值。默认是1.0。
起始点和终点的连线为中心,上下各占线宽的一半

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.beginPath();
        ctx.moveTo(10, 10);
        ctx.lineTo(100, 10);
        ctx.lineWidth = 10;
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(110, 10);
        ctx.lineTo(160, 10);
        ctx.lineWidth = 20;
        ctx.stroke();
    }
    draw();

示例14

2、lineCap = type
线条末端样式。
共有3个值:
butt:线段末端以方形结束
round:线段末端以圆形结束
square:线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域。

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        let lineCaps = ['butt', 'round', 'square'];
        for(let i = 0; i< 3; i++) {
            ctx.beginPath();
            ctx.moveTo(20 + 30*i, 30);
            ctx.lineTo(20 + 30*i, 100);
            ctx.lineWidth = 20;
            ctx.lineCap = lineCaps[i];
            ctx.stroke();
        }
        ctx.beginPath();
        ctx.moveTo(0, 30);
        ctx.lineTo(300, 30);

        ctx.moveTo(0, 100);
        ctx.lineTo(300, 100);
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 1;
        ctx.stroke();
    }
    draw();

实例15

3、lineJoin = type
同一个path内,设定线条与线条间接合处的样式。共有3个值round, bevel 和 miter:
round:通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度。
bevel:在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角。
miter(默认):通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        let lineJoin = ['round', 'bevel', 'miter'];
        ctx.lineWidth = 20;

        for (let i = 0; i < lineJoin.length; i++){
            ctx.lineJoin = lineJoin[i];
            ctx.beginPath();
            ctx.moveTo(50, 50 + i * 50);
            ctx.lineTo(100, 100 + i * 50);
            ctx.lineTo(150, 50 + i * 50);
            ctx.lineTo(200, 100 + i * 50);
            ctx.lineTo(250, 50 + i * 50);
            ctx.stroke();
        }

    }
    draw();

示例16、线条连接处样式

虚线

setLineDash 方法和 lineDashOffset 属性来制定虚线样式. setLineDash 方法接受一个数组,来指定线段与间隙的交替;lineDashOffset属性设置起始偏移量.
getLineDash() :返回一个包含当前虚线样式,长度为非负偶数的数组。

    function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.setLineDash([20, 5]);  // [实线长度, 间隙长度]
        ctx.lineDashOffset = 25;
        ctx.strokeRect(50, 50, 210, 210);
    }
    
    draw();

17、虚线

No.05 绘制文本

canvas 提供了两种方法来渲染文本:

1、fillText(text, x, y [, maxWidth])
在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的.

2、strokeText(text, x, y [, maxWidth])
在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的.

function draw(){
    let canvas = document.getElementById('tutorial');
    if(!canvas.getContext) return;
    let ctx = canvas.getContext("2d");
    //开始代码
    ctx.font = '100px sans-serif';
    ctx.fillText('颜乐乐', 10, 100);
    ctx.strokeText('颜乐乐',10, 200);
}
draw();

给文本添加样式

font = value
当前我们用来绘制文本的样式。这个字符串使用和 CSS font属性相同的语法. 默认的字体是 10px sans-serif。

textAlign = value
文本对齐选项. 可选的值包括:start, end, left, right or center. 默认值是 start。

textBaseline = value
基线对齐选项,可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。默认值是 alphabetic。

direction = value
文本方向。可能的值包括:ltr, rtl, inherit。默认值是 inherit。

No.06 绘制图片

  • 由零开始创建图片
    创建元素
var img = new Image();   // 创建一个<img>元素
img.src = 'myImage.png'; // 设置图片源地址

脚本执行后图片开始装载

绘制img

//参数1:要绘制的img  参数2、3:绘制的img在canvas中的坐标
ctx.drawImage(img,0,0); 

注意:
考虑到图片是从网络加载,如果 drawImage 的时候图片还没有完全加载完成,则什么都不做,个别浏览器会抛异常。所以我们应该保证在 img 绘制完成之后再 drawImage。

var img = new Image();   // 创建img元素
img.onload = function(){
  ctx.drawImage(img, 0, 0)
}
img.src = 'myImage.png'; // 设置图片源地址
  • 绘制 img 标签元素中的图片
    img 可以 new 也可以来源于我们页面的 标签
<img src="./img.jpg" alt="" width="300"><br>
<canvas id="tutorial" width="600" height="400"></canvas>

</body>
<script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        var ctx = canvas.getContext("2d");
        var img = document.querySelector("img");
        ctx.drawImage(img, 0, 0);
    }
    document.querySelector("img").onclick = function (){
        draw();
    }
</script>
  • 缩放图片
    drawImage() 也可以再添加两个参数:
    drawImage(image, x, y, width, height)
    这个方法多了2个参数:width 和 height,这两个参数用来控制 当像canvas画入时应该缩放的大小。
    ctx.drawImage(img, 0, 0, 400, 200)

  • 切片(slice)
    drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
    第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用。
    其他8个参数:
    前4个是定义图像源的切片位置和大小,
    后4个则是定义切片的目标显示位置和大小。

No.07 状态的保存和恢复

Saving and restoring state是绘制复杂图形时必不可少的操作。
save()和restore()
save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。
Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。

  • 1、关于 save()
    Canvas状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存。一个绘画状态包括:
    当前应用的变形(即移动,旋转和缩放)
    strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值
    当前的裁切路径(clipping path)
    可以调用任意多次 save方法。(类似数组的push())

  • 2、关于restore()
    每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。(类似数组的pop())

No.08 变形

  • translate(x, y)
    用来移动 canvas 的原点到指定的位置
    translate方法接受两个参数。x 是左右偏移量,y 是上下偏移量,如右图所示。
    在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore 方法比手动恢复原先的状态要简单得多。又如果你是在一个循环中做位移但没有保存和恢复canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。
    注意:translate移动的是canvas的坐标原点。(坐标变换)
<canvas id="tutorial" width="600" height="600"></canvas>
</body>
<script type="text/javascript">
    let ctx;
    function draw(){
        let canvas = document.getElementById('tutorial');
        if (!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        ctx.save(); //保存坐原点平移之前的状态
        ctx.translate(100, 100);
        ctx.strokeRect(0, 0, 100, 100);
        ctx.restore(); //恢复到最初状态
        ctx.translate(220, 220);
        ctx.fillRect(0, 0, 100, 100)
    }
    draw();
</script>
  • rotate
    rotate(angle)
    旋转坐标轴。
    这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。
    旋转的中心是坐标原点。
function draw(){
    let canvas = document.getElementById('tutorial');
    if(!canvas.getContext) return;
    let ctx = canvas.getContext("2d");
    //开始代码
    ctx.fillStyle = "red";
    ctx.save();

    ctx.translate(100, 100);
    ctx.rotate(Math.PI / 180 * 45);
    ctx.fillStyle = "blue";
    ctx.fillRect(0, 0, 100, 100);
    ctx.restore();

    ctx.save();
    ctx.translate(0, 0);
    ctx.fillRect(0, 0, 50, 50);
    ctx.restore();
}
draw();
  • scale
    scale(x, y)
    我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。
    scale方法接受两个参数。x,y分别是横轴和纵轴的缩放因子,它们都必须是正值。值比 1.0 小表示缩 小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有。
    默认情况下,canvas 的 1 单位就是 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。

  • transform(变形矩阵)
    transform(a, b, c, d, e, f)
    a (m11): Horizontal scaling. (水平伸缩)
    b (m12): Horizontal skewing.(水平歪斜)
    c (m21): Vertical skewing.(垂直歪斜)
    d (m22): Vertical scaling.(垂直伸缩)
    e (dx): Horizontal moving.(水平移动)
    f (dy): Vertical moving.(垂直移动)

示例:

function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        //开始代码
        let ctx;
        function draw(){
            let canvas = document.getElementById('tutorial');
            if (!canvas.getContext) return;
            let ctx = canvas.getContext("2d");
            ctx.transform(1, 1, 0, 1, 0, 0);
            ctx.fillRect(0, 0, 100, 100);
        }
        draw();
    }
    draw();

No.09 合成

在前面的所有例子中、,我们总是将一个图形画在另一个之上,对于其他更多的情况,仅仅这样是远远不够的。比如,对合成的图形来说,绘制顺序会有限制。不过,我们可以利用 globalCompositeOperation 属性来改变这种状况。
globalCompositeOperation = type

function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.fillStyle = "blue";                       //老图像
        ctx.fillRect(0, 0, 200, 200);

        ctx.globalCompositeOperation = "source-over"; //全局合成操作
        ctx.fillStyle = "red";                        //新图像
        ctx.fillRect(100, 100, 200, 200);
    }
    draw();

type是下面 13 种字符串值之一:

  1. source-over(default)
    这是默认设置,新图像会覆盖在原有图像。

  2. source-in
    仅仅会出现新图像与原来图像重叠的部分,其他区域都变成透明的。(包括其他的老图像区域也会透明)

  3. source-out
    仅仅显示新图像与老图像没有重叠的部分,其余部分全部透明。(老图像也不显示)

  4. source-atop
    新图像仅仅显示与老图像重叠区域。老图像仍然可以显示。(新图像也不显示)

  5. destination-over
    新图像会在老图像的下面。

  6. destination-in
    仅仅新老图像重叠部分的老图像被显示,其他区域全部透明。

  7. destination-out
    仅仅老图像与新图像没有重叠的部分。 注意显示的是老图像的部分区域。

  8. destination-atop
    老图像仅仅仅仅显示重叠部分,新图像会显示在老图像的下面。

  9. lighter
    新老图像都显示,但是重叠区域的颜色做加处理

  10. darken
    保留重叠部分最黑的像素。(每个颜色位进行比较,得到最小的)
    blue: #0000ff
    red: #ff0000
    所以重叠部分的颜色:#000000

  11. lighten
    保证重叠部分最量的像素。(每个颜色位进行比较,得到最大的)
    blue: #0000ff
    red: #ff0000
    所以重叠部分的颜色:#ff00ff

  12. xor
    重叠部分会变成透明

  13. copy
    只有新图像会被保留,其余的全部被清除(边透明)

No.10 剪裁路径

clip()
把已经创建的路径转换成裁剪路径。
裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。
注意:clip()只能遮罩在这个方法调用之后绘制的图像,如果是clip()方法调用之前绘制的图像,则无法实现遮罩。

function draw(){
        let canvas = document.getElementById('tutorial');
        if(!canvas.getContext) return;
        let ctx = canvas.getContext("2d");
        //开始代码
        ctx.beginPath();
        ctx.arc(20,20, 100, 0, Math.PI * 2);
        ctx.clip();

        ctx.fillStyle = "pink";
        ctx.fillRect(20, 20, 100,100);
    }
    draw();

No.11 动画

动画的基本步骤
1、 清空canvas
再绘制每一帧动画之前,需要清空所有。清空所有最简单的做法就是 clearRect() 方法

2、 保存canvas状态
如果在绘制的过程中会更改canvas的状态(颜色、移动了坐标原点等),又在绘制每一帧时都是原始状态的话,则最好保存下canvas的状态

3、 绘制动画图形
这一步才是真正的绘制动画帧

4、 恢复canvas状态
如果你前面保存了canvas状态,则应该在绘制完成一帧之后恢复canvas状态。

控制动画
我们可用通过canvas的方法或者自定义的方法把图像会知道到canvas上。正常情况,我们能看到绘制的结果是在脚本执行结束之后。例如,我们不可能在一个 for 循环内部完成动画。
也就是,为了执行动画,我们需要一些可以定时执行重绘的方法。

一般用到下面三个方法:
setInterval()
setTimeout()
requestAnimationFrame()

请参看如下两个示例示例代码:
26、太阳系
27、模拟时钟

参看博文

[docker] 入门 - 03 docker网络

Docker网络

主要分为 单机网络 和 多机网络

01、基础网络知识

比如笔记本访问别人的网络, 都是通信包访问的, http协议;
略........

路由的概念: 略..........

ip地址和路由:
ip 地址是网络唯一标识 略.........

共有IP 和 私有 IP:
共有IP 是唯一标识, 可以访问internet
私有IP 不可以在互联网使用, 仅供机构内部使用(例如校园网、公司内网)

网络地址转换 NAT:
这个可以理解为一个翻译, 比如作为一个校园网, 有一个或者有几个共有IP地址,
校园内网私有IP 访问外部网络的时候, 先经过 NAT 的转换, NAT 就记住了私有IP和端口, 然后发送请求。
返回成功之后, 回到NAT 就查询是哪个私有地址访问的,然后数据包就回到该访问的私有地址。

Ping(ICMP协议) 和 telnet
Ping:验证IP的可达性

[vagrant@docker-node1 ~]$ ping www.baidu.com
PING www.a.shifen.com (180.97.33.107) 56(84) bytes of data.
64 bytes from 180.97.33.107 (180.97.33.107): icmp_seq=1 ttl=63 time=44.0 ms
64 bytes from 180.97.33.107 (180.97.33.107): icmp_seq=2 ttl=63 time=91.3 ms
64 bytes from 180.97.33.107 (180.97.33.107): icmp_seq=3 ttl=63 time=38.6 ms
64 bytes from 180.97.33.107 (180.97.33.107): icmp_seq=4 ttl=63 time=43.5 ms
64 bytes from 180.97.33.107 (180.97.33.107): icmp_seq=5 ttl=63 time=55.7 ms
^C
--- www.a.shifen.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4011ms
rtt min/avg/max/mdev = 38.671/54.664/91.302/19.155 ms

telnet: 验证服务的可用性

02、网络命名空间

首先拉取一个 busybox(非常小的一个linux镜像)

然后运行它: sudo docker run -d --name=test1 busybox /bin/sh -c "while true; do sleep 3600; done" 这个命令就是为了保证这个容器会一直在后台执行

[vagrant@docker-node1 ~]$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
3688c4bbc164        busybox             "/bin/sh -c 'while t…"   17 seconds ago      Up 16 seconds                           test1

通过交互式命令 进入到容器里面(进入容器内部、进入容器里面): docker exec -it 3688c4bbc164 /bin/sh
在容器里面就可以运行命令了。
首先运行 ip a / ip address

/ # ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

上面就是一个网络的 name space 网络命名空间

在虚拟机本地也可以运行 ip address 命令

[vagrant@docker-node1 ~]$ ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:26:10:60 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0
       valid_lft 84244sec preferred_lft 84244sec
    inet6 fe80::5054:ff:fe26:1060/64 scope link 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:b3:7d:f8 brd ff:ff:ff:ff:ff:ff
    inet 192.168.205.10/24 brd 192.168.205.255 scope global noprefixroute eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:feb3:7df8/64 scope link 
       valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ea:c0:4f:9e brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:eaff:fec0:4f9e/64 scope link 
       valid_lft forever preferred_lft forever
6: veth656a81d@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether fa:eb:9e:97:04:21 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::f8eb:9eff:fe97:421/64 scope link 
       valid_lft forever preferred_lft forever

上面的也是一个 网络命名空间

两个网络命名空间是不一样的, 而且是完全隔离的。

运行第二个容器

sudo docker run -d --name=test2 busybox /bin/sh -c "while true; do sleep 3600; done"

[vagrant@docker-node1 ~]$ docker container ls 
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
04ac9d71cf7b        busybox             "/bin/sh -c 'while t…"   5 seconds ago       Up 5 seconds                            test2
3688c4bbc164        busybox             "/bin/sh -c 'while t…"   2 days ago          Up 28 seconds                           test1

如果只是想看某一个容器的网络, 就可以直接运行这样的命名: sudo docker exec [container id | name] ip address
例如, 查看第一个容器的IP 地址: sudo docker exec 04ac9d71cf7b ip address

[vagrant@docker-node1 ~]$ docker exec test1 ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

就可以打印出容器的命名空间

查看容器 test2 的命名空空间: docker exec test2 ip address

[vagrant@docker-node1 ~]$ docker exec test2 ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

可以发现 test1 和 test2 的命名空间的区别。

同时可以进入 test1 里面, 是能够ping通test2的。 例如交互式进入test1: [vagrant@docker-node1 ~]$ docker exec -it test1 /bin/sh

/ # ping 127.0.0.3
PING 127.0.0.3 (127.0.0.3): 56 data bytes
64 bytes from 127.0.0.3: seq=0 ttl=64 time=0.077 ms
64 bytes from 127.0.0.3: seq=1 ttl=64 time=0.095 ms
64 bytes from 127.0.0.3: seq=2 ttl=64 time=0.268 ms
64 bytes from 127.0.0.3: seq=3 ttl=64 time=0.243 ms
^
--- 127.0.0.3 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.077/0.170/0.268 ms

同理, test2 也是可以ping通 test1, 这就说明了, 这两个容器的net work space是可以相互通信的。

创建一个Linux NetWork NameSpace

查看本机的net work space: sudo ip netns list
删除本机的net work space: sudo ip netns delete [nws name]
创建本机的net work space: sudo ip netns add [nws name]

例如我创建了两个net work space, 分比为 test1 和 test2。
查看这两个创建好的net work space的ip: sudo ip netns exec test1 ip address

[vagrant@docker-node1 ~]$ sudo ip netns exec test1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

会发现有一个本地端口, 端口没有地址,没有up 状态。

输入命令行: ip link 可以查看 链接状态

[vagrant@docker-node1 ~]$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:26:10:60 brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:b3:7d:f8 brd ff:ff:ff:ff:ff:ff
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default 
    link/ether 02:42:5e:b4:8e:8d brd ff:ff:ff:ff:ff:ff
6: vethdb34b3d@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 
    link/ether ea:07:f0:af:67:25 brd ff:ff:ff:ff:ff:ff link-netnsid 0
8: veth9d224da@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 
    link/ether 46:18:24:54:94:9e brd ff:ff:ff:ff:ff:ff link-netnsid 1

查看test1 的ip link 状态: sudo ip netns exec test1 ip link

1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

如何让 test1 的ip 状态 up 起来: sudo ip netns exec test1 ip link set dev lo up
然后查看link状态

[vagrant@docker-node1 ~]$ sudo ip netns exec test1 ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

发现一个问题, 这个ip 的状态是一个 UNKNOWN , 而且本机的local 端口也是一个 UNKNOWN 状态。
出现这个情况的原因实际上是以为, ip link 是需要链接起来, 两个 NetWork NameSpace 链接起来之后, 才能是up 状态。 单个端口是没有办法up的。

创建一对链接

通过 ip link 可以查看本机link
添加一对link: sudo ip link add veth-test1 type veth peer name veth-test2

[vagrant@docker-node1 ~]$ sudo ip link add veth-test1 type veth peer name veth-test2
[vagrant@docker-node1 ~]$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:26:10:60 brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:b3:7d:f8 brd ff:ff:ff:ff:ff:ff
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default 
    link/ether 02:42:40:85:19:da brd ff:ff:ff:ff:ff:ff
5: veth-test2@veth-test1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 92:b2:ab:20:ed:43 brd ff:ff:ff:ff:ff:ff
6: veth-test1@veth-test2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether b2:28:fa:22:69:34 brd ff:ff:ff:ff:ff:ff

最后一对链接就是新添加的, 没有ip 状态也是down 的。

veth-test1 添加到 test1 里面去: sudo ip link set veth-test1 netns test1

[vagrant@docker-node1 ~]$ sudo ip netns exec test1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
6: veth-test1@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether b2:28:fa:22:69:34 brd ff:ff:ff:ff:ff:ff link-netnsid 0

执行之后, 发现, 这一条link 添加到 test1 里面去了, 然后本地的 这一条link 不见了。

同理 sudo ip link set veth-test2 natns test2
然后会发现本地, 原来的第五条link 也不见了, 这一天link 被添加到test2 里面去了。

[vagrant@docker-node1 ~]$ sudo ip netns exec test2 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: veth-test2@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 92:b2:ab:20:ed:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0

这样我们就成功的将这一对link 分别添加到了 两个 netns 中。
会有连个问题, 这两个link 都是down 的状态, 而且没有IP地址。

分别给两个 veth-test 分配地址:
sudo ip netns exec test1 ip addr add 192.168.1.1/24 dev veth-test1
sudo ip netns exec test2 ip addr add 192.168.1.2/24 dev veth-test2

但是添加了之后, 查看ip link 依然没有ip 地址
需要先启动这两个link:
sudo ip netns exec test1 ip link set dev veth-test1 up
sudo ip netns exec test2 ip link set dev veth-test2 up

启动完成之后, 就可以通过: sudo ip netns exec test1 ip link 查看link 状态
通过: sudo ip netns exec test1 ip a

这样已经有ip 地址了, 也是up 起来了。 这就说明两个netns 已经完全链接起来了。

[vagrant@docker-node1 ~]$ sudo ip netns exec test1 ping 192.168.1.2
PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=64 time=0.062 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=64 time=0.065 ms
64 bytes from 192.168.1.2: icmp_seq=3 ttl=64 time=0.064 ms
64 bytes from 192.168.1.2: icmp_seq=4 ttl=64 time=0.066 ms
64 bytes from 192.168.1.2: icmp_seq=5 ttl=64 time=0.065 ms
--- 192.168.1.2 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4001ms
rtt min/avg/max/mdev = 0.062/0.064/0.066/0.007 ms

同理 test2 netns 也可以ping 通 test1

03、bridge

上一节说的两个net work space 虽然是完全隔壁的, 但是是可以相互ping 通的。

启动一个容器test1: sudo docker run -d --name=test1 busybox /bin/sh -c "while true; do sleep 3600; done"
查看 docker 的网络: docker network ls

[vagrant@docker-node1 ~]$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
dd09816eb1ce        bridge              bridge              local
ad589c9fa968        host                host                local
0dfc9dbf0808        none                null                local

其中那个 bridge 就是本机的网络模式

查看docker 网络宿主情况: sudo docker network inspect [network id]

......
"Containers": {
    "3688c4bbc1644ec80362ed97fc9159d80f32e62135cdd7e79280c6b7f1aee72f": {
        "Name": "test1",
        "EndpointID": "d442b1182e6ca30a8107df5ea3c24595cd87423d95ffc215f28070802b187f21",
        "MacAddress": "02:42:ac:11:00:02",
        "IPv4Address": "172.17.0.2/16",
        "IPv6Address": ""
    }
},
......

可以查看本机ip情况

[vagrant@docker-node1 ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:26:10:60 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0
       valid_lft 82167sec preferred_lft 82167sec
    inet6 fe80::5054:ff:fe26:1060/64 scope link 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:b3:7d:f8 brd ff:ff:ff:ff:ff:ff
    inet 192.168.205.10/24 brd 192.168.205.255 scope global noprefixroute eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:feb3:7df8/64 scope link 
       valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:40:85:19:da brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:40ff:fe85:19da/64 scope link 
       valid_lft forever preferred_lft forever
8: vetha622445@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 4e:f1:3d:44:92:5b brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::4cf1:3dff:fe44:925b/64 scope link 
       valid_lft forever preferred_lft forever

其中docker0的 netns 是本机, 那么 test1 container 是如何连接 本机的呢, 就是通过 vetha622445@if7 连接的。

[vagrant@docker-node1 ~]$ docker exec test1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

其中 eth0@if8 和 本机的 vetha622445@if7实际上是一对link. 通过这样的链接,
就可以链接到主机上面去了, 准确的说是链接到 docker0的网络上面去了

链接情况的验证: brctl
这个工具是需要安装的: sudo yum install -y bridge-utils

[vagrant@docker-node1 ~]$ brctl show
bridge name	bridge id		STP enabled	i
nterfaces
docker0		8000.0242408519da	no		vetha622445

如果在创建一个container: sudo docker run -d --name=test2 busybox /bin/sh -c "while true; do sleep 3600; done"

bridge name	bridge id		STP enabled	interfaces
docker0		8000.0242408519da	no		vetha622445
							            vethbe1a1b5

就有两个链接了。

两个容器之间的通信:
01

外网链接:
02

04、容器之间的link

创建容器之前, 可以给容器一个name, 创建第二个容器的时候, 也给一个name。 两个容器之间可以通过name link 起来,
这样就不需要每次都通过ip link 起来了。

首先启动上一节的test1容器: docker start tset1
重新创建test2 容器, 通过link 的方式:
docker run -d --name=test2 --link test1 busybox /bin/sh -c "while true; do sleep 3600; done"
做了这样的操作之后, 进入test2 的bin/sh: docker exec -it test2 /bin/sh.
就可以直接ping test1了, 而且还可以ping 通.

[vagrant@docker-node1 ~]$ docker exec -it test2 /bin/sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
/ # ping test1
PING test1 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.112 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.171 ms
64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.150 ms
64 bytes from 172.17.0.2: seq=3 ttl=64 time=0.128 ms
--- test1 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.112/0.140/0.171 ms

但是反之, 是ping不通的, 因为link 是单向的。

自己创建一个bridge

命令行: docker network create [OPTIONS] NETWORK [flags]
创建命令: docker network create -d bridge my-bridge

[vagrant@docker-node1 ~]$ docker network create -d bridge my-bridge
c17581e7b9cf62b6a39ab4220313cd9246f9c598f2a1e8a1bf4896cdb8145fc4
[vagrant@docker-node1 ~]$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
4159a7a8ff49        bridge              bridge              local
ad589c9fa968        host                host                local
c17581e7b9cf        my-bridge           bridge              local
0dfc9dbf0808        none                null                local
[vagrant@docker-node1 ~]$ brctl show
bridge name	bridge id		STP enabled	interfaces
br-c17581e7b9cf		8000.02425a563121	no		
docker0		8000.02422e50fe40	no		veth6360140

新建一个container 链接到 新建的 bridge
docker run -d --name=test3 --network my-bridge busybox /bin/sh -c "while true; do sleep 3600; done"

[vagrant@docker-node1 ~]$ brctl show
bridge name	bridge id		STP enabled	interfaces
br-c17581e7b9cf		8000.02425a563121	no		veth6467873
docker0		8000.02422e50fe40	no		veth6360140

发现新建的就有端口了
可以通过 docker network inspect [bridge id] 查看 链接情况

已经创建好的容器链接到新的bridge上

sudo docker network connect [bridge] [continer]
例如我们希望把 test2 链接到 my-bridge : docker network connect my-bridge test2

如果是两个容器都link 到自己新建的bridge 上面, 那么就是可以直接默认 --link 在一起。
意思就是, 比如, 我们把test2 和 test3 都链接到了my-bridge上面, 那么test2和test3 是可以通过ip ping 通。
但是直接ping name 也是可以ping通的。

一个container是可以链接多个 bridge 的。

05、容器的端口映射

如果我们启动一个服务 docker run -d --name web nginx, 服务已经启动,在docker node 宿主机是可以访问的。 但是外界依然无法访问。

映射本地: docker run -d --name=web -p 80:80 nginx

[vagrant@docker-node1 ~]$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
29ac8c1c1d14        nginx               "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes        0.0.0.0:80->80/tcp   web

这个时候外面就可以访问了。

06、host和none

补充一个 docker network option

  connect     Connect a container to a network
  create      Create a network
  disconnect  Disconnect a container from a network
  inspect     Display detailed information on one or more networks
  ls          List networks
  prune       Remove all unused networks
  rm          Remove one or more networks
[vagrant@docker-node1 ~]$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
4159a7a8ff49        bridge              bridge              local
ad589c9fa968        host                host                local
c17581e7b9cf        my-bridge           bridge              local
0dfc9dbf0808        none                null                local

最后还有两种网络连接一种是 none 还有一种是 host
none 是一种孤立的网络连接方式 这种方式创建的容器, 只能通过 docker exec -it [name/id] /bin/sh 方式连接。没有其他的链接方式了。
host 这种方式创建的容器, 会成为跟住宿机共享所有网络配置, 非常不常用, 会出现网络冲突。

07、复杂docker链接示例

启动一个docker 1 放置node 服务应用, 链接mysql , mysql 启动在docker2

docker安装mysql

<<<<<<< Updated upstream
https://www.cnblogs.com/pwc1996/p/5425234.html
sudo docker run --name pwc-mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql

Docker 安装 MongoDB,配置用户名和密码

https://blog.csdn.net/xiaojin21cen/article/details/84994452

=======
https://www.cnblogs.com/pwc1996/p/5425234.html

sudo docker pull mysql:5.7

# 启动容器
sudo docker run --name pwc-mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql

Stashed changes

sudo docker pull redis

sudo docker run -p 6379:6379 --name redis -v /data/redis/redis.conf:/etc/redis/redis.conf  -v /data/redis/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes

-p 6379:6379:把容器内的6379端口映射到宿主机6379端口
-v /data/redis/redis.conf:/etc/redis/redis.conf:把宿主机配置好的redis.conf放到容器内的这个位置中
-v /data/redis/data:/data:把redis持久化的数据在宿主机内显示,做数据备份
redis-server /etc/redis/redis.conf:这个是关键配置,让redis不是无配置启动,而是按照这个redis.conf的配置启动
–appendonly yes:redis启动后数据持久化

08、多机通信

VXLAN: 多机通信的技术

首先启动两个虚拟机, ip 分别为, 192.168.205.10192.168.205.11 两个虚拟机是可以互相ping 通的。

多机通信的实现

解决出于不同机器上的容器, 是怎么通信的问题。

Overlay 通信方式 , 需要分布式存储
整个方式 请看这里: multi-host-network

多级实例

一个虚拟机部署一个node应用, 另外一个虚拟机部署一个mysql, node链接mysql

如何科学使用 createContext、useReducer、useContext

如何科学使用 createContext、useReducer、useContext

如何使用createContext、useReducer、useContext组合

定义reducer

import { Action, ReducerActionType, ReducerInitDataState } from '@/pages/reducer/reducerData/interface';

export const ReducerInitData: ReducerInitDataState = {
  count: 0,
  name: '',
};

export const ReducerType: ReducerActionType = {
  updateCount: 'UPDATE_COUNT',
  updateName: 'UPDATE_NAME',
};

export const reducerFn = (state: ReducerInitDataState, action: Action) => {
  switch (action.type) {
    case ReducerType.updateCount:
      return {
        ...state,
        count: action.payload,
      };
    case ReducerType.updateName:
      return {
        ...state,
        name: action.payload,
      };
    default:
      return state;
  }
};

定义context

import { createContext } from 'react';
import { ReducerInitData } from '@/pages/reducer/reducerData';
import { noop } from 'lodash';

export const ReducerContext = createContext({ state: ReducerInitData, dispatch: noop });

使用实例

import React, { FC, memo, useCallback, useContext, useReducer } from 'react';
import { reducerFn, ReducerInitData, ReducerType } from '@/pages/reducer/reducerData';
import { ReducerContext } from '@/pages/reducer/consts';
import { Button } from 'antd';
import WrapperReducerHOC from '@/pages/reducer/hoc/WrapperReducerHOC';

const ChildTow = memo(() => {
  console.log('child tow');
  const { state } = useContext(ReducerContext);
  return (
    <div>
      child tow - {state.count}
    </div>
  );
});

const ChildTow2: FC<Partial<{ count: number }>> = memo(props => {
  const { count } = props;
  console.log('child tow 2');
  return (
    <div>
      child tow 2 - {count}
    </div>
  );
});

const ChildTow2WrapperComponent = WrapperReducerHOC(ChildTow2, () => {
  const { state } = useContext(ReducerContext);
  return {
    count: state.count,
  };
});

const ChildOne = memo(() => {
  console.log('child one');
  return (
    <>
      <ChildTow />
      <ChildTow2 />
      <ChildTow2WrapperComponent />
    </>
  );
});

// reducer 主体组件
const Reducer: FC = () => {
  const [state, dispatch] = useReducer(reducerFn, ReducerInitData);

  // 观察是否渲染
  console.log('reducer');

  // 更新 count
  const handleClickUpdateCount = useCallback(() => {
    dispatch({
      type: ReducerType.updateCount,
      payload: 5,
    });
  }, []);

  // 更新 name
  const handleClickUpdateName = useCallback(() => {
    dispatch({
      type: ReducerType.updateName,
      payload: 'YanLe',
    });
  }, []);

  return (
    <ReducerContext.Provider value={{ state, dispatch }}>
      <div>
        <Button onClick={handleClickUpdateCount}>click - update count</Button>
        <Button onClick={handleClickUpdateName}>click - update name</Button>
        <ChildOne />
      </div>
    </ReducerContext.Provider>
  );
};

export default Reducer;

总结一下

  1. 总所周知, createContext 有一个严重的弊端, 就是如果 provider value 的内容发生改变,那么其挂载的所有组件以及子组件都要重新渲染。
    如果用 reducer 内容挂在在provider上面, 对性能来说无疑是一个灾难。

  2. 在子组件添加 memo 的话 可以一定程度上放置重复渲染问题出现。一定程度上是指:你使用到了 reducer 那么他还是会渲染, 如果没有使用, 就不会渲染。

  3. 如果希望进一步优化组件渲染, 可以添加一个高阶组件, 这个作用就是, 在一个函数里面, 把 useContext 返回的值, 作为参数传递给子组件。
    这样在子组件再做一层 memo 就可以用参数来判定决定渲染与否。

import React, { FC } from 'react';

export const WrapperReducerHOC = <P extends any, T extends any>(WrapperComponent: FC<P>, fn: Function) => (props: T) => {
  const state = fn(props);
  return <WrapperComponent {...state} {...props} />;
};

export default WrapperReducerHOC;
  1. 就算是按照 3 的方式做了, 其实还是有一定的问题,那就是:只能取组件需要用到的值,如果取多了, 依然还是会渲染的。
    举栗子就是,我在 ChildTow2 组件中, 只使用到了 count 数据, 如果我在上述高阶组件中, fn 返回的值中有 name 值,
    那么 ChildTow2 还是会渲染。

  2. createContext 最佳实践,个人认为是传递常量,而不是传递变量。

[设计模式] 05-技巧型设计模式

第五篇、技巧型设计模式

目录

第二十七章-链式模式

描述:
通过在对象方法中将对象返回, 实现对同一个对象多个方法的链式调用。简化对该对象的多个方法的多次调用时,对该对象的多次引用。
最典型的使用就是jQuery

简单的来说, 这种设计模式就是封装一个对象, 然后对象方法处理数据, 数据就放在对象的属性上面, 然后方法返回this指针, 就可以了。

来看一个实际使用的一个实例:

/*
* sum(1).sum(1,2).sum(3,4).sum(2).sum(2,3,4,5,6,7,8).result()  // 48
* */
class Test {
    constructor() {
        this.resultNumber = 0;
    }
    sum() {
        let args = arguments,
            len = args.length;
        if (len > 0) {
            for (let num of args) {
                this.resultNumber += num;
            }
        }
        return this;
    }
    result() {
        return this.resultNumber;
    }
}
let test = new Test();

let resultNumber = test.sum(1).sum(1, 2).sum(3, 4).sum(2).sum(2, 3, 4, 5, 6, 7, 8).result();
console.log(resultNumber);     // 48

其他的示例:
01、原型式继承存在的问题
02、对于类jQuery链式调用模式的研究
03、链式模式的一个实际使用实例

第二十八章-委托模式

描述:

多个对象接受并处理同一个请求, 他们请求委托给另一个对象统一处理请求。
这种设计模式是只针对于浏览器端的来使用的, 至于node没有dom, 所以没有这个特性。

实际场景

第一个场景
日历模块, 用户点击每个日期格子的时候将格子的背景色变为灰色, 大多数的做法就是把每个日期格子都绑定一个事件, 做法如下:

let ul = document.getElementById('container');
let li = ul.getElementById('li'),
    len = li.length - 1;
for (; len >= 0; len--) {
    li[i].onclick = function () {
        this.style.backgroundColor = 'gery';
    }
}

委托模式实际上就是讲事件委托给更高层面上的肤元素去绑定执行。

let ul = document.getElementById('container');
ul.onclick = function (e = window.event) {
    let tar = e.target || e.srcElement;
    if (tar.nodeName.toLowerCase() === 'li') {
        tar.style.color = 'blue';
    }
}

第二个场景
还有一种使用场景: 就是比如我们的dom 是动态添加的, 我们可以把它触发的时间, 暂时委托给父级。 这样的jquery中非常的常见。

$(document).on('click', '#target', function() {
    // 处理逻辑
})

第三个场景
委托模式可以解决一些内存泄漏的问题。

<div id="container">
    <button id="btn">dom</button>
</div>

<script>
/*
* 下面一段代码中, g变量中保存了元素绑定的click事件没有清楚, 这个时间就会泄露到内存中去
* 失去了对其的控制
* */
let g = function (id) {
    return document.getElementById(id);
};
g('btn').onclick = function () {
    g('container').innerHTML = '触发了事件'
};

/*
* 利用委托模式解决上面所面临的问题
* */
g('container').onclick = function (e = window.event.srcElement) {
    let target = e && e.target;
    if(target.id === 'btn') {
        g('container').innerHTML = '触发了事件'
    }
}
</script>

代码示例

01、实际场景-点击日期格子变色
02、实际场景-处理内存泄漏问题

第二十九章-数据访问对象模式

描述:

抽象和封装对数据源的访问与存储。

实际场景

场景一: 本地存储 localStorage

class BaseLocalStorage {
    constructor(preId, timeSign = '|-|') {
        this.preId = preId;
        this.timeSing = timeSign;
        this.status = {
            SUCCESS: 0,         // 成功
            FAILURE: 1,         // 失败
            OVERFLOW: 2,        // 溢出
            TIMEOUT: 3,         // 过期
        };
        this.storage = localStorage || window.localStorage;
    }

    // 获取本地存储数据库数据真实字段
    getKey(key) {
        return this.preId + key;
    }

    /**
     * 添加或者修改数据
     * @param key           数据字段标识
     * @param value         数据值
     * @param callback      回到函数
     * @param time          添加时间
     */
    set(key, value, callback, time) {
        // 默认操作状态是成功的
        let status = this.status.SUCCESS,
            getKey = this.getKey(key);
        try {
            time = +new Date(time) || time.getTime();
        } catch (e) {
            // 传入的时间参数或者时间参数有无获取默认时间, 一个月
            time = +new Date() + 1000 * 60 * 60 * 24 * 31;
        }

        try {
            this.storage.setItem(getKey, time + this.timeSing + value);
        } catch (e) {
            // 溢出失败, 返回溢出状态
            status = this.status.OVERFLOW;
        }
        callback && callback.call(this, status, getKey, value);
    }

    /**
     * 获取数据
     * @param key           数据字段标识
     * @param callback      回调函数
     * @returns {*}
     */
    get(key, callback) {
        let status = this.status.SUCCESS,
            getKey = this.getKey(key),
            value = null,                   // 默认值为空
            timeSignLen = this.timeSing.length,         // 时间戳与存储数据之间的拼接长度
            index,                  // 时间戳与春出数据之间的拼接符其实位置
            time,                   // 时间戳
            result;                 // 最终获取到的数据
        try {
            value = this.storage.getItem(getKey);
        } catch (e) {
            result = {
                status: this.status.FAILURE,
                value: null
            };
            callback && callback.call(this, result.status, result, value);
            return result;
        }

        // 获取成功
        if (value) {
            index = value.indexOf(this.timeSing);
            time = +value.slice(0, index);          // 获取时间戳
            if (+new Date(time) > +new Date() || time === 0) {
                // 获取数据结果(拼接后面的字符串)
                value = value.slice(index + timeSignLen);
            } else {
                // 获取则结果为null
                value = null;
                status = this.status.TIMEOUT;
                time.remove(key);
            }
        } else {
            status = this.status.FAILURE;
        }

        // 设置结果
        result = {
            status: status,
            value: value
        };
        callback && callback.call(this, result.status, result.value);
        return result;
    }

    /**
     * 删除数据
     * @param key           数据字段标识
     * @param callback      回调函数
     */
    remove(key, callback) {
        let status = this.status.FAILURE,
            getKey = this.getKey(key),
            value = null;
        value = this.storage.getItem(getKey);
        if (value) {
            // 删除数据
            this.storage.removeItem(getKey);
            status = this.status.SUCCESS;
        }
        callback && callback.call(this, status, status > 0 ? null : value.slice(value.indexOf(this.timeSing) + this.timeSing.length))
    }
}


/*使用实例*/
let LS = new BaseLocalStorage('LS__');
LS.set('a', 'xiao ming', function () {
    console.log(arguments)
});
LS.get('a', function () {
    console.log(arguments)
});
LS.remove('a', function () {
    console.log(arguments)
});
LS.remove('a', function () {
    console.log(arguments)
});
LS.get('a', function () {
    console.log(arguments)
});

场景二: 对于mongodb 的使用情况

第三十章-节流模式

描述

对于复杂的业务逻辑进行节流控制, 执行最后一次操作并取消其他操作, 提高性能。

实际场景

场景一: 返回顶部
返回顶部按钮添加动画, 每次拖动页面滚动时, 他都要不停的抖动。 原因是拖动页面滚动条件是, 不停的出发了scroll 时间, 所以返回东部按钮不听的执行动画。

let throttle = function () {
    let isClear = arguments[0], fn;
    // 第一个参数表示是否清楚计时器
    if (typeof isClear === 'boolean') {
        // 第二个参数则为函数
        fn = arguments[1];
        fn.__trottleID && clearTimeout(fn.__trottleID);
    } else {
        // 第一个参数为函数
        fn = isClear;
        // 第二个参数为函数执行时候的参数
        let param = arguments[1];
        let p = Object.assign({
            context: null,
            args: [],
            time: 300
        }, param);
        arguments.callee(true, fn);
        fn.__trottleID = setTimeout(function () {
            fn.apply(p.context, p.args)
        }, p.time)
    }
};

// 实际使用
function moveScroll() {
    let top = $(document).scrollTo();
    $('#back').animate({top: top + 30}, 400, 'easeOutCubic')
}
// 监听页面滚动事件
$(window).on('scroll', function () {
    throttle(moveScroll);
})

场景二: 优化浮层

场景三: 图片的延迟加载

场景四: 统计打包

第三十一章-简单模板模式

描述:

很简单, 就是类似于一个模板引擎而已, 更多的时候, 可以多考虑一下, 一个模板引擎的设计模式;

略。。。。。。。

第三十二章-惰性模式

描述

减少每次代码执行的重复性的分支判断, 通过对对象重定义来拼比原对象中的分支判断。

实际场景

场景一:
解决函数执行时候的重复性的分支判断。

A.on3 = function (dom, type, fn) {
    if (document.addEventListener) {
        A.on3 = function (dom, type, fn) {
            dom.addEventListener(type, fn, false);
        }
    } else if (document.attachEvent) {
        A.on3 = function (dom, type, fn) {
            dom.attachEvent('on' + type, fn);
        }
    } else {
        A.on3 = function (dom, type, fn) {
            dom['on' + type] = fn;
        }
    }
    A.on3(dom, type, fn);
};

代码示例: 场景1-解决重复性的分支判断

场景二:
创建XHR场景

let createXHR2 = function () {
    if (typeof XMLHttpRequest !== 'undefined') {
        createXHR2 = function () {
            return new XMLHttpRequest();
        }
    } else if (typeof ActiveXObject !== 'undefined') {
        createXHR2 =  function () {
            if (typeof arguments.callee.activeXString !== 'string') {
                let versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'],
                    i = 0,
                    len = versions.length;
                for (; i < len; i++) {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                }
            }
        }
    } else {
        createXHR2 = function () {
            throw new Error('您的浏览器不支持Ajax.');
        }
    }
    return createXHR2;
}

第三十三章-参与者模式

描述

在特定的作用于中执行给定的函数, 并将参数原封不动的传递。

实际场景

场景一:
天气模块, 打开页面后定时从后端拉去数据, 缓存下来, 一旦用户点击查看天气按钮, 就展开天气模块, 并现实天气信息。
这个场景比价复杂, 可以一步一步看示例代码: 场景1

第三十四章-等待着模式

描述

通过对多个异步进程的监听, 来触发未来发生的动作。

实际场景

场景1:
接口拆分, 以前的新闻搜索接口拆分为新闻搜索接口和新闻推荐结果接口。
解决这个情况的急要用到等待这模式, 监听两个异步请求的结果, 然后根据之前的分发逻辑执行就可以了。
等待着模式就是解决那些不确定先后完成的异步逻辑。
监听的是所有的异步逻辑完成, 这样才会自动执行成功回调函数, 如果有一个异步函数执行失败了,那么就执行失败回调函数。
这点儿很类似于promise.all方法
场景1-等待对象
场景2-封装一个异步请求

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.