Git Product home page Git Product logo

blogs's Introduction

Hi I'm Ly~ 👋

🌱 Learning and recording it~~

blogs's People

Contributors

lydever avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

lydev01

blogs's Issues

【TypeScript】TypeScript之Record的基本用法

Record<Keys,Type>

构造一个对象类型,其属性key是Keys,属性value是Type。被用于映射一个类型的属性到另一个类型。

简单来说,TypeScript中的Record可以实现定义一个对象的 key 和 value 类型,Record 后面的泛型就是对象键和值的类型。

实例

比如我需要一个cats对象,这个对象里有三个不同的属性,且属性的值必须是数字和字符串
那么可以这样写:

interface CatInfo {
  age: number;
  breed: string;
}
 
type CatName = "mincat" | "licat" | "mordred";
 
const cats: Record<CatName, CatInfo> = {
  mincat: { age: 10, breed: "小猫er" },
  licat: { age: 5, breed: "李猫er" },
  mordred: { age: 16, breed: "无名猫er" },
};
 
cats.licat;
 
const cats: Record<CatName, CatInfo>

学习文档:https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeystype

【前端开发】谈谈H5中的原生元素拖拽(drap& drop)事件

在原生h5 中,拖放(drag 和 drop)是 HTML5 标准的组成部分。它常用于:抓取对象以后拖放到另一个位置。

一个元素的拖放的过程:

选中-->拖动-->释放

选中

  • 在HTML5标准中, draggable是一个全局的枚举属性,它决定了一个元素是否可以被拖动。

draggable 的语法:

<element draggable="true | false | auto" >
  • true: 可以拖动;

  • false:禁止拖动

  • auto:跟随浏览器定义元素是否可以拖动

  • 在web页面中,默认只有text selection,images,links(选中文本、图片、链接)可以被拖动,当一个image或link被拖动时,image或link的url会被设置到drag data中。对于其他元素,必须是selection的一部分才能被拖动。要想所有的元素都能被拖动,需要做三件事情:

  • 1、设置draggable=“true”到元素上。

  • 2、添加dragstart事件监听。

  • 3、在dragstart事件中设置drag data.(通过dataTransfer对象实现)。

常用事件

针对对象 事件名 on系列事件名 说明
被拖动的元素 drag ondrag 元素正在被拖动的时候触发
dragend ondragend 拖动事件结束时触发(松开鼠标,或者按下escape键)
dragstart ondragstart 当用户开始拖动元素或者文本selection时触发
dragexit ondragexit 当一个元素不在是drag的选区目标时触发。
目的地对象 dragleave ondragleave 当拖动元素或seleciton离开有效的drop元素时触发
dragover ondragover 当拖动一个元素或seletion通过一个有效的drop元素时触发
drop ondrop 当元素或者文本selection在有效的drop元素中松开鼠标时触发
dragenter ondragenter 当拖动一个元素或者selection进入到一个有效的drop元素中时触发

有了这些drap&drop api事件,我们通过dataTransfer对象设置drag过程中回调函数来处理一些业务逻辑。

注意:dragenter和dragover事件的默认行为是拒绝任何被拖放的元素,因此,我们需要阻止浏览器这种默认行为:ev.preventDefault();

DataTransfer 对象

在drag&drop拖放操作的过程中会触发一个DragEvent对象,属于Dom event的一个子对象,这个对象有一个dataTransfer属性:该属性用于保存拖放的数据和交互信息,返回DataTransfer对象,只读,但其子属性可设置。

dataTransfer的属性:

属性 说明
DataTransfer.dropEffect 设置或获取当前的拖动操作的属性,值必须是下列之一:none,copy,link,move。
DataTransfer.effectAllowed 提供所有可能的操作效果类型,必须是以下的值:copyLink,copyMove,link,linkMove,all,uninitialized.这个属性应该在dragstart中设置,提供dropEffect使用。
DataTransfer.files 包含一个dataTransfer中的本地文件的文件列表,如果dragging操作中不设计文件,此属性是一个空列表。
DataTransfer.types 返回值dragstart事件中设置的拖动类型的字符串数组。

dataTransfer的方法:

方法 说明
DataTransfer.clearData([format]) 清空给定类型的数据,如果没有传入type,则清空所有数据。如果dataTransfer中没有数据,或者没有对应类型的数据,此方法没有效果。
DataTransfer.getData([format]) 返回给定类型的数据,如果dataTransfer没有此类型的数据,则返回空字符串。
DataTransfer.setData(format,data) 设置给定类型的数据,如果此类型数据不存在,就会添加到最后一列,如果此类型数据已经存在,则会替换已经存在的数据。
DataTransfer.setDragImage(img,xoffset,yoffset) 当拖动时会从拖动源创建一个半透明的图片,这个图片是自动创建的。如果需要自定义设置,需要使用这个方法,xoffset,yoffset,是图像距离鼠标指针的位置,此方法通常在dragstart事件中调用。

【JavaScript】详解Object.prototype.hasOwnProperty()

Object.prototype.hasOwnProperty()

hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。用通俗的话来说就是:用来判断一个属性是定义在对象本身而不是继承自原型链的。

语法

obj.hasOwnProperty(prop)
  • prop
    要检测的属性的 String 字符串形式表示的名称,或者 Symbol。
  • 返回值
    用来判断某个对象是否含有指定的属性的布尔值 Boolean。

实例

const obj = {};
obj.property1 = 88;

console.log(obj.hasOwnProperty('property1'));  // true

console.log(obj.hasOwnProperty('toString'));  // false

console.log(obj.hasOwnProperty('hasOwnProperty')); // false

注意: 即使属性的值是 null undefined,只要属性存在,hasOwnProperty 依旧会返回 true

o = new Object();
o.lisi = null;
o.hasOwnProperty('lisi'); // 返回 true
o.lizi = undefined;
o.hasOwnProperty('lizi'); // 返回 true

使用 hasOwnProperty 方法判断属性是否存在
下面的例子检测了对象 o 是否含有自身属性 prop:

o = new Object();
o.hasOwnProperty('prop'); // 返回 false
o.prop = 'exists';
o.hasOwnProperty('prop'); // 返回 true
delete o.prop;
o.hasOwnProperty('prop'); // 返回 false

自身属性与继承属性
下面的例子演示了 hasOwnProperty 方法对待自身属性和继承属性的区别:

o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop');             // 返回 true
o.hasOwnProperty('toString');         // 返回 false
o.hasOwnProperty('hasOwnProperty');   // 返回 false

使用 hasOwnProperty 作为属性名
JavaScript 并没有保护 hasOwnProperty 这个属性名,所以就会出现设置hasOwnProperty为函数名的情况:

var foo = {
  hasOwnProperty: function() {
    return false;
  },
  bar: 'Here be dragons'
};

那么,在使用:

foo.hasOwnProperty('bar'); // 始终返回 false

为解决这种情况,可以使用下面这两种方式:

// 1. 可以直接使用原型链上真正的 hasOwnProperty 方法
({}).hasOwnProperty.call(foo, 'bar'); // true

// 2. 使用 Object 原型上的 hasOwnProperty 属性
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true

==注意==,只有在最后一种情况下,才不会新建任何对象。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty

【Node后端】玩玩Express 框架

Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你创建各种 Web 应用和丰富的 HTTP 工具,如果你不会jJava or Python等后端,使用 Express可以帮助我们快速地搭建一个完整功能的网站。

其中Express 框架核心特性:

  • 可以设置中间件来响应 HTTP 请求。
  • 定义了路由表用于执行不同的 HTTP 请求动作。
  • 可以通过向模板传递参数来动态渲染 HTML 页面。

Express安装

express的使用过程有两种方式:

  • 方式一:通过express提供的脚手架,直接创建一个应用的骨架;
  • 方式二:从零搭建自己的express应用结构;

方式一:安装express-generator

npm install -g express-generator

创建项目

express express-app

项目目录如下

├── app.js
├── bin
   └── www
├── package-lock.json
├── package.json
├── public
   ├── images
   ├── javascripts
   └── stylesheets
       └── style.css
├── routes
   ├── index.js
   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

之后 cd 进入到对应的目录下,然后将项目跑起来:

npm install 
node bin/www
node app.js   

方式二: 自己搭建环境

上面通过express提供的脚手架,直接创建一个应用的骨架;现在我们自己从零搭建项目:
初始化一个新的项目:

npm init -y

安装express:

npm install express

新建app.js
在这里插入图片描述

express 初体验

现在搭建自己的第一个express程序:在app.js中加入如下代码

const express = require('express');

// 创建服务器
const app = express();

app.get('/',(req,res) => {
  res.end("Hello World");
});

app.listen(8000,() => {
  console.log("服务器启动成功~");
})

进入项目根目录下,在终端中将服务器跑起来:

node app.js

到浏览器:访问localhost:8000即可

请添加图片描述

请求和响应

请求的路径中如果有一些参数,可以这样表达:

/users/:userId
request对象中要获取可以通过 req.params.userId;

返回数据

返回数据,我们可以方便的使用json
res.json(数据)方式

const express = require('express');

const app = express();

app.get('/users/:userId', (req, res, next) => {
  console.log(req.params.userId);
  res.json({username: "liyingxia", password: "8i8i8i8i" });
});

app.listen(8000, () => {
  console.log("静态服务器启动成功~");
})

Express 中间件

认识中间件

Express是一个路由和中间件的Web框架,它本身的功能非常少:
Express应用程序本质上是一系列中间件函数的调用;

中间是什么?

  • 中间件的本质就是一个回调函数;
  • 这个回调函数接受三个参数:
  • 请求对象(request对象);
  • 响应对象(response对象);
  • next函数(在express中定义的用于执行下一个中间件的函数);

中间件中可以执行哪些任务?

  • 执行任何代码;
  • 更改请求(request)和响应(response)对象;
  • 结束请求-响应周期(返回数据);
  • 调用栈中的下一个中间件;

如果当前中间件功能没有结束请求-响应周期,则必须调用 next()将控制权传递给下一个中间件功能,否则,请求将被挂起。

使用中间件

express主要提供了两种方式:app/router.useapp/router.methods这两种方式把中间件应用到我们的应用程序中;

methods指的是常用的请求方式,比如:app.getapp.post

// express 中间件的使用
const express = require('express');
const res = require('express/lib/response');
const app = express();

app.use((req,res,next) => {
  console.log("middleware");
  next();
});

app.use((req,res,next) => {
  console.log("middleware");
  res.end("Hello Common Middleware");
})

app.listen(9000,()=>{
  console.log("中间件服务器启动成功~")
})

path匹配中间件:

//path 路径匹配中间件
app.use('/home',(req,res,next) => {
  console.log("home middleware 中间件");
  next();
});

app.use('/home',(req,res,next) => {
  console.log("home middleware02");
  next();
  res.end("middleware");
});

app.use((req,res,next) =>{
   console.log("middleware");
})

path 和 method 匹配中间件

// path 和 method 匹配中间件
app.get('/home',(req,res,next) => {
  console.log("home get middleware");
  next();
})

app.post('/login',(req,res,next) => {
  console.log("login post middleware");
  next();
});

app.use((req,res,next) => {
  console.log("common middleware");
})

app.use(express.json());
app.use(express.urlencoded({extended:true}));

app.post('/login',(req,res,next) => {
  console.log(req.body);
  res.end("登陆成功~");
});

日志记录中间件

如果我们希望将请求日志记录下来,那么可以使用express官网开发的第三方库:morgan
morgan安装:

npm install morgan

如何用?直接作为中间件使用即可:

const loggerWriter = fs.createWriteStream('./log/access.log', {
  flags: 'a+'
})
app.use(morgan('combined', {stream: loggerWriter}));

上传文件中间件

图片上传我们可以使用express官方开发的第三方库:multer
multer安装:

npm install multer

上传文件:

const upload = multer({
  dest: "uploads/"
})

app.post('/upload', upload.single('file'), (req, res, next) => {
  console.log(req.file.buffer);
  res.end("文件上传成功~");
})

添加上传文件后缀名:

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/")
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname));
  }
})

const upload = multer({
  storage
})

app.post('/upload', upload.single('file'), (req, res, next) => {
  console.log(req.file.buffer);
  res.end("文件上传成功~");
})

上传多张图片:

app.use('/upload', upload.array('files'), (req, res, next) => {
  console.log(req.files);
});

请求和响应

客户端传递到服务器参数的方法常见的是5种:
方式一:通过get请求中的URL的params;
方式二:通过get请求中的URL的query;
方式三:通过post请求中的body的json格式;
方式四:通过post请求中的body的x-www-form-urlencoded格式;
方式五:通过post请求中的form-data格式;

请求解析

方式一:params
请求地址: http://locahost:8000/login/asd/ass
获取参数:

app.use('/login/:id/:name', (req, res, next) => {
  console.log(req.params);
  res.json("请求成功~");
})

方式二:query
请求地址:http://localhost:8000/login?username=liyingxia&password=123456
获取参数:

app.use('/login', (req, res, next) => {
  console.log(req.query);

  res.json("请求成功~");
})

方式三:通过post请求中的body的json格式;
在客户端发送post请求时,会将数据放到body中:客户端可以通过json的方式传递,也可以通过form表单的方式传递;
自己编写中间件来解析JSON:

app.use((req, res, next) => {
  if (req.headers['content-type'] === 'application/json') {
    req.on('data', (data) => {
      const userInfo = JSON.parse(data.toString());
      req.body = userInfo;
    })
    req.on('end', () => {
      next();
    })
  } else {
    next();
  }
})

app.post('/login', (req, res, next) => {
  console.log(req.body);
  res.end("登录成功~");
});

适用express内置的中间件或者使用body-parser来完成:

app.use(express.json());

app.post('/login', (req, res, next) => {
  console.log(req.body);
  res.end("登录成功~");
});

方式四:通过post请求中的body的x-www-form-urlencoded格式;
在这里插入图片描述

解析application/x-www-form-urlencoded:
可以使用express自带的 urlencoded函数来作为中间件:

传入的extended用于表示使用哪一种解析方式:

  • true:使用qs第三方模块;
  • false:使用querystring内置模块;
app.use(express.json());
app.use(express.urlencoded({extended: true}));

app.post('/login', (req, res, next) => {
  console.log(req.body);
  res.end("登录成功~");
});

方式五:通过post请求中的form-data格式;
通过any借助multer去解析一些form-data中的普通数据:
在这里插入图片描述

app.use(upload.any());

app.use('/login', (req, res, next) => {
  console.log(req.body);
});

响应方式

  • end():类似于http中的response.end方法;
res.end("获取成功~")
  • json():json方法中可以传入很多的类型:object、array、string、boolean、number、null等,都会被转换成json格式返回
res.json({name:"liyignxia",password:"123456"});
  • status(): 设置状态码
res.status(200);

路由的使用

使用 express.Router来创建一个路由处理程序:一个Router实例拥有完整的中间件和路由系统;

// 用户相关的处理
const userRouter = express.Router();

userRouter.get('/', (req, res, next) => {
  res.end("用户列表");
});

userRouter.post('/', (req, res, next) => {
  res.end("创建用户");
});

userRouter.delete('/', (req, res, next) => {
  res.end("删除用户");
});

app.use('/users', userRouter);

静态资源服务器

Node也可以作为静态资源服务器,并且express给我们提供了方便部署静态资源的方法;

const express = require('express');

const app = express();

app.use(express.static('./build'));

app.listen(8000, () => {
  console.log("静态服务器启动成功~");
})

错误处理方式

app.use((req, res, next) => {
  next(new Error("USER DOES NOT EXISTS"));
});

app.use((err, req, res, next) => {
  const message = err.message;

  switch (message) {
    case "USER DOES NOT EXISTS":
      res.status(400).json({message})
  }

  res.status(500)
})

【数据结构与算法】基础知识(一)

msg:当初在数据结构课上被c语言的数据结构虐死,难。。。要想提高技术,躲不过的,还是得学造。

@TOC

1、常见的数据结构:

  • 栈(Stack):栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据结点的插入和删除操作。
  • 队列(Queue):队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。
  • 数组(Array):数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。
  • 链表(Linked List):链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。
  • 树(Tree):树是典型的非线性结构,它是包括,2 个结点的有穷集合 K。
  • 图(Graph):图是另一种非线性数据结构。在图结构中,数据结点一般称为顶点,而边是顶点的有序偶对。
  • 堆(Heap):堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。
  • 散列表(Hash table):散列表源自于散列函数(Hash function),其**是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。

2、常用算法

数据结构研究的内容:就是如何按一定的逻辑结构,把数据组织起来,并选择适当的存储表示方法把逻辑结构组织好的数据存储到计算机的存储器里。算法研究的目的是为了更有效的处理数据,提高数据运算效率。数据的运算是定义在数据的逻辑结构上,但运算的具体实现要在存储结构上进行。一般有以下几种常用运算:

  • 检索:检索就是在数据结构里查找满足一定条件的节点。一般是给定一个某字段的值,找具有该字段值的节点。
  • 插入:往数据结构中增加新的节点。
  • 删除:把指定的结点从数据结构中去掉。
  • 更新:改变指定节点的一个或多个字段的值。
  • 排序:把节点按某种指定的顺序重新排列。例如递增或递减。

链表

1. 反转链表:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

&循环解决方案

这道题是链表中的经典题目,充分体现链表这种数据结构 操作思路简单 , 但是 实现上 并没有那么简单的特点。

那在实现上应该注意一些什么问题呢?
保存后续节点。作为新手来说,很容易将当前节点的 next 指针直接指向前一个节点,但其实当前节点下一个节点 的指针也就丢失了。因此,需要在遍历的过程当中,先将下一个节点保存,然后再操作 next指向。
链表结构声定义如下:

function ListNode(val) {
this.val = val;
this.next = null;
}

实现如下:

/**
* @param {ListNode} head
* @return {ListNode}
*/
let reverseList = (head) => {
if (!head)
return null;
let pre = null, cur = head;
while (cur) {
// 关键: 保存下一个节点的值
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
};

递归解决方案

let reverseList = (head) =>{
let reverse = (pre, cur) => {
if(!cur) return pre;
// 保存 next 节点
let next = cur.next;
cur.next = pre;
return reverse(cur, next);
}
return reverse(null, head);
}

2. 区间反转

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明: 1 ≤ m ≤ n ≤ 链表长度。
示例:

输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL

思路:
这一题相比上一个整个链表反转的题,其实是换汤不换药。我们依然有两种类型的解法:循环解法和递归解法。
需要注意的问题就是 前后节点 的保存(或者记录),什么意思呢?看这张图你就明白了。
在这里插入图片描述
关于前节点和后节点的定义,大家在图上应该能看的比较清楚了,后面会经常用到。反转操作上一题已经拆解过,这里不再赘述。值得注意的是反转后的工作,那么对于整个区间反转后的工作,其实就是一个移花接木的过程,首先将**前节点的 next 指向区间终点,然后将区间起点的 next 指向后节点**。因此这一题中有四个需要重视的节点: 前节点 后节点 区间起点区间终点 。接下来我们开始实际的编码操作。

循环解法:

/**
* @param {ListNode} head
* @param {number} m
* @param {number} n递归解法
对于递归解法,唯一的不同就在于对于区间的处理,采用递归程序进行处理,大家也可以趁着复习一下
递归反转的实现。
* @return {ListNode}
*/
var reverseBetween = function(head, m, n) {
let count = n - m;
let p = dummyHead = new ListNode();
let pre, cur, start, tail;
p.next = head;
for(let i = 0; i < m - 1; i ++) {
p = p.next;
}
// 保存前节点
front = p;
// 同时保存区间首节点
pre = tail = p.next;
cur = pre.next;
// 区间反转
for(let i = 0; i < count; i++) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 前节点的 next 指向区间末尾
front.next = pre;
// 区间首节点的 next 指向后节点(循环完后的cur就是区间后面第一个节点,即后节点)
tail.next = cur;
return dummyHead.next;
};

递归解法

对于递归解法,唯一的不同就在于对于区间的处理,采用递归程序进行处理,大家也可以趁着复习一下递归反转的实现。

var reverseBetween = function(head, m, n) {
// 递归反转函数
let reverse = (pre, cur) => {
if(!cur) return pre;
// 保存 next 节点
let next = cur.next;
cur.next = pre;
return reverse(cur, next);
}
let p = dummyHead = new ListNode();
dummyHead.next = head;
let start, end; //区间首尾节点
let front, tail; //前节点和后节点
for(let i = 0; i < m - 1; i++) {
p = p.next;
}
front = p; //保存前节点
start = front.next;
for(let i = m - 1; i < n; i++) {
p = p.next;
}3. 两个一组翻转链表
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
思路
如图所示,我们首先建立一个虚拟头节点(dummyHead),辅助我们分析。
首先让 p 处在 dummyHead 的位置,记录下 p.next  p.next.next 的节点,也就是 node1 
node2。
随后让 node1.next = node2.next, 效果:
然后让 node2.next = node1, 效果:
最后,dummyHead.next = node2,本次翻转完成。同时 p 指针指向node1, 效果如下:
end = p;
tail = end.next; //保存后节点
end.next = null;
// 开始穿针引线啦,前节点指向区间首,区间首指向后节点
front.next = reverse(null, start);
start.next = tail;
return dummyHead.next;
}

3. 两个一组翻转链表

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:

给定 1->2->3->4, 你应该返回 2->1->4->3.

思路
如图所示,我们首先建立一个虚拟头节点(dummyHead),辅助我们分析。
在这里插入图片描述
首先让 p 处在 dummyHead 的位置,记录下 p.next p.next.next 的节点,也就是 node1
node2

随后让 node1.next = node2.next, 效果:
在这里插入图片描述

然后让node2.next = node1,效果:
在这里插入图片描述

最后,dummyHead.next = node2,本次翻转完成。同时 p 指针指向node1, 效果如下:
在这里插入图片描述
依此循环,如果 p.next 或者 p.next.next 为空,也就是 找不到新的一组节点 了,循环结束。

循环解决:

var swapPairs = function(head) {
if(head == null || head.next == null)
return head;
let dummyHead = p = new ListNode();
let node1, node2;
dummyHead.next = head;
while((node1 = p.next) && (node2 = p.next.next)) {
node1.next = node2.next;
node2.next = node1;
p.next = node2;
p = node1;
}
return dummyHead.next;
};

递归方式:

var swapPairs = function(head) {
if(head == null || head.next == null)
return head;
let node1 = head, node2 = head.next;
node1.next = swapPairs(node2.next);
node2.next = node1;
return node2;
};

4. K个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例 :

给定这个链表:1->2->3->4->5k = 2 时,应当返回: 2->1->4->3->5k = 3 时,应当返回: 3->2->1->4->5

说明 :

  • 你的算法只能使用常数的额外空间。
  • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

思路
思路类似No.3中的两个一组翻转。唯一的不同在于两个一组的情况下每一组只需要反转两个节点,而在K 个一组的情况下对应的操作是将 K 个元素 的链表进行反转。

递归解法:
以下代码的注释中 首节点 尾结点 等概念都是针对反转前的链表而言的。

/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var reverseKGroup = function(head, k) {
let pre = null, cur = head;
let p = head;
// 下面的循环用来检查后面的元素是否能组成一组
for(let i = 0; i < k; i++) {
if(p == null) return head;
p = p.next;
}
for(let i = 0; i < k; i++){
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// pre为本组最后一个节点,cur为下一组的起点
head.next = reverseKGroup(cur, k);
return pre;
};

循环解法

var reverseKGroup = function(head, k) {
let count = 0;
// 看是否能构成一组,同时统计链表元素个数
for(let p = head; p != null; p = p.next) {
if(p == null && i < k) return head;
count++;
  1. 如何检测链表形成环?
    给定一个链表,判断链表中是否形成环。

思路
思路一: 循环一遍,用 Set 数据结构保存节点,利用节点的内存地址来进行判重,如果同样的节点走过两
次,则表明已经形成了环。

思路二: 利用快慢指针,快指针一次走两步,慢指针一次走一步,如果 两者相遇 ,则表明已经形成了环。
可能你会纳闷,为什么思路二用两个指针在环中一定会相遇呢?

其实很简单,如果有环,两者一定同时走到环中,那么在环中,选慢指针为参考系,快指针每次 相对参
考系 向前走一步,终究会绕回原点,也就是回到慢指针的位置,从而让两者相遇。如果没有环,则两者
的相对距离越来越远,永远不会相遇。
接下来我们来编程实现。

方法一: Set 判重

let loopCount = Math.floor(count / k);
let p = dummyHead = new ListNode();
dummyHead.next = head;
// 分成了 loopCount 组,对每一个组进行反转
for(let i = 0; i < loopCount; i++) {
let pre = null, cur = p.next;
for(let j = 0; j < k; j++) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 当前 pre 为该组的尾结点,cur 为下一组首节点
let start = p.next;// start 是该组首节点
// 开始穿针引线!思路和2个一组的情况一模一样
p.next = pre;
start.next = cur;
p = start;
}
return dummyHead.next;
};

...

【TypeScript】TypeScript之Partial

Partial

Partial 可以快速把某个接口类型中定义的所有属性变成可选的。

举个栗子:

interface ApiKey {
  id: number;
  name: string;
}

const dataType1: ApiKey = {
  id: 1,
  name: 'static'
}

const dataType2: ApiKey = {
  name: 'json'
}

image.png

这段代码会在编译报错:

error TS2741: Property 'id' is missing in type '{ name: string; }' but required in type 'ApiKey'.Key'.

因为dataType2的类型是ApiKey,ApiKey中idname都是必选的,这导致编译报错。假如ApiKey中的参数是可选的,那么这个问题就会不复存在,而Partial的作用就在于此,它可以帮助我们把ApiKey中的所有属性都变成可选的。

我们永Partial来重写一下这个栗子:

interface ApiKey {
  id: number;
  name: string;
}

const dataType1: ApiKey = {
  id: 1,
  name: 'static'
}

const dataType2:  Partial<ApiKey> = {
  name: 'json'
}

这个时候在运行,就不会报错了。

【JavaScript权威指南(第七版)】之阅读学习总结

写在前面

image

最近借着空闲时间断断续续两个月看完了《JavaScript权威指南(第七版)》,《JavaScript权威指南》一直以来被称为“犀牛书”,前面的第六版我大概略过一遍,由于书的厚度实在有点“厚重”,将近1000多页左右,有一些知识已经过时了,看了一下就没有细啃,随后转向新出版的第七版,第七版是是在2020年5月出版上市的,据了解,而第6版是2011年出版的,距今已经10年了,前端的技术更迭换代非常“迅速”,至少对于像我这种之前是学习后端知识的人来说,前端的技术更迭比后端的技术更新快的多,而且要求也相对“苛刻”,这怎么说呢?比如Java都更新到了16 ,但是当前大部分的Java开发者还是停留在使用Java8的阶段,前端新技术更新了,但是你还不会用,就会有种说不过去的尴尬,比如Vite构建工具,再比如Vue3新特性,TypeScript 等。

扯些没用的废话,转回来。《JavaScript权威指南》第七版中最大的变化就是删除了过时的东西,增加了 ES6 新增的语法、新的 Web API、Node、流行工具库如 Babel 等内容。相比第六版,第七版相对“友好”多了,第6版中过时的内容都被删除了,比如 EX4、Rhino、JSONP、XMLHttpRequest、关于 IE 兼容性的讨论;第6版足足300页的语言参考和客户端参考在第7版中被删除掉了。第七版书本厚度大概600页不到左右,可谓轻薄了多好,让人阅读起来也舒服。

总体上

  • 前8章讲 JavaScript 的传统核心部分,与第6版基本相同。
  • 第9章至第14章讲 ES6 新语法。
  • 第15章至17章讲 JavaScript 主要的应用场景:浏览器和基于 Node 的服务端开发,最后涉及 JavaScript 生态和工程化,介绍了当前前端开发一些重要的流行类库。

书本内容目录

第1章,概述。

js概述,没什么好说的。

第2章,词法结构

与第6版基本相同。把对 unicode 转义的内容扩充为一个独立小节。

第3章,类型、值和变量

与第6版基本相同。增加了 Symbol 数据类型。

第4章,表达式和运算符

与第6版基本相同。增加了双引号(??)和 await 运算符。

第5章,语句

与第6版基本相同。增加了 yield, const, let, import, export 的内容。

第6章,对象

与第6版基本相同。增加了扩展运算符(...)的内容。

第7章,数组

与第6版基本相同。增加了 Array.from()、flat()、flatMap()、copyWithin() 的内容。

第8章,函数

与第6版基本相同。增加了箭头函数、参数缺省值、rest 参数的内容。

第9章,类

第6版的“第9章-类和模块”被拆成了2章分别讲解。
增加了 class 关键字及相关的内容。

第10章,模块。

在第6版时还没有内建的模块语法,所以在第6版第9章用一个小节讲到了模块。第7版进行了大幅扩充,分别讲解了 Node 下的模块和 ES6 的模块。

第11章,JavaScript 标准库

这一章是全新的,前面10章讲解的是 JavaScript 语言核心,这一章讲解语言集成的库和 API。内容包括 Set、Map、ArrayBuffer、正则匹配、日期时间类、Error 类、JSON 类、国际化 API、console API、URL API、计时器。第6版“第10章-正则表达式的模式匹配”的内容成为了本章的一个小节。

第12章,迭代器和生成器

这一章是全新的的。

第13章,异步 JavaScript

这一章是全新的。内容包括 callback 模式、Promise、async 和 await 等内容。

第14章,元编程

这一章是全新的,内容包括 Proxy、Reflect 对象。

第15章,浏览器中的JavaScript

介绍浏览器和js。

第16章,Node服务器端JavaScript

介绍基于 Node 的服务端开发。

第17章,JavaScript工具和扩展

当前前端开发一些重要的流行类库。比如perttier、ESlint、Babel转译器等。

【前端开发】与浏览器的那些点点滴滴(存储方式、缓存、请求方式、url请求处理过程、渲染步骤等)

@TOC

浏览器内核

浏览器内核主要分为两个部分:渲染引擎、js引擎;

  • 渲染引擎: 负责取得网页的内容(html css img ...),以及计算网页的显示方式,渲染成DOM 树,然后会输出至显示器或者打印机。浏览器的内核不同对于网页的语法解释也不同,所以渲染的效果也不一样;
  • js引擎: 解析和执行javascript,将javascript代码翻译成CPU指令来执行;
  • 主流浏览器内核:
    • IE: trident 内核;
    • Firefox: gecko 内核;
    • Safari:webkit 内核;
    • Opera:以前是presto内核,现在改用为Google-Chrome的Blink内核;
    • Chrome:Blibk内核,它是基于webkit,由Google和Opera Software共同开发;

浏览器存储方式

特性 cookie localSorage sessionStorage indexedDB
数据生命周期 一般由服务器生成,可以设置过期时间 除非被清除,否则会一直存在 页面关闭就被清理 除非被清理·,否则会一直存在
数据存储大小 4K 5M 5M 无限
与服务端通信 每次都会携带在header中,对于请求性能影响 不参与 不参与 不参与

cookies , sessionStorage 和 localStorage 的区别

  • cookie 是网站为了标示用户身份而储存在用户本地终端上的数据(通常经过加密)
  • cookie数据始终在同源的http请求中携带(即使不需要),记会在浏览器和服务器间来回传递(优化点)- sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存
  • 存储大小:
    • cookie 数据大小不能超过4k
    • sessionStorage 和 localStorage虽然也有存储大小的限制,但比 cookie 大得多,可以达到5M或更大
  • 有期时间:
    • localStorage 存储持久数据,浏览器关闭后数据不丢失 除非主动删除数据;
    • sessionStorage 数据在当前浏览器窗口关闭后自动删除;
    • cookie 设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭

localStorage 自带getItem(取数据)和setltem(存数据)来存取数据;

  • localStorage 只能存字符串,存取 JSON 数据需配合 JSON.stringify() 和 JSON.parse();遇上禁用 setItem 的浏览器,需要使用 try...catch 捕获异常
  • cookie 原本并不是用来储存的,而是用来与服务端通信的,需要存取需自行封装 api。

HTTP 的请求方式场景

  • Get:获取数据通常(查看数据)-查看;
  • POST: 向服务器提交数据通常(创建数据)-create;
  • PUT: 向服务器提交数据通常(更新数据)-update,与POST方法很像,也是提交数据,但PUT制定了资源在服务器上的位置,常用在修改数据;
  • HEAD:只请求页面的首页信息;
  • DELETE: 删除服务器上的资源;
  • OPTIONS: 用于获取当前URL 支持的请求方式;
  • TRACE: 用于激活一个远程的应用层请求消息回路;
  • CONNECT: 把请求链接转换到透明的TCP/IPd的通道;

HTTP 状态码

  • 1XX: 消息状态码
    • 100: continue 继续,一般在发送post请求时,已发送了http haeder 之后服务端将返回此信息,表示确认,之后发送具体参数信息;
  • 2XX: 成功状态码
    • 200:ok正常返回信息;
    • 201:created 请求成功并且服务端创建了新资源;
    • 202:accepted 服务器已经接收请求,但尚未处理;
  • 3XX:重定向
    • 301:move per请求的网页已经永久重定向;
    • 302:found临时重定向;
    • 303:see other临时冲重定向,且总是使用get请求新的url;
    • 304: not modified 自从上次请求后,请求的网页未修改过;
  • 4XX:客户端错误
    • 400:bad request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求;
    • 401:unauthorized 请求未授权;
    • 403:forbidden禁止访问;
    • 404:not found 找不到如何与url匹配的资源;
  • 5XX:服务器错误
    • 500:internal server error最常见的服务器端的错误;
    • 503:service unacailable服务器端暂时无法处理请求(可能是过载活维护)

简述浏览器处理过程:

一般我们从浏览器地址栏输入URL后,就等待浏览器给我们返回想要的东西,而这个过程中,浏览器进行了下面一系列操作:

  • 浏览器根据请求的URL 交给 DNS域名解析,找到真实 IP,向服务器发起请求;
  • 服务器交给后台处理完成后返回数据,浏览器接收文件( HTML、JS、CSS 、图象等);
  • 浏览器对加载到的资源( HTML、JS、CSS 等)进行语法解析,建立相应的内部数据结构(如 HTML 的 DOM );
  • 载入解析到的资源文件,渲染页面,完成。

详细描述浏览器的处理过程

在浏览器地址栏输入URL后:

  • 浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤:
    • 如果资源未缓存,发起新请求;
    • 如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
    • 检验新鲜通常有两个HTTP头进行控制 Expires 和 Cache-Control :
      • 2.3.1 HTTP1.0提供Expires,值为一个绝对时间表示缓存新鲜日期
      • 2.3.2 HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位的最大新鲜时间
  • 浏览器解析URL获取协议,主机,端口,path
  • 浏览器组装一个HTTP(GET)请求报文
  • 浏览器获取主机ip地址,过程如下:
    • 浏览器缓存
    • 本机缓存
    • hosts文件
    • 路由器缓存
    • ISP DNS缓存
    • DNS递归查询(可能存在负载均衡导致每次IP不一致)
  • 打开一个socket与目标IP地址,端口建立TCP链接,三次握手
    如下:
    • 客户端发送一个TCP的SYN=1,Seq=X的包到服务器端口
    • 服务器发回SYN=1,ACK=x+1,Seq=Y的相应包
    • 客户端发送ACK=Y+1,Seq=z
  • TCP链接建立后发送HTTP请求
  • 服务器接收请求后解析,将请求转发到服务器程序,如虚拟主机使用HTTP Host头部判断请求的服务程序
  • 服务器检测HTTP请求头是否包含缓存验证信息,如果验证缓存新鲜,返回304等对应状态
  • 出合理程序读取完整请求并准备HTTP相应,可能需要查询数据库等操作
  • 服务器将相应报文通过TCP链接发送回浏览器
  • 浏览器接收HTTP相应,然后根据情况选择关闭TCP链接或者保留重用,关闭TCP链接的四次握手如下:
    • 主动方发送Fin=1,ACK=z,Seq=x报文
    • 被动方发送ACK=X+1,Seq=Y报文
    • 被动方发送Fin=1,ACK=X,Seq=Y报文
    • 主动方发送ACK=Y,Seq=x报文
  • 浏览器检查相应状态码
  • 如果资源可缓存,进行缓存
  • 对相应进行解码
  • 根据资源类型决定如何处理
  • 解析HTML文档,构建DOM树,下载资源,构建CSSOM树,执行js脚本,这些操作每月严格的先后顺序
  • 构建DOM树:
    • Tokenizing:根据HTML规范将字符流解析为标记
    • Lexing:词法分析将标记转换为对象并定义属性和规则
    • DOM construction:根据HTML标记关系将对象组成DOM树
  • 解析过程中遇到图片、样式表、js文件,启动下载
  • 构建CSSOM树:
    • Tokenizing:字符流转换为标记流
    • Node:根据标记创建节点
    • CSSOM:节点创建CSSOM树
  • 根据DOM喝CSSOM树构建渲染树
    - 从DOM树的根节点遍历所有可见节点,不可见节点包括:1) script , meta这样本身不可见的标签。2)被css隐藏的节点,如 display: none
    - 对每一个可见节点,找到恰当的CSSOM规则并应用
    - 发布可视节点的内容和计算样式
  • js解析如下
    • 浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading
    • HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的文档内容
    • 当解析器遇到设置了async属性的script时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素
    • 当文档完成解析,document.readState变成interactive
    • 所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用document.write()
    • 浏览器在Document对象上触发DOMContentLoaded事件
    • 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState变为complete,window触发load事件
  • 显示页面(HTML解析过程中会逐步显示页面)

Cookie的优缺点

  • 优点: 极高的扩展性和可用性
  1. 数据持久性;
  2. 不需要任何服务器资源。 Cookie 存储在客户端并在发送后由服务器读取;
  3. 可配置到期规则。 控制 cookie 的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的 cookie;
  4. 简单性。 基于文本的轻量结构;
  5. 通过良好的编程,控制保存在 cookie 中的 session 对象的大小;
  6. 通过加密和安全传输技术( SSL ),减少 cookie 被破解的可能性;
  7. 只在 cookie 中存放不敏感数据,即使被盗也不会有重大损失;
  • 缺点:
  1. Cookie 数量和长度的限制 。
    数量:每个域的 cookie 总数有限。
    a). IE6 或更低版本最多 20 个 cookie;
    b). IE7 和之后的版本最后可以有 50 个 cookie;
    c). Firefox 最多 50 个 cookie;
    d). chrome 和 Safari 没有做硬性限制长度:每个 cookie 长度不超过 4KB ( 4096B ),否则会被截掉;
  2. 潜在的安全风险 。 Cookie 可能被拦截、篡改。如果 cookie 被拦截,就有可能取得所有的 session 信息;
  3. 用户配置为禁用 。有些用户禁用了浏览器或客户端设备接受 cookie 的能力,因此限制了这一功能;
  4. 有些状态不可能保存在客户端 。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。

浏览器缓存

浏览器缓存分为强缓存和协商缓存。当客户端请求某个资源时,获取缓存的流程如下:

  • 先根据这个资源的一些 http header 判断它是否命中强缓存,如果命中,则直接从本地获取缓存资源,不会发请求到服务器;
  • 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些 request header验证这个资源是否命中协商缓存,称为http 再验证,如果命中,服务器将请求返回,但不返回资源,而是告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;
  • 强缓存和协商缓存共同之处在于,如果命中缓存,服务器都不会返回资源; 区别是,强缓存不对发送请求到服务器,但协商缓存会。
  • 当协商缓存也没命中时,服务器就会将资源发送回客户端。
  • 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • 当 f5 刷新网页时,跳过强缓存,但是会检查协商缓存;

浏览器渲染步骤

  1. HTML 解析出 DOM Tree
  2. CSS 解析出 Style Rules
  3. 两者关联生成 Render Tree
  4. Layout(布局)根据 Render Tree 计算每个节点的信
  5. Painting 根据计算好的信息进行渲染整个页面

浏览器解析文档的过程中:
如果遇到 script 标签,会立即解析脚本,停止解析文档(因为 JS 可能会改变 DOM 和 CSS,如果继续解析会造成浪费);
如果是外部 script, 会等待脚本下载完成之后在继续解析文档。现在 script 标签增加了 defer 和 async 属性,脚本解析会将脚本中改变 DOM 和 css 的地方> 解析出来,追加到 DOM Tree 和 Style Rules 上

GET 和 POST 请求的区别

  • GET 参数通过 url 传递,POST 放在 body 中。(http 协议规定,url 在请求头中,所以大小限制很小)
  • GET 请求在 url 中传递的参数是有长度限制的,而 POST 没有。
  • GET 在浏览器回退时是无害的,而 POST 会再次提交请求
  • GET 请求会被浏览器主动 cache,而 POST 不会,除非手动设置
  • GET 比 POST 更不安全,因为参数直接暴露在 url 中,所以不能用来传递敏感信息
  • 对参数的数据类型,GET 只接受 ASCII字符,而 POST 没有限制
  • GET 请求只能进行 url(x-www-form-urlencoded)编码,而 POST 支持多种编码方式
  • GET 产生一个 TCP 数据包;POST 产生两个 TCP 数据包。对于 GET 方式的请求,浏览器会把 http 的 header 和 data 一并发送出去,服务器响应200(返回数据)。而对于 POST,浏览器先发送 header,服务器响应100 continue,浏览器再发送 data,服务器响应200 ok(返回数据)

什么是reflow

浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构的进程叫做 reflow . 通俗点说就是当开发人员定义好了样式后(也包括浏览器的默认样式),浏览器根据这些来计算并根据结果将元素放到它应该出现的位置上,这个过程叫做 reflow . 由于reflow是一种浏览器中的用户拦截操作,所以我们了解如何减少 reflow 次数,及DOM的层级,css 效率对 refolw 次数的影响是十分有必要的。 reflow (回流)是导致DOM脚本执行效率低的关键因素之一,页面上任何一个节点触发了 reflow,会导致它的子节点及祖先节点重新渲染。 简单解释一下 Reflow:当元素改变的时候,将会影响文档内容或结构,或元素位置,此过程称为 Reflow。

导致reflow发生的原因

  • 改变窗口大小
  • 改变文字大小
  • 添加/删除样式表
  • 内容的改变,(用户在输入框中写入内容也会)
  • 激活伪类,如:hover操作class属性
  • 脚本操作DOM
  • 计算offsetWidth和offsetHeight
  • 设置style属性

mapStateToProps和mapDispatchToProps

首先我们在组件当中使用redux,就需要使用react-redux中的connect将该组件与store连接起来,而connect又可以接受两个参数,分别是mapStateToProps和mapDispatchToProps,前者则是获取store里面的状态,用于建立组件跟store的state的映射关系,后者则是用于建立组件跟store.dispatch的映射关系。
一、首先使用connect连接到Store

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(TopNav))

二、使用mapStateToProps建立组件跟store的state的映射关系

const mapStateToProps = ({user,userMessage,uploadImage}) => ({
user,userMessage,uploadImage
})

三、使用mapDispatchToProps用于建立组件跟store.dispatch的映射关系
const mapDispatchToProps = (dispatch) => ({
getMessage : (id) => dispatch(UserMessage(id)),
updateUser : (user,callback) => dispatch(updateUser(user,callback))
})

【Vue3.2】setup & Composition API 新特形总结

起初 Vue3.0 暴露变量必须 return 出来,template中才能使用;
Vue3.2 中 只需要在 script 标签上加上 setup 属性,组件在编译的过程中代码运行的上下文是在 setup() 函数中,无需return,template可直接使用。

1、文件结构

<template>
  // Vue2中,template标签中只能有一个根元素,在Vue3中没有此限制
  // ...
</template>

<script setup>
  // ...
</script>

<style lang="scss" scoped>
  // 支持CSS变量注入v-bind(color)
</style>

2、data

<script setup>
  import { reactive, ref, toRefs } from 'vue'

  // ref声明响应式数据,用于声明基本数据类型
  const name = ref('Jerry')
  // 修改
  name.value = 'Tom'

  // reactive声明响应式数据,用于声明引用数据类型
  const state = reactive({
    name: 'Jerry',
    sex: '男'
  })
  // 修改
  state.name = 'Tom'
  
  // 使用toRefs解构
  const {name, sex} = toRefs(state)
  // template可直接使用{{name}}、{{sex}}
</script>

3、method

<template>
  // 调用方法
  <button @click='changeName'>按钮</button>  
</template>

<script setup>
  import { reactive } from 'vue'

  const state = reactive({
    name: 'Jery'
  })

  // 声明method方法
  const changeName = () => {
    state.name = 'Tom'
  }  
</script>

4、computed

<script setup>
  import { computed, ref } from 'vue'

  const count = ref(1)

  // 通过computed获得doubleCount
  const doubleCount = computed(() => {
    return count.value * 2
  })
</script>

5、watch

<script setup>
  import { watch, reactive } from 'vue'

  const state = reactive({
    count: 1
  })

  // 声明方法
  const changeCount = () => {
    state.count = state.count * 2
  }

  // 监听count
  watch(
    () => state.count,
    (newVal, oldVal) => {
      console.log(state.count)
      console.log(`watch监听变化前的数据:${oldVal}`)
      console.log(`watch监听变化后的数据:${newVal}`)
    },
    {
      immediate: true, // 立即执行
      deep: true // 深度监听
    }
  )
</script>

6、props父传子

子组件:

<template>
  <span>{{props.name}}</span>
  // 可省略【props.】
  <span>{{name}}</span>
</template>

<script setup>
  // import { defineProps } from 'vue'
  // defineProps在<script setup>中自动可用,无需导入
  // 需在.eslintrc.js文件中【globals】下配置【defineProps: true】

  // 声明props
  const props = defineProps({
    name: {
      type: String,
      default: ''
    }
  })  
</script>

父组件:

<template>
  <child name='Jerry'/>  
</template>

<script setup>
  // 引入子组件(组件自动注册)
  import child from './child.vue'
</script>

7、emit子传父

子组件:

<template>
  <span>{{props.name}}</span>
  // 可省略【props.】
  <span>{{name}}</span>
  <button @click='changeName'>更名</button>
</template>

<script setup>
  // import { defineEmits, defineProps } from 'vue'
  // defineEmits和defineProps在<script setup>中自动可用,无需导入
  // 需在.eslintrc.js文件中【globals】下配置【defineEmits: true】、【defineProps: true】
	
  // 声明props
  const props = defineProps({
    name: {
      type: String,
      default: ''
    }
  }) 
  // 声明事件
  const emit = defineEmits(['updateName'])
  
  const changeName = () => {
    // 执行
    emit('updateName', 'Tom')
  }
</script>

父组件:

<template>
  <child :name='state.name' @updateName='updateName'/>  
</template>

<script setup>
  import { reactive } from 'vue'
  // 引入子组件
  import child from './child.vue'

  const state = reactive({
    name: 'Jerry'
  })
  
  // 接收子组件触发的方法
  const updateName = (name) => {
    state.name = name
  }
</script>

8、v-model

子组件:

<template>
  <span @click="changeInfo">我叫{{ modelValue }},今年{{ age }}</span>
</template>

<script setup>
  // import { defineEmits, defineProps } from 'vue'
  // defineEmits和defineProps在<script setup>中自动可用,无需导入
  // 需在.eslintrc.js文件中【globals】下配置【defineEmits: true】、【defineProps: true】

  defineProps({
    modelValue: String,
    age: Number
  })

  const emit = defineEmits(['update:modelValue', 'update:age'])
  const changeInfo = () => {
    // 触发父组件值更新
    emit('update:modelValue', 'Tom')
    emit('update:age', 30)
  }
</script>

父组件:

<template>
  // v-model:modelValue简写为v-model
  // 可绑定多个v-model
  <child
    v-model="state.name"
    v-model:age="state.age"
  />
</template>

<script setup>
  import { reactive } from 'vue'
  // 引入子组件
  import child from './child.vue'

  const state = reactive({
    name: 'Jerry',
    age: 20
  })
</script>

9、nextTick

<script setup>
  import { nextTick } from 'vue'
	
  nextTick(() => {
    // ...
  })
</script>

10、子组件ref变量和defineExpose

在标准组件写法里,子组件的数据都是默认隐式暴露给父组件的,但在 script-setup 模式下,所有数据只是默认 return 给 template 使用,不会暴露到组件外,所以父组件是无法直接通过挂载 ref 变量获取子组件的数据。
如果要调用子组件的数据,需要先在子组件显示的暴露出来,才能够正确的拿到,这个操作,就是由 defineExpose 来完成。

子组件:

<template>
  <span>{{state.name}}</span>
</template>

<script setup>
  import { reactive, toRefs } from 'vue'
  // defineExpose无需引入
  // import { defineExpose, reactive, toRefs } from 'vue'

  // 声明state
  const state = reactive({
    name: 'Jerry'
  }) 
	
  // 将方法、变量暴露给父组件使用,父组件才可通过ref API拿到子组件暴露的数据
  defineExpose({
    // 解构state
    ...toRefs(state),
    // 声明方法
    changeName () {
      state.name = 'Tom'
    }
  })
</script>

父组件:

<template>
  <child ref='childRef'/>  
</template>

<script setup>
  import { ref, nextTick } from 'vue'
  // 引入子组件
  import child from './child.vue'

  // 子组件ref
  const childRef = ref('childRef')
  
  // nextTick
  nextTick(() => {
    // 获取子组件name
    console.log(childRef.value.name)
    // 执行子组件方法
    childRef.value.changeName()
  })
</script>

11、插槽slot

子组件:

<template>
  // 匿名插槽
  <slot/>
  // 具名插槽
  <slot name='title'/>
  // 作用域插槽
  <slot name="footer" :scope="state" />
</template>

<script setup>
  import { useSlots, reactive } from 'vue'
  const state = reactive({
    name: '张三',
    age: '25岁'
  })
  
  const slots = useSlots()
  // 匿名插槽使用情况
  const defaultSlot = reactive(slots.default && slots.default().length)
  console.log(defaultSlot) // 1
  // 具名插槽使用情况
  const titleSlot = reactive(slots.title && slots.title().length)
  console.log(titleSlot) // 3
</script>

父组件:

<template>
  <child>
    // 匿名插槽
    <span>我是默认插槽</span>
    // 具名插槽
    <template #title>
      <h1>我是具名插槽</h1>
      <h1>我是具名插槽</h1>
      <h1>我是具名插槽</h1>
    </template>
    // 作用域插槽
    <template #footer="{ scope }">
      <footer>作用域插槽——姓名:{{ scope.name }},年龄{{ scope.age }}</footer>
    </template>
  </child> 
</template>

<script setup>
  // 引入子组件
  import child from './child.vue'
</script>

12、路由useRoute和useRouter

<script setup>
  import { useRoute, useRouter } from 'vue-router'
	
  // 必须先声明调用
  const route = useRoute()
  const router = useRouter()
	
  // 路由信息
  console.log(route.query)

  // 路由跳转
  router.push('/newPage')
</script>

13、路由导航守卫

<script setup>
  import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
	
  // 添加一个导航守卫,在当前组件将要离开时触发。
  onBeforeRouteLeave((to, from, next) => {
    next()
  })

  // 添加一个导航守卫,在当前组件更新时触发。
  // 在当前路由改变,但是该组件被复用时调用。
  onBeforeRouteUpdate((to, from, next) => {
    next()
  })
</script>

14、store

*Vue3 中的Vuex不再提供辅助函数写法

<script setup>
  import { useStore } from 'vuex'
  import { key } from '../store/index'

  // 必须先声明调用
  const store = useStore(key)
	
  // 获取Vuex的state
  store.state.xxx

  // 触发mutations的方法
  store.commit('fnName')

  // 触发actions的方法
  store.dispatch('fnName')

  // 获取Getters
  store.getters.xxx
</script>

15、生命周期

在这里插入图片描述

16、css变量注入

<template>
  <span>Jerry</span>  
</template>

<script setup>
  import { reactive } from 'vue'

  const state = reactive({
    color: 'red'
  })
</script>
  
<style scoped>
  span {
    // 使用v-bind绑定state中的变量
    color: v-bind('state.color');
  }  
</style>

17、原型绑定与组件内使用

man.js

import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)

// 获取原型
const prototype = app.config.globalProperties

// 绑定参数
prototype.name = 'Jerry'

组件内使用

<script setup>
  import { getCurrentInstance } from 'vue'

  // 获取原型
  const { proxy } = getCurrentInstance()
  
  // 输出
  console.log(proxy.name)
</script>

18、对 await 的支持

不必再配合 async 就可以直接使用 await 了,这种情况下,组件的 setup 会自动变成 async setup 。

<script setup>
  const post = await fetch('/api').then(() => {})
</script>

19、定义组件的name

用单独的<script>块来定义

<script>
  export default {
    name: 'ComponentName',
  }
</script>

20、provide和inject

父组件:

<template>
  <child/>
</template>

<script setup>
  import { provide } from 'vue'
  import { ref, watch } from 'vue'
  // 引入子组件
  import child from './child.vue'

  let name = ref('Jerry')
  // 声明provide
  provide('provideState', {
    name,
    changeName: () => {
      name.value = 'Tom'
    }
  })

  // 监听name改变
  watch(name, () => {
    console.log(`name变成了${name}`)
    setTimeout(() => {
      console.log(name.value) // Tom
    }, 1000)
  })
</script>

子组件:

<script setup>
  import { inject } from 'vue'
	// 注入
  const provideState = inject('provideState')
  
  // 子组件触发name改变
  provideState.changeName()
</script>

js之深浅拷贝的原理与实现

一、浅拷贝的原理与实现

浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值;如果拷贝的是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。

1. Object.assign()

object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并。我们可以使用它来实现浅拷贝。

该方法的参数 target 指的是目标对象,sources指的是源对象。使用形式如下:

Object.assign(target, ...sources)

使用实例:

let target = {a: 1};
let object2 = {b: {d : 2}};
let object3 = {c: 3};
Object.assign(target, object2, object3);  
console.log(target);  // {a: 1, b: {d : 2}, c: 3}

这里通过 Object.assign 将 object2 和 object3 拷贝到了 target 对象中,下面来尝试将 object2 对象中的 b 属性中的d属性由 2 修改为 666:

object2.b.d = 666;
console.log(target); // {a: 1, b: {d: 666}, c: 3}

可以看到,target的b属性值的d属性值变成了666,因为这个b的属性值是一个对象,它保存了该对象的内存地址,当原对象发生变化时,引用他的值也会发生变化。​

==注意:==

  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性;
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回;
  • 因为null 和 undefined 不能转化为对象,所以第一个参数不能为null或 undefined,否则会报错;
  • 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。

实际上,Object.assign 会循环遍历原对象的可枚举属性,通过复制的方式将其赋值给目标对象的相应属性。

2. 拓展运算符

使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。使用形式如下:

let cloneObj = { ...obj };

使用实例:

let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

扩展运算符...object.assign 实现的浅拷贝的功能差不多,如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

3. 数组浅拷贝

3.1 Array.prototype.slice()

slice()方法是JavaScript数组方法,该方法可以从已有数组中返回选定的元素,不会改变原始数组。使用方式如下:

array.slice(start, end)

该方法有两个参数,两个参数都可选:

  • start: 规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
  • end:规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

如果两个参数都不写,就可以实现一个数组的浅拷贝:

let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false

slice 方法不会修改原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串数字布尔值来说,slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

如果向两个数组任一中添加了新元素,则另一个不会受到影响。

3.2 Array.prototype.concat()

concat() 方法用于合并两个或多个数组,此方法不会更改原始数组,而是返回一个新数组。使用方式如下:

arrayObject.concat(arrayX,arrayX,......,arrayX)

该方法的参数arrayX是一个数组或值,将被合并到arrayObject数组中。如果省略了所有 arrayX 参数,则concat会返回调用此方法的现存数组的一个浅拷贝:

let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false

concat方法创建一个新的数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(参数是数组)或参数本身(参数不是数组)。它不会递归到嵌套数组参数中。

concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝,它包含与原始数组相结合的相同元素的副本。 原始数组的元素将复制到新数组中,如下所示:

  • 对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。
  • 数据类型如字符串,数字和布尔值:concat将字符串和数字的值复制到新数组中。

4. 手写实现浅拷贝

根据以上对浅拷贝的理解,实现浅拷贝的思路:

  • 对基础类型做最基本的拷贝;
  • 对引用类型开辟新的存储,并且拷贝一层对象属性。

代码实现:

// 浅拷贝的实现;
function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;
  // 根据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};
  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }
  return newObject;
}

这里用到了hasOwnProperty()方法,该方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性。所有继承了 Object 的对象都会继承到 hasOwnProperty() 方法。这个方法可以用来检测一个对象是否是自身属性。

可以看到,所有的浅拷贝都只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝就无能为力了。深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。

二、深拷贝的原理与实现

深拷贝是指,对于简单数据类型直接拷贝他的值,对于引用数据类型,在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个不会发生改变。

1. JSON.stringify()

JSON.parse(JSON.stringify(obj))是比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify 将JavaScript对象序列化成为JSON字符串),并将对象里面的内容转换成字符串,再使用JSON.parse来反序列化,将字符串生成一个新的JavaScript对象。

使用示例:

let obj1 = {  
  a: 0,
  b: {
    c: 0
  }
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}

这个方法虽然简单粗暴,但也存在一些问题,在使用该方法时需要注意:

  • 拷贝的对象中如果有函数undefinedsymbol,当使用过JSON.stringify()进行处理之后,都会消失。
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 Date 引用类型会变成字符串;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaNInfinity 以及 -InfinityJSON 序列化的结果会变成 null
  • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)

在日常开发中,上述几种情况一般很少出现,所以这种方法基本可以满足日常的开发需求。如果需要拷贝的对象中存在上述情况,还是要考虑使用下面的几种方法。

2. 函数库lodash

该函数库也有提供_.cloneDeep用来做深拷贝,可以直接引入并使用:

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

3. 手写实现深拷贝

3.1 基础递归实现

实现深拷贝的思路就是,使用for in来遍历传入参数的属性值,如果值是基本类型就直接复制,如果是引用类型就进行递归调用该函数,实现代码如下:

function deepClone(source) {
      //判断source是不是对象
      if (source instanceof Object == false) return source;
      
  		//根据source类型初始化结果变量
      let target = Array.isArray(source) ? [] : {}; 
      for (let i in source) {
        // 判断是否是自身属性
        if (source.hasOwnProperty(i)) {
          //判断数据i的类型
          if (typeof source[i] === 'object') {
            target[i] = deepClone(source[i]);
          } else {
            target[i] = source[i];
          }
        }
      }
      return target;
    }
   
console.log(clone({b: {c: {d: 1}}}));  // {b: {c: {d: 1}}})

这样虽然实现了深拷贝,但也存在一些问题:

  • 不能复制不可枚举属性以及 Symbol 类型;
  • 只能对普通引用类型的值做递归复制,对于 Date、RegExp、Function 等引用类型不能正确拷贝;
    可能存在循环引用问题。

3.2 优化递归实现

上面只是实现了一个基础版的深拷贝,对于上面存在的几个问题,可以尝试去解决一下:

使用 Reflect.ownKeys() 方法来解决不能复制不可枚举属性以及 Symbol 类型的问题。

Reflect.ownKeys() 方法会返回一个由目标对象自身的属性键组成的数组。它的返回值等同于:

Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));

当参数值为 DateRegExp 类型时,直接生成一个新的实例并返回;
利用 Object.getOwnPropertyDescriptors() 方以获得对象的所有属性以及对应的特性。简单来说,这个方法返回给定对象的所有属性的信息,包括有关gettersetter的信息。它允许创建对象的副本并在复制所有属性(包括getter和setter)时克隆它。

使用 Object.create() 方法创建一个新对象,并继承传入原对象的原型链。Object.create()方法会创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

使用 WeakMap 类型作为 Hash 表,WeakMap 是弱引用类型,可以防止内存泄漏,所以可以用来检测循环引用,如果存在循环,则引用直接返回 WeakMap 存储的值。WeakMap的特性就是,保存在其中的对象不会影响垃圾回收,如果WeakMap保存的节点,在其他地方都没有被引用了,那么即使它还在WeakMap中也会被垃圾回收回收掉了。

在深拷贝的过程当中,里面所有的引用对象都是被引用的,为了解决循环引用的问题,在深拷贝的过程中,希望有个数据结构能够记录每个引用对象有没有被使用过,但是深拷贝结束之后这个数据能自动被垃圾回收,避免内存泄漏。

代码实现:

function deepClone (obj, hash = new WeakMap()) {
  // 日期对象直接返回一个新的日期对象
  if (obj instanceof Date){
  	return new Date(obj);
  } 
  //正则对象直接返回一个新的正则对象     
  if (obj instanceof RegExp){
  	return new RegExp(obj);     
  }
  //如果循环引用,就用 weakMap 来解决
  if (hash.has(obj)){
  	return hash.get(obj);
  }
  // 获取对象所有自身属性的描述
  let allDesc = Object.getOwnPropertyDescriptors(obj);
  // 遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    if(typeof obj[key] === 'object' && obj[key] !== null){
    	cloneObj[key] = deepClone(obj[key], hash);
    } else {
    	cloneObj[key] = obj[key];
    }
  }
  return cloneObj
}

可以使用下面数据进行测试:

let obj = {
  num: 1,
  str: 'str',
  boolean: true,
  und: undefined,
  nul: null,
  obj: { name: '对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('函数') },
  date: new Date(1),
  reg: new RegExp('/正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' 
});
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 将loop设置成循环引用的属性
let cloneObj = deepClone(obj)

console.log('obj', obj)
console.log('cloneObj', cloneObj)

【JavaScript进阶】高阶函数之Reduce的解析&使用

reduce() 的高级用法

Array.prototype.reduce()

reduce() 方法接收一个函数作为累加器,为数组中的每一个元素执行回调函数,并将其回调的的结果值作为返回值。

语法:

arr.reduce(callback(priviousValue,currentValue,currentIndex,sourceArray),[initValue])

reduce 函数接收四个参数:

  • callback:reduce得回调函数,接收四个参数:

    • priviousValue:累加器,即上一次回调返回的值,或者提供的初始值(initValue)
    • currentValue:数组中当前正在被处理的值
    • currentIndex: 数组中当前元素的索引(可选)
    • sourceArray:reduce的源数组(可选)
  • initValue:作为第一次调用callback得第一个参数的初始值。(可选)

  • 返回值

    回调函数累计处理的结果

reduce() 执行过程

从MDN文档描述中:

回调函数第一次执行时,priviousValuecurrentValue的取值有两种情况:如果调用reduce()时提供了initValuepriviousValue取值为initValuecurrentValue取数组中的第一个值;如果没有提供 initValue,那么priviousValue取数组中的第一个值,currentValue取数组中的第二个值。

解释:也就是说如果没有我们没有给出初始值,开始执行时,数组中被处理的取值索引是1,而不是从0开始的。这会导致数组中第一个元素(即索引为0没有被处理,因为数组的元素索引是从0开始的)

大白话显得苍白无力,实践出真知。我们来实践一下:

实例解析initValue 参数

第一个例子:没有initValue 时

let array = [1,2,3,4,5];
  let sum = array.reduce((preValue,curValue,curIndex,array) => {
      console.log(preValue,curValue,curIndex);
      return preValue + curValue;
  })
  console.log(array,sum);

打印结果:

1 2 1
3 3 2
6 4 3
10 5 4
[1,2,3,4,5] 15

image-20211107125227832.png

从打印的结果来看,我的数组长度是5,但是reduce()循环打印的次数只有4次,函数开始执行的数组索引是从index为1开始的,第一次的preValue值是数组的第一个值1

第二个例子:有initValue 时:

我们在第一个例子的基础上,修改一下:

let array = [1,2,3,4,5];
let sum = array.reduce((preValue,curValue,curIndex,array) => {
    console.log(preValue,curValue,curIndex);
    return preValue + curValue;
},0) //与上个例子相比,这里设置初始值0
console.log(array,sum);

这时候的打印结果:

0 1 0
1 2 1
3 3 2
6 4 3
10 5 4
[1,2,3,4,5] 15

image-20211107130312049.png

这时候的函数的执行是从index为0开始的,第一次执行回调函数时,preValue的值是我们设置的初始值0,回调函数执行5次,数组长度为5.

总之:如果没有提供initValue,reduce 会从索引1的地方开始执行 callback 函数,直接跳过第一个索引。如果提供initialValue,从索引0开始。

还有一个值得注意的是:

如果没有提供初始值,可能会有下面这四种输出的可能:

var maxCallback = ( acc, cur ) => Math.max( acc.x, cur.x );
var maxCallback2 = ( max, cur ) => Math.max( max, cur );// reduce() 没有初始值
[ { x: 2 }, { x: 22 }, { x: 42 } ].reduce( maxCallback ); // NaN
[ { x: 2 }, { x: 22 }            ].reduce( maxCallback ); // 22
[ { x: 2 }                       ].reduce( maxCallback ); // { x: 2 }
[                                ].reduce( maxCallback ); // TypeError// map/reduce; 这是更好的方案,即使传入空数组或更大数组也可正常执行
[ { x: 22 }, { x: 42 } ].map( el => el.x )
                        .reduce( maxCallback2, -Infinity );

如果数组为空且没有提供initialValue,会抛出TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供initialValue, 或者有提供initialValue但是数组为空,那么此唯一值将被返回并且callback不会被执行。因此提供初始值通常更安全。

reduce的运用

(1)计算数组中每个元素出现的次数

// 计算数组中每个元素出现的次数
let nums = ["01","02","03","01","05","04","02"];
let count = nums.reduce((preV,currentV) => {
    if (currentV in preV) {
        preV[currentV] ++
    } else {
        preV[currentV] = 1
    }
    return preV
},{})
console.log(count);

image-20211107134211645.png
(2) 数组去重

// 数组去重
let nums = ["01","02","03", ,"04","02","01","05"];
let newArr = nums.reduce((preV,currentV) => {
    if (!preV.includes(currentV)) {
        return preV.concat(currentV)
    } else {
        return preV
    }
},[])
console.log(newArr);

image-20211107134847322.png

(3)数组扁平化:将多维数组转化为一维数组:

// 数组扁平化运用:将二维数组转化为一维
  let arr = [[0,1],[2,3,4],[5,6,7,8]]
  let newArr = arr.reduce((preV,currentV) => {
      return preV.concat(currentV)
  },[])
  console.log(newArr);

打印结果:

image-20211107135505889.png
(4)对象里的属性求和

// 对象里的属性求和
  let items = [
      {
          id: 1,
          name: 'python',
          count: 20
      },
      {
          id: 2,
          name: 'javascript',
          count: 30
      },
      {
          id: 3,
          name: 'java',
          count: 50
      }
  ];
  let total = items.reduce((preV,currentV) => {
      return currentV.count +preV;
  },0);
  console.log("总统计人数:" + total)

打印结果:

image-20211107140106666.png

【MySQL】深入浅出MySQL基础知识(一)

@TOC

一、 MySQL 介绍

1.1 MySQL 简介

Michael Widenius "Monty"是一位编程天才。19 岁的时候,他从赫尔辛基理工大学辍学开始全职工作,因为大学已经没有什么东西可以教他了。33 岁时,他发布了MySQL,成为了全世界最流行的开源数据库。并以 10亿美元的价格,将自己创建的公司 MySQL AB卖给了 SUN,此后,Oracle 收购了 SUN。
Widenius 离开了 Sun 之后,觉得依靠 Sun/Oracle 来发展 MySQL,实在很不靠谱,于是决定另开分支,这个分支的名字叫做 MariaDB。MariaDB名称来自他的女儿Maria的名字。
MariaDB跟 MySQL在绝大多数方面是兼容的,对于开发者来说,几乎感觉不到任何不同。目前 MariaDB 是发展最快的 MySQL 分支版本,新版本发布速度已经超过了 Oracle官方的 MySQL版本。

1.2 关系型数据库

关系型数据库以行和列的形式存储数据,这一系列的行和列被称为表,一组表组成了数据库。
表与表之间的数据记录有关系。
数据保存在表内,行(记录)用于记录数据,列(字段)用于规定数据格式

二、 MySQL 管理

2.1 MySQL 数据库管理

查看数据库

show databases;

创建数据库

# 如果没有修改 my.ini 配置文件的默认字符集,在创建数据库时,指定字符集
create database db_name character set 'utf8';

# 特殊字符(关键字)用反引号
create database `create`;

MySQL\data 目录下将自动生成一个对应名称的目录,目录内部有一个 db.opt 文件

显示数据库创建信息

show create database db_name;   # db_name 即你的数据库名

删除数据库

drop database db_name;   # db_name 即你的数据库名

进入/使用数据库

use database;

显示当前打开的数据库

select database();

2.2 MySQL 表结构管理

创建数据表

create table 表名(字段 字段类型,....)

MySQL\data目录下的数据库目录中将自动生成一个对应名称的.frm 文件

删除数据表

drop table 表名;

查看数据表

show tables; 

# 查看字母是'l'开头的表
show tables like 'l%'; # %是通配符

查看表创建信息

show create table 表名;

查看数据表结构

desc 表名;

2.3 MySQL 用户管理

登录

MySQL 是基于C/S 架构,必须在客户端通过终端窗口,连接MySQL 服务器进行操作

mysql -h host -u user -p
Enter password:*********

用户管理

  • 超级用户 root
  • 修改账号密码
    例如:
## DOS命令下修改,将 root 账号密码修改为 123456
mysqladmin -u root password 1234 ##语句最后不要加分号,否则密码就是 “123456;”

##mysql 命令
set password for 'root'@'localhost'= password('123456');
  • 创建用户
    使用 create语句进行创建用户,语句格式如下:
create user 'username'@'host' identified by 'password';

参数说明:

  1. username: 表示要创建的用户名;
  2. host: 表示指定该用户在哪个主机上可以登陆,如果是本地用户可用 localhost,如果想让该用户可以从任意远程主机登陆,可以使用通配符%;
  3. password: 表示该用户的登陆密码,密码可以为空,如果为空则该用户可以不需要密码登陆服务器。例:
create user 'zhangsan'@'localhost' identified by '123456';
  • 删除用户
    删除用户使用drop 语句,语法格式为:
drop user 'username'@'host';

修改配置文件 my.ini

字符集
MySQL默认字符集是 latin( 拉丁 ),改变为 utf8 才能正确显示中文
[mysqld] 下添加
character-set-server=utf8
init-connect='\set NAMES utf8'
修改默认引擎
InnoDB 优于 MYISAM
default-storage-engine=MYISAM 修改为 default-storage-engine=InnoDB
化 个性化 mysql  提示符
MySQL默认提示符是” mysql>“ ,可以个性化定制,例如:"mysql(数据库)>"
[mysql]下添加
prompt="mysql(\d)>"

2.4 默认数据库

  • information_schema
    提供了访问数据库元数据的方式。什么是元数据呢?元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。有些时候用于表述该信息的其他术语包括“数据词典”和“系统目录”。
  • performance_schema
    mysql 5.5 版本 新增了一个性能优化的引擎
  • mysql
    这个是 MySQL的核心数据库,主要负责存储数据库的用户、权限设置、关键字等 MySQL自己需要使用的控制和管理信息。不可以删除,也不要轻易修改这个数据库里面的表信息。
  • test
    安装时候创建的一个测试用数据库,空数据库,没有任何表,可以删除(新版mysql 已取消)。

三、 SQL 基础语法

3.1 SQL 语句简介

SQL 语言

SQL(Structured Query Language)是用于访问和处理数据库的 标准计算机语言。使用SQL 访问和处理数据系统中的数据,这类数据库包括:Oracle,mysql,Sybase, SQLServer, DB2, Access 等等。

基本规范

  • SQL 对大小写不敏感,一般数据库名称、表名称、字段名称全部小写
  • MySQL要求在每条 SQL 命令的末端使用分号(MS Access 和 SQL Server2000,则不必在每条 SQL 语句之后使用分号)。

注释

mysql> select 1+1; # 这个注释直到该行结束
mysql> select 1+1; -- 这个注释直到该行结束
mysql> select 1 /* 这是一个在行中间的注释 */ + 1;
mysql> select 1+
/*
这是一个
多行注释的形式
*/
1;

3.2 MySQL 基本数据类型

字段类型

数据类型是指列、存储过程参数、表达式和局部变量的数据特征,它决定了数据的存储方式,代表了不同的信息类型。不同的数据库,数据类型有所不同,MySQL数据库有以下几种数据类型:

字符串型

类型 字节 大小 说明
char 1 0-255 字符 (2^8) 定长字符串
varchar 2 0-65 535 字符 (2^16) 变长字符串
tinytext 1 0-255 字符 (2^8) 短文本(与 char存储形式不同)
text 2 0-65 535 字符(2^16) 文本
mediumtext 3 0-16 777 215字符 (2^24) 中等长度文本
longtext 4 0-4 294 967 295字符(2^32) 极大文本

注意:char 和 和 varchar 需要指定长度,例如:char(10)

整数型

类型 字节 范围(有符号) 范围(无符号) 用途
tinyint 1 (-128,127) (0,255) 很小整数值
smallint 2 (-32 768,32 767) (0,65 535) 小整数值
mediumint 3 (-8 388 608,8 388 607) (0,16 777 215) 中整数值
int 或integer 4 (-2 147 483 648,2 147 483647) (0,4 294 967 295) 整数值
bigint 8 (-9 233 372 036 854 775 808,9 223 372 036 854 775 807) (0,18 446 744073 709 551 615) 很大的整数值

很多人喜欢在定义数据时,这样写:

create table tbl_name(
age int(10)
);

int 后面()中的数字,不代表占用空间容量。而代表最小显示位数。这个东西基本没有意义,除非你对字段指定 zerofill。mysql 会自动分配长度:int(11)tinyint(4)smallint(6)mediumint(9)bigint(20)。所以,建议在使用时,就用这些默认的显示长度就可以了。不用再去自己填长度(比如:int(10)tinyint(1)之类的基本没用)。

浮点型

类型 字节 范围 用途
float(M,D) 4 23bit(约 6~7位 10进制数字) 单精度浮点数
值绝对能保证精度为 6~7位有效数字
double(M,D) 8 52bit(约 15~16位10 进制数字) 双精度浮点数值
精度为15~16位有效数字
decimal(M,D) M+2 依赖于 M 和 D的值 定点型

M(精度),代表总长度(整数位和小数位)限制
D(标度),代表小数位的长度限制。
M 必需大于等于 D

数字的修饰符 功能 说明
unsigned 无符号 非负数
zerofill 前导 0 整形前加 0(自动添加 unsigned)

日期型

类型 字节 范围 格式 用途
date 3 1000-01-01---9999-12-31 YYYY-MM-DD 日期值
time 3 -838:59:59---838:59:59 HH:MM:SS 时间值或持续时间
year 1 1901---2155 YYYY 年份值
datetime 8 1000-01-01 00:00:00---9999-12-3123:59:59 YYYY-MM-DDHH:MM:SS 混合日期和时间值
timestamp 4 1970-01-01 00:00:00/2038 结束时间是第 2147483647 秒,北京时间2038-1-19 11:14:07,格林尼治时间2038 年 1 月 19日 凌晨 03:14:07 YYYYMMDDHHMMSS

列举与枚举

名称 字节 说明
set 1、2、3、4或 8 列举:可以取 SET列表中的一个或多个元素(多选)
enum 1或 2 枚举:可以取 ENUM 列表中的一个元素(单选)
create table students(
id tinyint, # 微小整型
name varchar(10), #变长字符
sex enum('m','w'), #单选
birthday date, # 日期型
tel char(11), # 定长字符
city char(1), # 城市
hobby set('1','2','3','4'), #多选
introduce  text # 个人介绍
);

字段属性

属性 功能 说明
not null 非空 必须有值,不允许为 null
default 默认值 当插入记录时没有赋值,自动赋予默认值(允许为null)
primary key 主键 惟一标识一行数据的字段(主键自动为 not null)
auto_increment 自动增量 不能单独使用,必须与 primary key 一起定义
unique(uniquekey) 唯一 记录不能重复(一张表可以有多个 unique,允许为null)

3.3 数据的增删改查(简称:CURD)

数据的增删改

新增(增加/插入数据)

# 方法 1:指定字段
insert into students(name,age) values('张三','20');

# 方法 2: 省略字段名,字段位要一一对应,不能跳过(auto_increment 字段,可以使用 null 或 default)
insert into students values(null,'张三','20');

# 方法 3:批量增加数据
insert into students(name,age) values('张三','20'),('李四','21'),('王五','22') ……

删除

# 用 delete删除记录,一定要加 where条件,否则表数据全部删除!!
delete from 表名 where xx=xxx;

# 用 truncate删除记录,不能加 where条件,直接删除全部记录,id索引重新从 1开始
truncate table 表名;

修改

单条修改
update 表名 set xx=xx,xxx=xx where xxx=xxx and xxx=xxx;

#多条修改
update students
set name = case id # id 字段
when 1 then 'zhangsan'
when 2 then 'lisi'
when 3 then 'wangwu'
when 4 then 'zhaoliu'
end,
city = case id
when 1 then '2'
when 2 then '4'
when 3 then '1'
when 4 then '2'
end
where id in (1,2,3,4);

四、数据的查询

查询比较繁杂,单拎出来

SELECT 语句用于从表中选取数据。结果被存储在一个结果表中(称为*结果集*)。

4.1 查询表达式

# 当前使用的数据库
select database();
# 查看当前 MySQL版本
select version();
# 查看当前用户
select user();
# 查看运算结果
select 1+2;

4.2 条件表达式

from 子句

# 字段用','隔开,至少有一个字段,最终结果集按照这个这个顺序显示
select 字段 1,字段 2... from 表名;
# *代表所有字段
select * from 表名;

distinct(去重复)

# 去重后的结果,distinct 必须紧接在 select 后面
select distinct 字段 from 表名;

# 统计不重复的个数
select count(distinct 字段) from 表名;

where 子句

  • where子句适用于对记录的 删、改、查 操作
  • 对记录进行过滤,如果没有指定 where子句,则显示所有记录
  • 在 where表达式中,可以使用函数或运算符,运算符包括如下表:
类型 运算符
算术 + - * / %
比较 > < >= <= != =
逻辑 or
提升优先级 ( )
# 搜索 id<20的所以数据
select * from students where id<20;
# 搜索 id 编号为偶数的数据
select * from students where id%2=0;

where 条件关键字:

  • in : 查询一个集合的数据
# 搜索 id 在(1,3,7)之中的数据
select * from students where id=1 || id=3 || id=7;
select * from students where id in(1,3,7);
# 一次删除多条记录
delete from students where id=3 || id=15 || id=23;
delete from students where id in(3,15,23);
  • between...and...: 查询一个区间的数据
# 搜索 id 在 20-40之间的数据
select * from students where id>20 && id<40;
select * from students where id between 20 and 40;
# 删除 id 在 20-40之间的数据
delete from students where id between 20 and 40;
  • not : 排除
# 搜索 id 除了 20-40之间的数据
select * from students where id not between 30 and 40;

like 子句

用于模糊查询 %:任意字符长度 _ :一个字符长度

# 搜索 name名字以 5 结尾的数据
select * from students where name like '张%';
# 搜索 name名字包含字母 s 的数据
select * from students where name like '%二%';
# 搜索 id 以 5结尾的两位数 数据
select * from students where id like '_5';

limit 子句

控制查询记录条数,数据表中的记录,从 索引从 0 开始

select * from students limit 2 # 返回两条记录
select * from students limit 3,4 # 从索引为 3的记录开始,返回 4条记录
# php中的分页功能,偏移值的计算:(当前页-1) * 每页记录数
select name from student limit 4 offset 3
# 还可以使用 offset(偏移):从索引为 3的记录开始,返回 4 条

group by (结果分组)

根据 给定数据列的每个成员对查询结果进行分组统计,最终得到一个分组汇总表利用 group by分组信息进行统计,常见的是配合 max 等聚合函数筛选数据后分析。

select 指定的字段要么作为分组的依据(Group By 语句的后面),要么就要被包含在聚合函数中。

# 简单分组
select sex from students group by sex;
+-----+
| sex |
+-----+
| m |
| w |
+-----+
# 聚合函数分组
select count(*),city from students group by city;
+----------+----------+
| count(*) | livecity |
+----------+----------+

# 结果
| 7 | 1 |
| 8 | 2 |
| 1 | 3 |
| 3 | 4 |
| 2 | 5 |
+----------+----------+

order by( 结果排序)

按照给定的字段进行排序,asc:升序(默认) desc:降序
如果同时选择多个字段,先按第一个字段排序,如果第一个字段值相等,再尝试第二个字段,以此类推

# 默认升序
select * from students order by birthday;
# 降序
select * from students order by birthday desc;

查询语句的书写顺序

select → 字段→ from→ 表名→where→group by→order by→limit

别名

在这里插入图片描述

4.3 多表查询

例如这里有三个表:
在这里插入图片描述
从姓名表中查询为id为6,成绩表中他课程号为4,科目表中id为4的所有信息:

select a.name,c.name,b.score from students as a,score as b,course as c where b.user_id=6 and b.course_id=4 and a.id=b.user_id and c.id=b.course_id;

表连接

有时为了得到完整的结果,我们需要从两个或更多的表中获取结果。我们就需要执行 join
在这里插入图片描述

内连接

  • JOIN: 如果表中有至少一个匹配,则返回行
select a.*,b.name from students as a [inner] join city as b on a.livecity=b.id;
  • LEFT JOIN: 即使右表中没有匹配,也从左表返回所有的行
select a.*,b.name from students as a left join city as b on a.livecity=b.id;
  • RIGHT JOIN: 即使左表中没有匹配,也从右表返回所有的行
select a.*,b.name from students as a right join city as b on a.livecity=b.id;
  • FULL JOIN: 只要其中一个表中存在匹配,就返回行(mysql 不支持)
# 带有连接的分组
select city.name, count(*) from students,city where students.livecity=city.id group by st
udents.livecity+--------+----------+
| name | count(*) |
+--------+----------+
| 北京 | 7 |
| 上海 | 8 |
| 杭州 | 1 |
| 深圳 | 3 |
+--------+----------+

4.4 子查询

子查询(subquery)是指出现在其他 SQL语句内的 select 子句(嵌套在查询内部,且必须始终出现在圆括号内)

# 查找城市名称是北京的
# 普通方式查询
select * from students where livecity=1;
# 子查询方式
select * from students where livecity=(select id from city where name='北京');
  • 子查询可以包含多个关键字或条件,如:distinct、group by、order by、limit、函数等
  • 子查询的外层可以是:select,insert,update

五、视图与事务

5.1 视图

视图是从一个或几个基本表(或视图)中导出的虚拟的表。在系统的数据字典中仅存放了视图的定义,不存放视图对应的数据。视图是原始数据库数据的一种变换,是查看表中数据的另外一种方式。可以将视图看成是一个移动的窗口,通过它可以看到感兴趣的数据。 视图是从一个或多个实际表中获得的,这些表的数据存放在数据库中。那些用于产生视图的表叫做该视图的基表。一个视图也可以从另一个视图中产生。

数据库中视图是一个重要的概念,其优势在于:

  • 安全:有的数据是需要保密的,如果直接把表给出来进行操作会造成泄密,那么可以通过创建视图把相应视图的权限给出来即可保证数据的安全。
  • 高效:复杂的连接查询,每次执行时效率比较低,建立视图,每次从视图中获取,将会提高效率。
  • 定制数据:将常用的字段放置在视图中。

创建视图

CREATE VIEW view_name AS SELECT column_name(s) FROM table_name WHERE conditi
on;
参数说明:

  • viewname 为欲创建的视图名
  • columnname(s)为查询的列名
  • table_name 为查询的表名
  • condition 为查询条件。

修改视图

# ALTER 语句:
ALTER VIEW view_name AS SELECT column_name(s) FROM table_name WHERE c
ondition;
# CREATE OR REPLACE 语句:
ALTER VIEW view_name AS SELECT column_name(s) FROM table_name WHERE c
ondition;

删除视图

DROP view_name;

查询视图

SHOW TABLES;
SHOW TABLE STATUS;

这两个命令不仅可以显示表名及表信息,而且会显示出所有视图名称及视图信息。除此之外,使用 SHOW CREATE VIEW 命令可以查看某个视图的定义,格式如下:

SHOW CREATE VIEW view_name;

关系数据库表是用于存储和组织信息的数据结构,数据结构的不同,直接影响操作数据的效率和功能,对于 MySQL来说,它提供了很多种类型的存储引擎,可以根据对数据处理的需求,选择不同的存储引擎,从而最大限度的利用 MySQL强大的功能。

5.2 事务

MyISAM 引擎

MyISAM 表是独立于操作系统的,这说明可以轻松地将其从 Windows 服务器移植到 Linux 服务器,建立一个 MyISAM 引擎的 tb_Demo 表,就会生成以下三个文件:

  • tbdemo.frm 存储表定义
  • tbdemo.MYD 存储数据
  • tb_demo.MYI 存储索引。

MyISAM 无法处理事务 ,特别适合以下几种情况下使用:

  1. 选择密集型的表。MyISAM 存储引擎在筛选大量数据时非常迅速,这是它最突出的优点。
  2. 插入密集型的表。MyISAM 的并发插入特性允许同时选择和插入数据。例如:MyISAM 存储引擎很适合管理邮件或 Web 服务器日志数据。

InnoDB 引擎

InnoDB是一个健壮的事务型存储引擎,InnoDB还引入了外键约束,在以下场合下,使用 InnoDB是最理想的选择:

  1. 更新密集的表。InnoDB存储引擎特别适合处理多重并发的更新请求。
  2. 事务。InnoDB存储引擎是支持事务的标准 MySQL存储引擎。
  3. 外键约束。MySQL支持外键的存储引擎只有 InnoD
  4. 自动灾难恢复。与其它存储引擎不同,InnoDB表能够自动从灾难中恢复。

事务处理

以银行转账业务为例,张三→李四转账 100 元,这是一个完整事务,需要两步操作:

  1. 张三数据表减去 100元
  2. 李四数据表增加 100元

如果在 1步完成后,操作出现错误(断电、操作异常等),使 2步没有完成,此时,张三减去了 100元,而张三却没有收到 100 元

为了避免这种情况的发生,就将整个操作定义为一个事务,任何操作步骤出现错
误,都会回滚到上一次断点位置,避免出现其他错误。
语法:

# 开始
begin;
update tbl_a set money=money-100 where name='zhangsan';
update tbl_b set money=money+100 where name='lisi';
# 提交
commit;
# 回滚
rollback;

六、 索引约束分区

6.1 索引

索引是帮助 MySQL高效获取数据的数据结构

数据库在保存数据之外,还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。索引可以大大提高 MySQL的检索速度。

在 MySQL中,对于一个 Primary Key的列,MySQL已经自动对其建立了 UniqueIndex

创建索引

# 创建索引
create table 表名(
id int not null,
username varchar(16) not null,
index(username(length)) ### 用 username字段作为索引
);

显示索引

# 显示索引
show index from 表名;

删除索引

# 删除索引
alter table 表名 drop index name;

6.2 约束

约束保证数据的完整性和一致性,根据约束的字段数目的多少,约束又分为表级约束和列级约束

  • 列级约束:针对某一字段来使用
  • 表级约束:针对两个或两个以上的字段使用

约束类型包括:

  • not null(非空约束)
  • primary key (主键约束)
  • unique key (唯一约束)
  • default (默认约束)
  • foreign key(外键约束)

唯一(unique)约束

  • unique 约束唯一标识数据库表中的每条记录。
  • unique和 primary key约束均为列提供了唯一性的保证。
  • primary key 被自动定义为 unique 约束。
    注意: 每个表可以有多个 unique约束,但是每个表只能有一个 primary key 约束。
# 第一种方式
create table persons(
id_p int not null,
address varchar(255),
city varchar(255),
phone varchar(11) unique # 定义字段的同时,定义约束
);
# 第二种方式
create table persons(
id_p int not null,
address varchar(255),
city varchar(255),
phone varchar(11),
unique phone(phone) # 单独一行命令,定义约束
# 第三种方式
alter table persons add unique city(city); #修改表

默认(default)约束

用于约束对应列中的值的默认值(除非默认为空值,否则不可插入空值)

create table persons(
id tinyint primary key auto_increment,
name varchar(30),
sex enum('m','w') default 'm', # 定义 sex 默认值为:'m'
)

主键(primary key)约束

每张数据表只能存在一个主键,主键保证记录的唯一性,主键自动为 not null(同时作为表的索引)。

# 为没有主键的表添加主键
alter table 表名 add primary key (字段名)

外键(foreign key) 约束

外键约束是为了保持数据一致性,完整性,实现一对一或一对多关系

  • 子表(具有外键列的表)和 父表(子表所参照的表),存储引擎只能为innoDB
  • 外键列和参照列必须具有相似的数据类型。
    • 如果是数字类型,数字的长度、是否有符号位 必须相同
    • 字符类型的长度则可以不同
  • 外键列和参照列必须创建索引(如果外键列不存在索引的话,MySQL将自动创建索引)。
# 先建父表 子表才能建外键 父表和子表必须都是 innodb 引擎
# city父表
create table city(
id tinyint primary key,
name varchar(10) not null
)engine=INNODB;
# students 子表
create table students(
id tinyint primary key auto_increment, #id
# 定义字段时同时定义
city tinyint, # 外键字段类型要与主表相同
foreign key(city) references city(id), # city 字段作为外键 ,引用 city表中的 id
)engine=INNODB;
-----------------------------------------------------------------------------
# 主表的数据可以修改,但不能删除
# 删除 city中的记录
delete from city where id=1;
# 创建外键以后,再删除 city记录,就会报错:
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fail
s (`hxsd`.`students`, CONSTRAINT `students_ibfk_1` FOREIGN KEY (`city`) REFERE
NCES `city` (`id`))

6.3 删除约束

删除 primary key

alter table 表名 drop primary key;

删除 index

alter table 表名 drop index index_name;

删除外键约束

alter table 表名 drop foreign key FK_ID;

6.4 索引与约束的关系

索引是面向数据库本身的,用于查询优化等操作。约束则更多的是业务上的关系。

通常,创建唯一约束就自动获取唯一索引,是因为数据库认为对数据库进行唯一检查时,如果该字段上有索引会很快,所以创建唯一约束就默认创建唯一索引。同样,常见的主键即是唯一性的约束,也是个索引。但对于 not null 这样的约束,数据库是不会创建索引的。

6.5 分区

如果一张表的数据量太大,不仅查找数据的效率低下,而且难以找到一块集中的存储来存放。为了解决这个问题,数据库推出了分区的功能。MySQL 表分区主要有以下四种类型:

  • RANGE分区:
    RANGE即范围分区,根据区间来判断位于哪个分区。这些区间要连续且不能相互重叠,使用 VALUES LESS THAN 操作符来进行定义。
create table test(
id int DEFAULT null,
name char(30),
datedata date
)
PARTITION BY RANGE (year(datedata)) (
PARTITION part1 VALUES LESS THAN (1990) ,
PARTITION part2 VALUES LESS THAN (1995) ,
PARTITION part3 VALUES LESS THAN (2000) ,
PARTITION part4 VALUES LESS THAN MAXVALUE ); 
  • LIST 分区:
    LIST 分区类似于按 RANGE 分区,区别在于 LIST 分区是基于列值匹配一个离
    散值集合中的某个值来进行选择。
create table test1(
id int not null,
name char(30),
career VARCHAR(30)
)
PARTITION BY LIST (id) (
PARTITION part0 VALUES IN (1,5) ,
PARTITION part1 VALUES IN (11,15) ,
PARTITION part2 VALUES IN (6,10) ,
PARTITION part3 VALUES IN (16,20) 
);
  • HASH分区:
    HASH分区基于用户定义的表达式返回值来选择分区,该表达式对要插入到表的行中列值进行 Hash 计算。
CREATE TABLE employees (
id INT NOT NULL, 
firstname VARCHAR(30), 
lastname VARCHAR(30), 
hired DATE NOT NULL DEFAULT '1970-01-01', 
separated DATE NOT NULL DEFAULT '9999-12-31', 
job_code INT, 
store_id INT
) 
PARTITION BY HASH(store_id) PARTITIONS 4;
  • KEY分区
    KEY分区类似 HASH,但是 HASH允许用户使用自定义表达式,而 KEY分区不允许,它需要使用 MySQL服务器提供的 HASH 函数,同时 HASH 分区只支持整数分区,而 KEY分区支持除 BLOB 和 TEXT类型外其他列。
CREATE TABLE tk ( 
col1 INT NOT NULL, 
col2 CHAR(5), 
col3 DATE,
PRIMARY KEY(col1)
) 
PARTITION BY KEY (col1) PARTITIONS 3;

七、 存储过程 & 触发器

7.1 存储过程

存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。(*)

存储过程是为了完成特定功能的 SQL语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。

存储过程**上很简单,就是数据库 SQL 语言层面的代码封装与重用。

优点:

  • 存储过程可封装,并隐藏复杂的商业逻辑。
  • 存储过程可以回传值,并可以接受参数。
  • 存储过程无法使用 SELECT 指令来运行,因为它是子程序,与查看表,数据表或用户定义函数不同。
  • 存储过程可以用在数据检验,强制实行商业逻辑等。
    缺点:
  • 存储过程,往往定制化于特定的数据库上,因为支持的编程语言不同。当切换到其他厂商的数据库系统时,需要重写原有的存储过程。
  • 存储过程的性能调校与撰写,受限于各种数据库系统。

存储过程的创建和调用

存储过程就是具有名字的一段代码,用来完成一个特定的功能。
创建的存储过程保存在数据库的数据字典中。
语法:

create procedure 存储过程名称(in|out|inout 参数名称 参数类型,......)
begin
过程体;
end

实例:

create procedure getStudentCount()
begin
select count(*) as num from student where classid=8;
end

查询,修改与删除

查询

查看所有存储过程状态:

SHOW PROCEDURE STATUS;

查看对应数据库下所有存储过程状态:

SHOW PROCEDURE STATUS WHERE DB='数据库名';

查看名称包含 Student 的存储过程状态:

SHOW PROCEDURE STATUS WHERE name LIKE '%Student%';

查询存储过程详细代码:

SHOW CREATE PROCEDURE 过程名;

修改

ALTER PROCEDURE 过程名 ([过程参数[,...]]) 过程体;

删除

DROP PROCEDURE 过程名;

==注意:不能在一个存储过程中删除另一个存储过程,只能调用另一个存储过程。==

调用存储过程

MySQL存储过程用 call 和过程名以及一个括号,括号里面根据需要,加入参数,参数包括输入参数、输出参数、输入输出参数调用,格式如下:

CALL存储过程名 ([过程参数[,...]])

7.2 触发器

触发器(trigger),也叫触发程序,是与表有关的命名数据库对象,是 MySQL中提供给程序员来保证数据完整性的一种方法,它是与表事件 INSET、UPDATE、DELETE相关的一种特殊的存储过程,它的执行是由事件来触发,比如当对一个表
进行 INSET、UPDATE、DELETE 事件时就会激活它执行。因此,删除、新增或者修改操作可能都会激活触发器,所以不要编写过于复杂的触发器,也不要增加过多的触发器,这样会对数据的插入、修改或者删除带来比较严重的影响,同时也会带来可移植性差的后果,所以在设计触发器的时候一定要有所考虑。

创建触发器

CREATE TRIGGER trigger_name trigger_time trigger_event ON tb_name FOR EACH ROW tri
gger_stmt

参数说明:

  • trigger_name 标识触发器名称,由用户自行指定;
  • trigger_time: 标识触发时机,取值为 BEFORE 或 AFTER;
  • trigger_event: 标识触发事件,取值为 INSERT、UPDATE 或 DELETE;
  • tb_name: 标识建立触发器的表名,即在哪张表上建立触发器;
  • trigger_stmt: 触发器程序体,可以是一句 SQL语句,
    或者用BEGIN 和 END 包含的多条语句,与存储过程类似。
  • FOR EACHROW: 表示任何一条记录上的操作满足触发事件都会触发该触发器。

触发事件类型:

事件 描述
INSERT型触发器 插入某一行时激活触发器,可能通过 INSERT、LOADDATA、REPLACE 语句触发
UPDATE型触发器 更改某一行时激活触发器,可能通过 UPDATE 语句触发
DELETE型触发器 删除某一行时激活触发器,可能通过 DELETE、REPLACE语句触发

我们想要使班级表中的班内学生数随着学生的添加自动更新,则采用触发器来实现
最为合适,创建触发器如下:

CREATE trigger tri_stuInsert after insert
on student for each row
BEGIN
declare c int;
set c = (select stuCount from class where classID=new.classID);
update class set stuCount = c + 1 where classID = new.classID;
END

从需求我们可以得知,班内学生数的变化是在插入学生记录之后发生的,所以创建的触发器类型为 after insert 类型。

查看触发器

SHOW TRIGGERS;

删除触发器

DROP TRIGGER [IF EXISTS] trigger_name

触发器执行顺序

日常开发中创建的数据库通常都是 InnoDB 数据库,在数据库上建立的表大都是事务性表,也就是事务安全的,这时触发器的执行顺序主要是:

  1. 如果 BEFORE 类型的触发器执行失败,SQL 无法正确执行。
  2. 如果 SQL 执行失败时,AFTER 类型的触发器不会触发。
  3. 如果 AFTER 类型的触发器执行失败,数据会回滚。

如果是对数据库的非事务表进行操作,当触发器执行顺序中的任何一步执行出错,那么就无法回滚了,数据可能会出错。

八、 MySQL 函数

8.1 运算函数

  • abs(x) : 返回 x 的绝对值
  • round(x,y) : 返回参数 x 的四舍五入的有 y位小数的值
  • mod(x,y) : 返回 x/y的模(余数)
  • greatest(x1,x2,...,xn) : 返回集合中最大的值
  • least(x1,x2,...,xn) : 返回集合中最小的值

8.2 字符串函数

  • trim(str) :去除字符串首尾两端的空格
  • upper(str) : 字符串转大写
  • concat(s1,s2...,sn) : 将 s1,s2...,sn 连接成字符串
# concat
insert into tbl_name values( concat('abc','def') );

8.3 日期函数

  • year(date) : 返回日期 date的年份(1000~9999)
  • month(date) : 返回 date 的月份值(1~12)
  • day(date):返回 date 的日(1~31)
  • curdate() : 返回当前的日期
  • week(date) : 返回日期 date为一年中第几周(0~53)
  • now() : 返回当前的日期和时间
  • curtime() : 返回当前的时间
  • hour(time) : 返回 time的小时值(0~23)
  • minute(time) : 返回 time的分钟值(0~59)

8.3 聚合函数

  • count(col) : 统计记录的条数
  • sum(col) : 求和
  • avg(col) : 求平均值
  • max(col): 求最大值
  • min(col) : 求最小值
# count 统计总记录数
select count(*) from tbl_name;
# sum 年龄总和
select sum(age) from tbl_name;
# avg 平均年龄
select avg(age) from tbl_name;
# 最大年龄
select min(birthday) from tbl_name; # 日期最小的

FOUND_ROWS()

FOUND_ROWS 函数配合 SQL_CALC_FOUND_ROWS 用于获取总记录数(忽略limit)

select SQL_CALC_FOUND_ROWS * from qa_list limit 3;
select FOUND_ROWS();

第一个 sql 里面的 SQL_CALC_FOUND_ROWS 不可省略,它表示需要取得结果数,也是后面使用 FOUND_ROWS()函数的铺垫。
FOUND_ROWS()返回的结果是临时的。如果程序往后会用到这个数字,必须提前它保存在一个变量中待用。

FOUND_ROWS()与 count()的区别:
1、当 SQL限制条件太多时, count()的执行效率不是很高,最好使用 FOUND_RO
WS()
2、当 SQL查询语句没有 where等条件限制时,使用 count()函数的执行效率较高。

九、 数据库备份

9.1 备份数据

mysqldump -u [用户名] -p [密码] [数据库名] > [path]/[名称].sql

9.2 恢复数据

mysql>source c:\system.sql

..................

JavaScript 防抖与节流的实现

一、防抖

1. 认识防抖

防抖:在第一次触发事件时,不立即执行函数,而是给出一个限定值,比如200ms,然后:

  • 如果在200ms内没有再次触发事件,那么执行函数
  • 如果在200ms内再次触发函数,那么当前的计时取消,重新开始计时

应用场景:

  • 输入框中频繁的输入内容,搜索或者提交信息
  • 频繁的点击按钮,触发某个事件
  • 监听浏览器滚定事件,完成某些特定操作
  • 用户缩放浏览器的resize 事件

效果:如果短时间内大量触发同一件事,只会执行一次函数。

2.防抖函数的实现

防抖函数的核心思路如下:

  • 当触发一个函数时,并不会立即执行这个函数,而是会延迟(通过定时器来延迟函数的执行)
    • 如果在延迟时间内,有重新触发函数,那么取消上一次的函数执行(取消定时器);
    • 如果在延迟时间内,没有重新触发函数,那么这个函数就正常执行(执行传入的函数);

接下来,就是将思路转成代码:

  • 定义debounce函数要求传入两个参数

    • 需要处理的函数fn;
    • 延迟时间;
  • 通过定时器来延迟传入函数fn的执行

    • 如果在此期间有再次触发这个函数,那么clearTimeout取消这个定时器;
    • 如果没有触发,那么在定时器的回调函数中执行即可;

自定义防抖函数:

function debounce(fn, delay) {
    var timer = null;
    return function () {
        if (timer) clearTimeout(timer);
        timer = setTimeout(function () {
            fn();
        },delay)
    }
}

3. 优化参数和this

假如我们需要获取input框中输入的值,如何获取?我们知道在oninput事件触发时会有参数传递,并且触发的函数中this是指向当前的元素节点的

  • 目前我们fn的执行是一个独立函数调用,它里面的this是window,我们需要将其修改为对应的节点对象,而返回的function中的this指向的是节点对象;

  • 目前我们的fn在执行时是没有传递任何的参数的,它需要将触发事件时传递的参数传递给fn,而我们返回的function中的arguments正是我们需要的参数;

所以我们的代码可以进行如下的优化:

function debounce(fn, delay) {
  var timer = null;
  return function() {
    if (timer) clearTimeout(timer);
    // 获取this和argument
    var _this = this;
    var _arguments = arguments;
    timer = setTimeout(function() {
      // 在执行时,通过apply来使用_this和_arguments
      fn.apply(_this, _arguments);
    }, delay);
  }
}

二、节流

1. 认识节流

节流:

  • 如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不在工作,直至过了这段时间才重新生效

效果:

  • 在某个时间内(比如500ms),某个函数只能被触发一次

应用场景:

  • 监听页面的滚定事件

  • 鼠标移动事件

  • 搜索框input事件,要支持实时搜索可以使用节流方案

  • 用户频繁点击按钮操作

  • 游戏中的一些设计

总结:同样是密集的事件触发,但是这次密集事件触发的过程,不会等待最后一次才进行函数调用,而是会按照一定的频率进行调用。

2. 节流的实现

节流函数的默认实现思路采用时间戳的方式来完成:

  • 我们使用一个last来记录上一次执行的时间

  • 每次准备执行前,获取一下当前的时间now:now - last > interval,函数执行,并且将now赋值给last即可。

基础版:

    function throttle(fn, interval) {
        var last = 0;
        return function () {
            // this和arugments
            var _this = this;
            var _arguments = arguments;
            var now = new Date().getTime();
            if (now - last > interval) {
                fn.apply(_this, _arguments);
                last = now;
            }
        }
    }

优化最后执行:

默认情况下,我们的防抖函数最后一次是不会执行的

  • 因为没有达到最终的时间,也就是条件now - last > interval满足不了的
  • 但是,如果我们希望它最后一次是可以执行的,那么我们可以让其传入对应的参数来控制

代码实现:

    function throttle(fn, interval) {
        var last = 0;
        var timer = null; // 记录定时器是否已经开启
        return function () {
            // this和arguments;
            var _this = this;
            var _arguments = _arguments;
            var now = new Date().getTime();
            if (now - last > interval) {
                if (timer) { //若已经开启,则不需要开启另外一个定时器了
                    clearTimeout(timer);
                    timer = null;
                }
                fn.apply(_this, _arguments);
                last = now;
            } else if (timer === null) { // 没有立即执行的情况下,就会开启定时器
                //只是最后一次开启
                timer = setTimeout(function () {
                    timer = null; // 如果定时器最后执行了,那么timer需要赋值为null
                    fn.apply(_this, _arguments);
                },interval)
            }
        }
    }

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.