美文网首页
JavaScript的几个高级应用

JavaScript的几个高级应用

作者: dpzxsm | 来源:发表于2018-06-08 10:03 被阅读0次

    正则表达式在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的几个高级应用

          本文链接:https://www.haomeiwen.com/subject/rlposftx.html