360前端星计划—正则/NODEJS

415 阅读11分钟

RegExp

1 正则的简单介绍

1.1 匹配

匹配模式,要不匹配字符,要不匹配位置。而正则表达式的强大在于实现模糊匹配,横向模糊和纵向模糊。

1.1.1 横向模糊匹配

一个正则可匹配的字符串长度不是固定的,而是多种情况的。量词[+ ? *] 表示1个或者多个;0个或者1次,任意次;{m,n}表示连续出现最少m次,最多n次。

1.1.2 纵向模糊匹配

纵向模糊指的是一个正则匹配的字符串,具体到某一位时,可以是不确定的字符串,有多种可能。[abc] 表示一个字符可以是a,b或者c。

1.1.3 字符组

上面提到的[abc]就是一个字符组,字符组可以有很多功能

  • 范围表示法:[1-6], 若想匹配-,顺序必须调换,[-16]
  • 排除字符组:就是反向纵向模糊匹配,反正不可以是这些字符串,[^abc], 也可以用在范围表示法
字符组简写
  • \d[0-9], \D[^0-9]
  • \w就是[0-9a-zA-Z_] \W[^0-9a-zA-Z_] w就是word
  • \s[ \t\v\n\r\f]\S[^ \t\v\n\r\f]
  • .就是[^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外
  • 匹配任意字符串,可以使用[\d\D][\w\W][\s\S][^]中任何的一个

1.1.4 量词

  • {m,} 表示至少出现m次。
  • {m} 等价于{m,m},表示出现m次。
  • ? 等价于{0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗?
  • + 等价于{1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。
  • * 等价于{0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。

1.1.5 惰性匹配

默认下匹配是惰性匹配,如果加了全局标识符就是贪婪匹配,尽可能找出最多的匹配模式,而惰性匹配就是找到第一个就停手,加一个问号就可以。如果只是希望匹配数量的话,大可不必使用问号,更改全局标识符即可。

var regex = /\d{2,5}?/g;

惰性匹配在对数量有要求的较复杂环境下有特别的作用,比如上面的例子,我们是希望找到2个就停手,还是尽可能多的匹配到再停手?这个时候问号标识符就排上用场了。加个问好可以保证匹配到两个就停手。

1.2 多选分支

使用管道符号 | 可以表示多选,和或者or 的表达符很像。多选分支反而是默认惰性的,即使一个完整的字符串,匹配上第一个就立即结束并返回匹配上的字符串。 处理分支还可以使用捕获组配合横向模糊匹配。

1.3 位置匹配攻略

1.3.1 什么是位置

位置指的是相邻字符之间的位置。

1.3.2 如何匹配位置

ES5中有六个锚字符

^ $ \b \B (?=p) (?!p)

下面是例子

匹配开头和结尾

^ 匹配开头,多行匹配时匹配每行的开头 $ 匹配结尾,多行匹配时匹配每行的结尾

var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
console.log(result);
/*
#I#
#love#
#javascript#
*/
单词边界

\b是单词边界,\w\W的边界,单词与非单词的边界,也包括了字符与开头,结尾的边界。

var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result); // => "[#JS#] #Lesson_01#.#mp4#"

\B则是上面的反面,

你瞅啥

(?=p) (?!p) (?<=p) (?<!p) 这四者表示四个位置,分别是positive lookahead, negative lookahead, positive lookbehind, negative lookbehind.

这几个可以匹配相关的位置,具体就是某个字符的前面,后面,除了某个字符前/后的所有地方,包括开头结尾。

1.4 位置的特性

位置可以理解成空字符串"",且开头和结尾可以有无数个空字符串。 因此,把/^hello$/写成/^^hello?$/,是没有任何问题的:

1.4.1 案例

  • 不匹配任何东西的正则 /.^/
  • 数字的千位分割表示法 ( /(?=(\d{3})+$)/g ,'#' )
  • 千位分割的高级形式,上面的方法在正好是3的倍数的情况下会在开头有一个逗号。因此不能是开头,要加上(?!^) ( /(?!^)(?=(\d{3})+$)/g ,'#' )

1.4.2 密码判断,6-12位,数字,小写字符,大写字符,至少2种

1.4.3 判断是否包含某一种字符

假设,要求的必须包含数字,怎么办?此时我们可以使用(?=.*[0-9])来做

var reg = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是数字
console.log( reg.test("abcdef") ); // false 全是小写字母
console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
console.log( reg.test("abcdEF234") ); // true 三者都有

1.5 括号的使用

1.5.1 分组和分支结构

括号主要提供分组功能,而括号加或者符,可以进行分支结构的选择。

1.5.2 引用分组

可以使用RegExp.$1 或者在string.replace(regex, "$2$3$1") 指代最近定义一个regex。

var regex = /(\d{4})-(\d{2})-(\d{2})/; var string = "2017-06-12"; var result = string.replace(regex, "$2/$3/$1"); 
console.log(result); 
// => "06/12/2017"


var result = string.replace(regex, function() { return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1; });

1.5.3 反向引用

只能引用已经出现了的,使用\num的格式引用之前出现过的第几个分组,从1开始。

var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false

比如上面的例子,保持日期匹配的分隔符一致

1.5.4 括号嵌套

括号嵌套下,以左括号出现的次序判断,

var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3

1.5.5 \10?

能正确表达10的意思

1.5.6 引用不存在的分组

会进行匹配,匹配最原本的意思。

1.6 非捕获分组

(?:ab)这种分组不会被api捕获

相关案例

  • 去开头结尾空白符, str.replace(/^\s+|\s+$/g, '')

  • 第二种,提取方法 str.replace(/^\s*(.*?)\s*$/g, '$1' )
    第二种方法使用了惰性匹配,不然会一致匹配到最后一个空格之前。

  • 将每个单词首字母大写,我们要匹配的是首字母,有两种可能,前面是开头或者空格/()/g

2 创建正则

2.1 正则字面量 const reg = /[a-z]/i

优点:

  • 简单方便
  • 不需要考虑二次转义 缺点:
  • 子内容无法重复使用
  • 过长可能导致可读性差

2.2 使用RegExp

const s1 = '[a-z]'
const reg = new RegExp(`${s1}\\d+${s1}`,i)

优点:

  • 子内容可以重复使用
  • 动态创建
  • 控制子内容的粒度提高可读性 缺点:
  • 二次转义的问题容易导致bug

3 用法

RegExp.prototype.test()

  • 输入要求字符串,如果类型住转换失败,会抛出TypeError
  • 输出true/false 表示匹配结果

RegExp.prototype.source & RegExp.prototype.flags

  • 前者获得当前正则对象的模式文本字符串
  • 后者获得当前正则对象的修饰字符串,比如i,g,返回按照字母升序

RegExp.prototype.exec & String.prototype.match()

  • 前者输入字符串,后者输出正则表达式
  • 匹配成功返回匹配结果,否则null
  • 当全局匹配(g)时,前者每次只返回一个匹配结果,多次调用才能返回所有匹配结果;后者会返回所有匹配结果,是一个字符串数组。
  • match返回格式不固定,推荐使用前者

RegExp.prototype.lastIndex

  • 主要是配合exec使用,返回最后一次匹配成功的位置(下一次匹配开始的位置),匹配失败归0。
const reg = /(a)/g;
const str = 'a1a';

reg.lastIndex; //0
reg.exec(str); //
reg.lastIndex; //1
reg.exec(str); //
reg.lastIndex; //3
reg.exec(str); //
reg.lastIndex; //0 失败了
reg.exec(str); //

String.prototype.replace()/search()/split()

  • replace 替换所有匹配到的文本换成自定义文本
  • search 返回第一次匹配到的文本开头在字符串中的index
  • split 将匹配到的文本移除,返回一个字符串数组,注意开头结尾空字符串

4 场景

4.1 数值判断&解析

字符集纵向匹配

  • [0-9] 匹配数字 等于\d
  • [a-zA-Z] 匹配字母
  • [\u4e00-\u9fa5] 匹配汉字
  • [xyz123] 自定义

如何匹配数字?

  • 要求全匹配 使用^$匹配开头
  • 要求正负号的处理 [+-]?允许有无符号
  • 要求小数的处理 /^[+-]?\d+(\.\d+)?$/.匹配除了换行符之外的所有字符,\s匹配换行符,这里要转义)
  • 要求处理无整数部分的小数 /^[+-]?(\d+)?(\.\d+)?$/
  • 捕获组的额外开销,(?:)创建一个非捕获组 /^[+-]?(?:\d*\.)?\d+?$/
  • 要求匹配无小数部分的数字 比如2./^[+-]?(?:\d*\.)?\d+?(?:\.)?$/
  • 要求处理科学计数法 /^[+-]?(?:\d+\.?|\d*\.\d+)(?:e[+-]?\d+)?$/i

解析CSS数值

没有2.0这种写法,其余情况都要考虑,还要考虑后缀单位

reg = `/[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?=px|\s|$)/ig`
function execNumberList(str){
    reg.lastIndex = 0 //手动归零
    let exec = reg.exec(str);
    cosnt result = []
    while(exec){
        result.push(parseFloat(exec[0]));
        exec = reg.exec(str);
    }
    return result;
}
console.log(execNumberList('1.0px .2px -3px') //[1,0.2,-3]
console.log(execNumberList('-1e+1px') //[10]

这里使用了(?=expression) 先行断言。 用于匹配位置,表示该位置应该有这些字符,但是并不会获取到这些字符。 与之类似,还有先行否定断言,后行断言,后行否定断言。

数值转货币

reg = `/(\d)(?=(\d{3})+(,|$))/g`
function execNumberList(str){
    return str.replace(reg,'$1,')
}
console.log(execNumberList('1') //1
console.log(execNumberList('12345678') // 12,345,678

这里使用捕获组加先行断言的方式确定要加逗号的位置,然后全局匹配并替换。$1表示第一个捕获组。$&表示完整匹配。注意非捕获组断言里面的小括号也会生成捕获组。

4.2 正则与颜色

有以下几种情况

color: #rrggbb;
color: #rgb;      //16进制缩写
color: #rrggbbaa; //有alpha透明度
color: #rgba      //有alpha的缩写

正则可以写成如下格式

const hex = [0-9a-f];
const reg = new RegExp(`^(?:#${hex}{6}|#${hex}{8}|#${hex}{3,4})$`,'i');

直接匹配数字字母出现3,4,6,8次即可。

还有rgb表示法:

color: rgb(r,g,b)
color: rgb(r%,g%,b%,a%)      
color: rgba(r,g,b,a) 
color: rgba(r%,g%,b%,a)
color: rgba(r,g,b,a%)
color: rgba(r%,g%,b%,a%)

对应的正则

const num = '[+-]?(?:\\d*\\.)?\\d+(?:e[+-]?\\d+)?';
const comma = '\\s*,\\s*';
const reg = new RegExp(`rgba?\\(\\s*${num}(%?)(?:${comma}${num}\\1)[2](?:${comma}${num}%?)?\\s*\\)`)
  • \n 反向引用,表示引用第n个捕获组
  • \s 字符集缩写用于匹配空白
  • 捕获组内容可选,问号在捕获组内,可以使用嵌套捕获组如果圆括号不可省略

缩写的转化:

//我的答案
const hex = '[0-9a-z]'
const reg = new RegExp( `/^#(${hex})\1(${hex})\2(${hex})\3(?:(${hex})\4)?/i` )
function shortenColor(str){
    let result = reg.exec(str);
    let res ='#'
    if(!result[0]){
        return str;
    }else{
        result.shift();
        res = res + result.join("");
    }
    return res;
}
const hex = '[0-9a-z]'
const hexreg = new RegExp( `^#(?<r>${hex})\\k<r>(?<g>${hex})\\k<g>(?<b>${hex})\\k<b>(?<a>${hex}?)\\k<a>$`,'i');
function shortenColor(str){
    return str.replace(hexreg,'#$<r>$<g>$<b>$<a>')
}

console.log(shortenColor('#336600') //#360
console.log(shortenColor('#19b955') // #19b955
console.log(shortenColor('#33660000') // #3600

教程答案使用了具名捕获组,(?<key>),反向引用使用\k<key>,而在replace中,使用$<key>来访问具名捕获组。 当应用exec时,具名捕获组可以通过execResult.groups[key]访问。

4.3 正则与URL

const protocol = '(?<protocol>https?:)';
const host = '(?<host>(?<hostname>[^/#?:]+)(?::(?<port>\\d+))?)'
const path = '(?<pathname>(?:\\/[^/#?]+)*\\/?)';
const search = '(?<search>(?:\\?[^#]*)?)';
const hash = '(?<hash>(?:#.*)?)';
const reg = new RegExp(`^${protocol}\/\/${host}${path}${search}${hash}$`);
function execURL(url){
    const result = reg.exec(url);
    if(result){
        result.groups.port = result.groups.port || ""; //考虑undefined的情况
        return result.groups;
    }
    return {
        protocol:'',host:'',hostname:'',port:'',
        pathname:'',search:'',hash:'',
    }; 
}

解析search和hash(腾讯面试原题)

function execUrlParams(str){
    str = str.replace(/^[#?&]/,"");
    const result  = {}
    if(!str){ // 可能匹配全空字符串,要特殊处理
        return result;
    }
    const reg = /(?:^|&)([^&=]*)=?([^&]*?)(?=&|$)/y;
    let exec = reg.exec(str);
    while(exec){
        result[exec[1]] = exec[2];
        exec = reg.exec(str);
    }
    return result;
}
console.log(execUrlParams('#')); // {}
console.log(execUrlParams('##')); // {'#':''}
console.log(execUrlParams('?q=360&src=srp')); // {q:'360',src:'srp'}
console.log(execUrlParams('test=a=b=c&&==&a=')); // {test:'a-b-c','':'=',a:''}

reg可能是以&开头,这是第一个非捕获组,然后是获取key,key里面不能有^=&并且任意长度,之后的等号也可能是没有的,考虑到两个&&之间是个空键值对。 [^&]*?表示任意长度匹配但是非贪婪,这里要和后面的先行断言结合,意思是第一个断言匹配到的地方就是我们想要的。但是这里似乎不影响,因为后面有y修饰符

y修饰符是ES6新增的粘连修饰符,是全局匹配但是:

  • 每次匹配结果是连续的
  • match时只会返回第一个匹配结果

练习

function getUrlParams(str,key){
    str = str.replace(/^[#?&]/,"");
    if(!str){ // 可能匹配全空字符串,要特殊处理
        return "";
    }
    const reg = new RegExp(`(?:^|&)${key}=([^&]*?)(?=&|$)`,'g');
    let exec = reg.exec(str);    
    let result;
    while(exec){
        result = exec?exec[1]:""
        exec = reg.exec(str);  
    }

    return result;
}

NODEJS

介绍

与JS的区别

  • 基于异步的IO接口
  • 基于node_modules 和require的模块依赖
  • 提供C++ addon API与系统交互

可以干什么?

  • WEB服务端
  • CLI命令行
  • GUI客户端
  • IOT

内置模块

  • http
  • fs
  • net
  • process
  • path
const fs = require('fs');
const {readFile} = require('fs');
fs.readFile(path,callback)//是异步的哦

文件模块

//import
const circle = require('./circle.js');
console.log(circle.area())

//define
exports.area = function(){}
exports.circumference = function(){}

//package name
require('pkg-name')
//从当前目录依次往上查找目标文件

模块类型

  • .js
  • .json 引入是一个对象
  • .node C++编译生成的文件
  • .mjs 新格式,基于ES6 module
  • ...

模块路径查找

  • 绝对路径
  • 相对路径 和当前路径处理过后变成绝对路径
  • 模块/文件夹
    • 原始模块直接读取缓存
    • 从当前路径以此往上查找
    • 解析package.json 查找main属性,没有则使用index.js
  • 未找到 报错

js模块解析

const circle = require('./circle.js');
  • require 并不是全局变量,是怎么引入的?
  • circle变量会污染其他文件吗?

解析过程

  1. 通过fs.readFileSync同步拿到文件内容
  2. 对内容进行包装,在闭包内部有单独的作用域,不会污染全局
(function(exports, require, module, __filename, __dirname){
    var circle = require('./circle.js');
    console.log('The area is ' + circle.area(4));
})
  1. 通过vm.runInThisContext执行
  2. 获取module对象的值作为模块的返回值

模块缓存

  • 模块加载后会将返回值缓存起来
  • 下次加载时直接读取缓存内容,避免文件IO和解析时间
  • 导出对象缓存在Module._cache对象上

Node Package Manager

  • 一个package.json文件应该存在于包顶级目录之下
  • 二进制文件应该在bin目录下
  • javascript代码应该在lib目录下
  • 文档应该在doc目录下
  • 单元测试在test目录下

可以使用npm init -y的命令创建一个package.json

依赖

  • dependencies 一个包需要其他包才能运行,放在dependencies下面
  • devDependencies 仅仅是开发的时候使用,放在devDependencies,比如测试工具
  • peerDependencies 装一个包必须安装放在peerDependencies下面的包,比如一个依附于特定框架的插件
  • bundledDependencies 捆绑安装
  • optionalDependencies 安装一个包的时候可以选择另一个包

samver

1.0.0三位分别代表大版本,中版本,小版本。小版本一般是bug的修复,中版本一般是特性的增加,大版本是整体的重构,可能向下不兼容。

registry

包依赖版本写法分别代表了什么?

  • ^1.2.2 允许中版本和小版本的更新
  • ~0.5.0 允许小版本的更新
  • *接受任何更新
  • 0.0.2 只接受这一个版本
  • , >= 的意思就是字面意思

开发应用小栗子

WEB服务器

const http = require('http');
const server = http.createServer((req,res)=>{
    res.end('hello world');
});
server.listen(3000);

ToDoList 实战

数据表

使用自增的id作为主键,desc是显示的内容。

RESTful 接口规范

  • 每个API都对应一种资源或资源集合
  • 使用HTTP Method表示对资源的动作
  • 使用HTTP Status Code表示资源操作结果

RESTful API 设计

  • GET /ticket 获取ticket列表
  • GET /ticket/:id 查看具体ticket
  • POST /ticket 新建一个ticket
  • PUT /ticket/:id 更新id为12的ticket
  • DELETE /ticket/:id 删除id为12的ticket

脚手架创建

三个文件分别是REST基类,ticket路由文件和逻辑校验层的文件。然后要配置路由。
RESTful API会将具体的method和资源映射到controller里面具体的action,get映射到getAction,post映射到postAction。

以基类的getAction为例,就是简单的返回数据,如果有id就根据id查询

async getAction(){
    let data;
    if(this.id){
        const pk = this.modelInstance.pk;
        data = await this.modelInstance.where({[pk]:this.id}).find();
        return this.success(data);
    }
    data = await this.modelInstance.select();
    return this.success(data);
}

数据库配置

exports.model = {
  type: 'mysql',
  common: {
    logConnect: isDev,
    logSql: isDev,
    logger: msg => think.logger.info(msg)
  },
  mysql: {
    handle: mysql,
    database: 'todo',
    prefix: '',
    encoding: 'utf8',
    host: '127.0.0.1',
    port: '',
    user: 'root',
    password: 'root',
    dateStrings: true
  }
};

数据校验

  • 使用logic层专门支持数据校验,非业务功能
  • 文件和Action与Controller一一对应
  • 使用配置代替逻辑
// src/logic/ticket.js
module.exports = class extends think.Logic {
  getAction() {
    this.rules = {
      id: {
        int: true
      }
    };
  }
  deleteAction() {
    this.rules = {
      id: {
        int: true,
        required: true,
        method: 'get'
      }
    };
  }
  putAction() {
    this.rules = {
      id: {
        int: true,
        required: true,
        method: 'get'
      },
      status: {
        int: true,
        required: true
      },
      desc: {
    ```