JavaScript的几个高级应用

正则表达式在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);

最后调试运行,可以发现去重数据成功了,完全符合规则但是这样做,但是却有几个很明显的缺点

  1. 代码量大,不易理解和阅读
  2. 时间复杂度较高
  3. 如果修改了去重规则,不易维护

如果我们换一种思路,其实我们可以先对数据进行排序,把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模块)

你可能感兴趣的:(JavaScript的几个高级应用)