正则表达式在JS中的应用
RegExp内置对象详解
MDN-JS-RegExp 中文文档
有几个正则符号需要特别注意
.符号 并不意味着匹配所有字符, 换行符会排除在外的,例如: \n \r \u2028 或 \u2029
?符号 如果在数量词 *、+、? 或 {}, 任意一个后面紧跟该符号?,会使数量词变为非贪婪( non-greedy) ,即匹配次数最小化。反之,默认情况下,是贪婪的(greedy),即匹配次数最大化。
分组符号 主要分为(pattern) (?:pattern)这两种,一个可以获取匹配结果,一个只做匹配不获取结果
\n符号 其中n是一个正整数。一个反向引用(back reference),指向正则表达式中第 n 个括号(从左开始数)中匹配的子字符串。
前向断言符号 在大部分浏览器中,js只支持前向断言,即(?=pattern)、(?!pattern) 这两种方式,而后向断言目前只发现Chrome浏览器提供了支持。前向断言主要用于复杂数据的匹配,同样不会获取匹配结果。
正则中的分组
正则中分组都是用()符号包裹的一个正则表达式来体现的,大致上可以分为两种,一是捕获组,另一个则是分捕获组。捕获组的意思就是匹配括号中的表达式并且匹配的结果将放在match数组中,非捕获组则是匹配括号中的表达式但是不会将匹配的结果将放在match数组中
捕获组与反向引用
捕获组只有有一种情况,即(pattern),其中pattern是一个正则表达式,将一个捕获组嵌入在一个正则表达式中,将可以在匹配结果中的match数组中获取这一结果。捕获组的顺序可以通过从左到右计算其开括号来编号,并且在任何情况下,顺序都不会发生改变,并且第一组结果一定是整个正则表达式的匹配结果
例如,在这样一个正则表达式中/((A(B(C))(D))/, 去匹配ABCD这个字符串。大家可以先思考下,在这种情况下,如果匹配成功时,一共将分为多少组,分组的顺序又是怎么样呢?
我们按照上面分组规则,最后得到如下分组结果
下标 | 分组结果 | 匹配结果 |
---|---|---|
0 | ((A(B(C))(D)) | ABCD |
1 | (A(B(C))(D) | ABCD |
2 | (A) | A |
3 | (B(C)) | BC |
4 | (C) | C |
5 | (D) | D |
之所以这样命名捕获组是因为在匹配中,保存了与这些组匹配的输入序列的每个子序列。捕获的子序列稍后可以通过 Back 引用(反向引用) 在表达式中使用,也可以在匹配操作完成后从匹配器检索。既然说到反向引用, 我自己的理解就是对于前面某个分组的引用,反向引用的匹配结果一定与被引用的那个组的匹配结果相同,并且不会再重新分组,不能单纯理解为分组的拷贝。当然,反向引用是不可能引用整个表达式的,所有反向引用的下标必须大于0。
例如,有这样一个带反向引用的正则表达式/hello (Leo|Alan),\sgoodbye \1!/, 它可以匹配'hello Leo, goodbye Leo!',但是不能匹配'hello Leo, goodbye Alan!'。也就是,这个正则表达式实质上是先获取Leo这一结果,再匹配goodbye之后的Leo,而不是直接匹配(Leo|Alan)
非捕获组
非捕获组主要分为非获取匹配(?: pattern)和断言,其中非获取匹配会匹配但不获取匹配结果,但不会把匹配结果分组,但却会出现在外层正则的匹配结果中。而断言则是对正则的修饰,不匹配也不获取匹配结果,同样也不会把匹配结果分组。但是不同于非获取匹配,其匹配结果不会会出现在外层正则的匹配结果中。
例如,/A(?:B)/ 和/A(?=B)/, 两者匹配AB字符串都会匹配成功,但是第一个匹配的是AB,第二个则只是匹配A
断言主要分为前向断言和后向断言,由于历史原因,JS的作者没有考虑正则的后向断言,所以目前大部分浏览器中都不支持后向断言,但在其他语言中是支持的,所以还是在此说明一下。
不管是前向断言还是后向断言,都分为正向预查和负向预查,区别是一个是匹配后面,一个是匹配前面,可以用来做符合特定规则的字符匹配
字符串的replace方法的高级使用
语法 str.replace(regexp|substr, newSubStr|function)
第一个参数为字符串或者正则表达式, 如果想要替换全部结果,必须加上/g
第二个参数会替换的字符串或者一个函数
这里直接贴上MDN的Demo代码
在 replace() 中使用正则表达式
var str = 'Twas thexmas night before Xmas...';
var newstr = str.replace(/xmas/i, 'Christmas');
console.log(newstr);
使用字符串作为参数
变量名 | 代表的值 |
---|---|
$$ | "$"符号转义 |
$& | 插入匹配的子串 |
$` | 插入当前匹配的子串左边的内容 |
$' | 插入当前匹配的子串右边的内容 |
$n | 插入第 n 个括号匹配的字符串 |
将华氏温度转换为对等的摄氏温度
function f2c(x)
{
function convert(str, p1, offset, s)
{
return ((p1-32) * 5/9) + "C";
}
var s = String(x);
var test = /(\d+(?:\.\d*)?)F\b/g;
return s.replace(test, convert);
}
使用行内函数和正则来避免循环
var str = 'x-x_';
var retArr = [];
str.replace(/(x_*)|(-)/g, function(match, p1, p2) {
if (p1) { retArr.push({ on: true, length: p1.length }); }
if (p2) { retArr.push({ on: false, length: 1 }); }
});
console.log(retArr);
高阶函数的应用
高阶函数的定义
通常的编程语言中,函数的参数只能是基本类型或者对象引用,返回值也只是基本数据类型或对象引用。但在Javascript中函数本质上来说也是对象,所以可以当做参数也可以被当做返回值返回。所谓高阶函数就是可以把函数作为参数,或者是将函数作为返回值的函数。
JS内置高阶函数
在JavaScript一些内置对象,存在着大量的高阶函数,我们比较常用便有Array类map、filter、forEach、sort,所以下文将会着重说明Array类的部分高阶函数的一些应用。
使用filter和sort来对数据进行去重和排序
在JavaScript中,Array并没有提供distinct之类的操作函数,所以如果想要去重数据,必须去对数组遍历,然后在手动去重数据。这些去重数据的话,代码会比较长,也比较难懂。其实,我们可以通过filter这个高阶函数来简化自己的代码,以一种比较优雅的方式来对复杂数据进行去重。
我们假定某个学校的数据库中存在着这样一些数据,里面有些数据过时了,需要去重并且重新排序处理。
// 数据源
let array = [
{name: 'Bill', age: 15},
{name: 'Bill', age: 18},
{name: 'Colin', age: 18, describe: 'Boy'},
{name: 'Colin', age: 18, describe: 'Girl'},
{name: 'Alan', age: 15},
{name: 'Alan', age: 15, describe: 'Boy'},
{name: 'Bill', age: 18},
{name: 'Colin', age: 12, describe: 'Boy'},
{name: 'Alan', age: 18, describe: 'Boy'}
];
// 期望去重、排序结果
[ { name: 'Alan', age: 18, describe: 'Boy' },
{ name: 'Bill', age: 18 },
{ name: 'Colin', age: 18, describe: 'Boy' } ]
我们唯一可以确定的是,name和age字段一定存在,类型一定是string和number类型,describe字段可能存在。
去重数据的时候要符合三个规则:
一、只对name字段完全一样的数据进行去重
二、如果name相同,age不一样,取age大的
三、在name和age相同的情况下,如果存在describe字段,则取存在describe字段的那条数据,如果都存在的话就按ascii码,取小的。
排序规则:name字段按ascii码排序
通常情况下,如果不使用高阶函数, 我们可能会按照下面这种方式来对数据去重
let distinct = [];
function getDataByName(name) {
for (let i = 0; i < distinct.length; i++) {
if(distinct[i].name === name){
return distinct[i]
}
}
}
function setData(data) {
for (let i = 0; i < distinct.length; i++) {
if(distinct[i].name === name){
distinct[i] = data
return
}
}
}
for (let i = 0; i < array.length; i++) {
let data = array[i];
let existData = getDataByName(data.name)
if(existData){
if(data.age > existData.age){
setData(data)
}else if(data.age === existData.age){
if(data.describe && data.describe < existData.describe){
setData(data)
}
}
}else {
distinct.push(data)
}
}
distinct = distinct.sort((a, b) => a.name > b.name);
console.log('suming-log', distinct);
最后调试运行,可以发现去重数据成功了,完全符合规则但是这样做,但是却有几个很明显的缺点
- 代码量大,不易理解和阅读
- 时间复杂度较高
- 如果修改了去重规则,不易维护
如果我们换一种思路,其实我们可以先对数据进行排序,把name相同的放在一起,越符合规则的排在前面,最后进行去重,只取最上面的一条数据。这样做的话,我们就可以通过sort和filter这两个高阶函数来对数据进去去重排序了, 例如下面代码:
function distinctByKey(key) {
let valueSet = new Set();
return (item) => !valueSet.has(item[key])
&& valueSet.add(item[key])
}
function sortData(a, b) {
if(a.name === b.name){
if(a.age < b.age){
return 1
}else if(a.age === b.age){
if(a.describe && a.describe > b.describe){
return 1
}
}
return 0
}else {
return a.name > b.name ? 1: -1
}
}
let distinct = array.sort(sortData).filter(distinctByKey('name'));
console.log('suming-log', distinct);
虽然代码量并没有减少太多,但是可以看出来思路清晰了很多。不需要向上面一样去各种遍历找数据来做比较,如果修改了去重规则,我们只需要在sortData方法内做相应修改即可,方便了代码的维护。当然这样做其实也有缺点,虽然看起来好像这样做时间复杂度会比较小,但在数据量特别大,重复的数据比较多的情况下,先排序再去重反而可能会更花费更多的时间。
使用reduce对数据进行合并
当我们想一组数据合并或者分类时,使用reduce函数是最为高效和简洁的方式,如下,可以对于相同的grade进行分类
let array = [
{name: 'Bill', grade: 'second'},
{name: 'Ben', grade: 'three'},
{name: 'Colin', grade: 'three'},
{name: 'Jack', grade: 'first'},
{name: 'Ken', grade: 'second'},
{name: 'Alan', grade: 'second'},
{name: 'Rock', grade: 'three'},
{name: 'Lance', grade: 'first'},
{name: 'Mark', grade: 'three'}
];
function reducer (accumulator, currentValue) {
let grade = currentValue.grade;
if(accumulator[grade]){
accumulator[grade].push(currentValue)
}else {
accumulator[grade] = [currentValue]
}
return accumulator
}
let sortArray = array.reduce(reducer, {});
console.log('suming-log', sortArray);
高阶函数实现AOP
AOP面向切面编程,其主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,然后再将这些支撑模块“动态织入” 到 另一个函数中去。例如react中的高阶组件,本质上就是高阶函数在AOP上面的应用。
Function.prototype.before = function( beforefn ){
var __self = this; // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply( this, arguments ); // 执行新函数,修正 this
return __self.apply( this, arguments ); // 执行原函数
}
};
Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
var func = function(){
console.log( 2 );
};
func = func.before(function(){
console.log( 1 );
}).after(function(){
console.log( 3 );
});
func();//1 2 3
高阶函数实现与柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
函数的柯里化有什么好处
适用性广,减少耦合,易维护
柯里化可用帮助我们很好分离数据操作和业务展示,让我们的项目更加的模块化。
控制可变因素,延迟代码执行
柯里化可以让我们不马上得到函数的最终执行结果,但可以提前进行部分计算工作,并且延迟执行得到最好结果
柯里化的一个简单例子:
let dataSource = [ 1, 2.2344, 3.2245, 4, 5.4444, 6.2222, 7 ];
// 不使用柯里化
function noCurrying(data, isSort, renderFunc) {
return (isSort ? data.sort((a, b) => a > b) : data).map((item) => {
return renderFunc(item)
})
}
// 使用柯里化
function currying(data) {
return function (isSort) {
return function (renderFunc) {
return (isSort ? data.sort((a, b) => a > b) : data)
.map((item) => {
return renderFunc(item)
})
}
}
}
// 渲染数据的方式1
function renderData1(data) {
return data.toFixed(2)
}
// 渲染数据的方式2
function renderData2(data) {
return parseInt(data)
}
// 不使用柯里化的话,所有参数必须传递才可以得到想要的渲染函数
console.log('suming-log', noCurrying(dataSource, true, renderData1));
console.log('suming-log', noCurrying(dataSource, false, renderData2));
// 使用柯里化的话,可以定制出来各种需要的渲染函数
let renderList = currying(dataSource);
console.log('suming-log', renderList(true)(renderData1));
console.log('suming-log', renderList(false)(renderData2));
let renderSortList = currying(dataSource)(true);
console.log('suming-log', renderSortList(renderData1));
console.log('suming-log', renderSortList(renderData2));
设计模式在js中的应用
观察者模式
观察者模式应用非常广泛,一个非常常见的例子便是在线聊天室。在聊天室中,聊天室负责统一发布事件,而每个成员则作为事件的订阅者来接收这个事件。观察者模式必须符合三个条件,监听事件、触发事件、移除事件。对应聊天室中,每当有成员加入这个聊天室,就必须为这个成员添加监听事件,退出时则要移除这个监听事件,聊天室则负责触发事件,每当有成员发送信息时,就遍历所有成员,并且给他们发布事件。
通过观察者模式来实现的聊天室功能
configChatRome.js
var app = require('./app')
var ChatRoom = require('./ChatRoom')
var ChatMember = require('./ChatMember')
function createRoom() {
var rome = new ChatRoom ()
app.ws('/chat', function(ws, req) {
let member;
ws.send('请输入你的昵称')
ws.on('close', function() {
rome.exit(member)
});
ws.on('message', function(msg) {
if(member){
rome.sendAll(msg)
}else {
member = new ChatMember(msg, ws)
let isJonSuccess = rome.join(member)
if(!isJonSuccess){
member = null
}
}
});
});
}
module.exports = {
createRoom
}
ChatRoom.js
function ChatRoom () {
this.members = []
this.messages = []
}
ChatRoom.prototype.join = function (member) {
let existMember = this.getMemberByName(member.nickname)
if (existMember) {
member.send('已经存在这个昵称了,请重新输入')
return false
} else {
this.members.push(member)
console.log('suming-log', member.nickname + '加入了聊天室')
this.sendAll(member.nickname + '加入了聊天室')
return true
}
}
ChatRoom.prototype.exit = function (member) {
if(!member) return
for (let i = 0; i < this.members.length; i++) {
if (this.members[i].nickname === member.nickname) {
this.members = this.members.slice(0, i).concat(this.members.slice(i + 1, this.members.length))
console.log('suming-log', member.nickname + '退出了聊天室')
this.sendAll(member.nickname + '退出了聊天室')
}
}
}
ChatRoom.prototype.sendAll = function (message) {
this.messages.push(message)
for (let i = 0; i < this.members.length; i++) {
this.members[i].send(message)
}
}
ChatRoom.prototype.getMemberByName = function (nickname) {
for (let i = 0; i < this.members.length; i++) {
if (this.members[i].nickname === nickname) {
return this.members[i]
}
}
}
module.exports = ChatRoom
ChatMember.js
function ChatMember (nickname, ws) {
this.nickname = nickname
this.ws = ws
}
ChatMember.prototype.send = function(message){
this.ws.send(message)
}
module.exports = ChatMember
代理模式
代理模式就是为其他对象提供一种代理以控制对这个对象的访问。代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西
同样以为上面的聊天室为例子,之前聊天室里面不会显示这条信息是谁发的,我们想要加强这一功能,但是不太想直接去修改ChatRoom里面的代码,因为ChatRoom的sendToAll方法是一个比较通用的方法,直接修改这个方法会让其以后哦更加难以维护。这时候我们就可以考虑使用代理模式了,创造一个新的对象去代理ChatRoom,扩展ChatRoom的sendAll方法,但绝对不会修改ChatRoom的任何代码。
例如下面这个代理对象
ProxyChatRoom.js
function ProxyChatRoom (room) {
this.room = room
}
Object.keys(ChatRoom.prototype).forEach((funcName) => {
if(funcName === 'sendAll'){
ProxyChatRoom.prototype.sendAll = function (message, nickname) {
this.room.sendAll(nickname + '说:'+ message)
}
}else {
ProxyChatRoom.prototype[funcName] = function () {
return this.room[funcName].apply(this.room, arguments)
}
}
})
module.exports = ProxyChatRoom
使用了代理模式后的configChatRome.js
var app = require('./app')
var ChatRoom = require('./ChatRoom')
var ProxyChatRoom = require('./ProxyChatRoom')
var ChatMember = require('./ChatMember')
function createRoom() {
var rome = new ProxyChatRoom(new ChatRoom ())
app.ws('/chat', function(ws, req) {
let member;
ws.send('请输入你的昵称')
ws.on('close', function() {
rome.exit(member)
});
ws.on('message', function(msg) {
if(member){
rome.sendAll(msg, member.nickname)
}else {
member = new ChatMember(msg, ws)
let isJonSuccess = rome.join(member)
if(!isJonSuccess){
member = null
}
}
});
});
}
module.exports = {
createRoom
}
可以看到,使用了代理模式之后,代码并没有修改过多,这样不仅仅可以让代码更加看起来更加清晰,还可以让聊天室更加的抽象化、多元化,因为聊天室只需要处理好它最简单的命令,例如成员的加入、退出、接收信息。其他复杂的逻辑完全可以通过代理对象来实现,大大降低了代码的维护难度。
相关代码已经部署至服务器 ws://suming.leanapp.cn/chat
测试命令: wscat -c ws://suming.leanapp.cn/chat
(确保你已经通过npm全局安装了wscat模块)