介绍这周开始学习老姚大佬的《JavaScript 正则表达式迷你书》 , 然后习惯性的看完一遍后,整理一下知识点,便于以后自己重新复习。 我个人觉得:自己整理下来的资料,对于知识重现,效果不错。 感谢原书作者老姚,本文无意抄袭,只是作为自己知识点的整理,后续也会整理到自己的 JavaScript知识库——《Cute-JavaScript》 网站中。 另外,请读者们注意,这篇文章是知识点的整理,方便复习,所以不会介绍太详细,因为毕竟原书写得非常棒,刚入门的朋友,我还是建议看下原书。 然后可以看看这篇文章,来回顾重要知识点。《JavaScript 正则表达式迷你书》《Cute-JavaScript》目录一、正则表达式字符匹配二、正则表达式位置匹配三、正则表达式括号的使用四、正则表达式回溯法原理五、正则表达式的拆分六、正则表达式的构建七、正则表达式编程文章推荐(补充中)老姚 —— JS正则表达式完整教程常见的正则表达式可视化描述工具推荐Regulex*%3F%24)Rubular一、正则表达式字符匹配原书这么一句话,特别棒:正则表达式是匹配模式,要么匹配字符,要么匹配位置,要记住。1. 两种模糊匹配正则表达式的强大在于它的模糊匹配,这里介绍两个方向上的“模糊”:横向模糊和纵向模糊。横向模糊匹配即一个正则可匹配的字符串长度不固定,可以是多种情况。 如 /ab{2,5}c/ 表示匹配: 第一个字符是 “a” ,然后是 2 - 5 个字符 “b” ,最后是字符 “c” :let r = /ab{2,5}c/g;let s = “abc abbc abbbc abbbbbbc”;s.match(r); // [“abbc”, “abbbc”]纵向模糊匹配即一个正则可匹配某个不确定的字符,可以有多种可能。 如 /[abc]/ 表示匹配 “a”, “b”, “c” 中任意一个。let r = /a[123]b/g;let s = “a0b a1b a4b”;s.match(r); // [“a1b”]2. 字符组范围表示法可以指定字符范围,比如 [1234abcdUVWXYZ] 就可以表示成 [1-4a-dU-Z] ,使用 - 来进行缩写。 如果要匹配 “a”, “-”, “z” 中任意一个字符,可以这么写: [-az] 或 [a-z] 或 [az-] 。排除字符组即需要排除某些字符时使用,通过在字符组第一个使用 ^ 来表示取反,如 [^abc] 就表示匹配除了 “a”, “b”, “c” 的任意一个字符。常见简写形式字符组具体含义\d表示 [0-9],表示一位数字。\D表示 [^0-9],表示除数字外的任意字符。\w表示 [0-9a-zA-Z_],表示数字、大小写字母和下划线。\W表示 [^0-9a-zA-Z_],表示非单词字符。\s表示 [\t\v\n\r\f],表示空白符,包含空格、水平制表符、垂直制表符、换行符、回车符、换页符。\S表示 [^\t\v\n\r\f],表示非空白字符。.表示 [^\n\r\u2028\u2029] 。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。然后表示任意字符,就可以使用 [\d\D]、[\w\W]、[\s\S] 和 [^] 任意一个。3. 量词量词也称重复,常用简写如下:量词具体含义{m,}表示至少出现 m 次。{m}等价于 {m, m} ,表示出现 m 次。?等价于 {0, 1} ,表示出现或不出现。+等价于 {1, } ,表示至少出现1次。等价于 {0, } ,表示出现任意次,也有可能不出现。贪婪匹配和惰性匹配在正则 /\d{2,4}/ ,表示数字连续出现 2 - 4 次,可以匹配到 2 位、 3 位、4 位连续数字。 但是在 贪婪匹配 如 /\d{2,4}/g ,会尽可能多匹配,如超过 4 个,就只匹配 4 个,如有 3 个,就匹配 3 位。 而在 惰性匹配 如 /\d{2,4}?/g ,会 尽可能少 匹配,如超过 2 个,就只匹配 2 个,不会继续匹配下去。let r1 = /\d{2,4}/g;let r2 = /\d{2,4}?/g;let s = “123 1234 12345”; s.match(r1); // [“123”, “1234”, “1234”]s.match(r2); // [“12”, “12”, “34”, “12”, “34”]惰性量词贪婪量词{m,m}?{m,m}{m,}?{m,}???+?+?4. 多选分支即提供多个子匹配模式任选一个,使用 |(管道符)分隔,由于分支结构也是惰性,即匹配上一个后,就不会继续匹配后续的。 格式如:(r1|r2|r3),我们就可以使用 /leo|pingan/ 来匹配 “leo” 和 “pingan”。let r = /leo|pingan/g;let s = “leo cool,pingan good.";s.match(r);// [“leo”, “pingan”]// 多选分支的惰性表现let r1 = /leo|leooo/g;let r2 = /leooo|leo/g;let s = “leooo”;s.match(r1);// [“leo”]s.match(r2);// [“leooo”]5. 案例分析匹配字符,无非就是字符组、量词和分支结构的组合使用。十六进制颜色值匹配let r = /#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g;let s = “#ffaacc #Ff00DD #fFF #01d #9Gd”;s.match(r); // ["#ffaacc”, “#Ff00DD”, “#fFF”, “#01d”]时间和日期匹配// 时间 12:23 或 01:09let r = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/; r.test(“23:25”); // truer.test(“03:05”); // true// 时间 12:23 或 1:9let r = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/; r.test(“23:25”); // truer.test(“03:05”); // truer.test(“3:5”); // true// 日期 yyyy-mm-ddlet r = /^[0-9]{4}-(0[1-9]|[1][0-2])-(0[1-9]|[12][0-9]|3[01])$/;r.test(“2019-09-19”); // truer.test(“2019-09-32”); // falseWindows操作系统文件路径匹配盘符使用 [a-zA-Z]:\ ,这里需要注意 \ 字符需要转义,并且盘符不区分大小写; 文件名或文件夹名,不能包含特殊字符,使用 [^\:<>|"?\r\n/] 表示合法字符; 并且至少有一个字符,还有可以出现任意次,就可以使用 ([^\:<>|"?\r\n/]+\) 匹配任意个 文件夹\; 还有路径最后一部分可以是 文件夹 ,即没有 \ 于是表示成 ([^\:<>|"?\r\n/]+)?。let r = /^[a-zA-Z]:\([^\:<>|"?\r\n/]+\)([^\:<>|"?\r\n/]+)?$/;r.test(“C:\document\leo\a.png”); // truer.test(“C:\document\leo\”); // truer.test(“C:\document”); // truer.test(“C:\”); // trueid匹配如提取 <div id=“leo” class=“good”></id> 中的 id=“leo” :let r1 = /id="."/; // tips1let r2 = /id=".?"/; // tips2let r3 = /id="[^"]"/; // tips3let s = ‘<div id=“leo” class=“good”></id>’;s.match(r1)[0]; // id=“leo” class=“good"s.match(r2)[0]; // id=“leo"s.match(r3)[0]; // id=“leo"tips1:由于 . 匹配双引号,且 * 贪婪,就会持续匹配到最后一个双引号结束。 tips2:使用惰性匹配,但效率低,有回溯问题。 tips3:最终优化。二、正则表达式位置匹配位置匹配,就是要匹配每个字符两边的位置。 在 ES5 中有6个位置: ^,$,\b,\B,(?=p) 和 (?!p)。 另外把位置理解成空字符是非常有用的:/^^hello$$/.test(‘hello’); // true/^^^hello$$/.test(‘hello’); // true1. ^ 和 $^ 匹配开头,多行中匹配行开头。 $ 匹配结尾,多行中匹配行结尾。“hello”.replace(/^|$/g, “#”); // “#hello#““hello\nleo\nhaha”.replace(/^|$/gm, “#”);/#hello##leo##haha#/多行匹配模式使用 m 修饰符。2. \b 和 \B\b 匹配单词边界,即 \w 和 \W 之间的位置,包括 \w 和 ^ 之间的位置,和 \w 和 $ 之间的位置。 \B 和 \b 相反,即非单词边界,匹配中除去 \b,剩下的都是 \B 的。也就是 \w 与 \w、 \W 与 \W、^ 与 \W,\W 与 $ 之间的位置。。"[HI] Leo_1.mp4”.replace(/\b/g,”#”);// “[#HI#] #Leo_1#.#mp4#”"[HI] Leo_1.mp4”.replace(/\B/g,”#");// “#[H#I]# L#e#o#_#1.m#p#4"3. (?=p) 和 (?!p)p 为一个子模式,即 (?=p) 匹配前面是 p 的位置,而 (?!p) 则匹配前面不是 p 的位置。“hello”.replace(/(?=l)/g, “#”);// “he#l#lo"“hello”.replace(/(?!l)/g, “#”);// “#h#ell#o#“4. 相关案例匹配数字千位分隔符// 匹配最后一个逗号"12345678”.replace(/(?=\d{3}$)/g, “,”); // “12345,678”// 匹配所有逗号"12345678”.replace(/(?=(\d{3})+$)/g, “,”); // “12,345,678”// 匹配其余"123456789”.replace(/(?=(\d{3})+$)/g, “,”); // “,123,456,789”// 修改"123456789”.replace(/(?!^)(?=(\d{3})+$)/g, “,”); // “12,345,678”// 其他形式"12345678 123456789".replace(/(?!\b)(?=(\d{3})+\b)/g, “,”); // (?!\b) 等于 \B ,要求当前是一个位置,但不是 \b 前面的位置// “12,345,678 123,456,789"数据格式化let num = 1888;num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, “,”).replace(/^/,"$$ “);// “$ 1,888.00"验证密码// 密码长度 6-12 位数字或字母let r = /^[0-9A-Za-z]{6,12}$/;// 必须包含一个字符(数字) + 密码长度 6-12 位数字或字母let r = /(?=.[0-9])^[0-9A-Za-z]{6,12}$/;// 必须包含两个个字符(数字和小写字符) + 密码长度 6-12 位数字或字母let r = /(?=.[0-9])(?=.[a-z])^[0-9A-Za-z]{6,12}$/;r.test(“aa1234566”); // truer.test(“1234566”); // false// 密码长度 6-12 位数字或字母 // 即 不能全是数字 或 不能全是大写或小写字母let r = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;三、正则表达式括号的使用简单理解:括号提供了分组,便于我们使用它。 通常有两种引用情况:在JS代码中引入,和在正则表达式中引入。 分组和分支结构,主要是强调括号内是一个整体,即提供子表达式。分组如 /(ab)+/g 匹配连续出现的 ab 。分支结构如 /(a|b)+/g 匹配出现的 a 或 b 表达式。1.分组引用如在日期匹配的时候,就可以这么改造:// 原来let r = /\d{4}-\d{2}-\d{2}/;// 现在let r = /(\d{4})-(\d{2})-(\d{2})/;提取数据"2019-03-14”.match(r);r.exec(“2019-03-14”);// [“2019-03-14”, “2019”, “03”, “14”, index: 0, input: “2019-03-14”]RegExp.$1; // “2019"RegExp.$2; // “03"RegExp.$3; // “14"替换将 yyyy-mm-dd 转成 mm/dd/yyyy。“2019-03-14”.replace(r, “$2/$3/$1”);// 等价于"2019-03-14”.replace(r, function(){ return RegExp.$2 + ‘/’ + RegExp.$3 + ‘/’ + RegExp.$1;});2. 反向引用使用 \n 表示第 n 个分组,比如 \1 表示第 1 个分组:let r = /\d{4}(-|/|.)\d{2}\1\d{2}/;r.test(“2019-03-15”);r.test(“2019/03/15”);r.test(“2019.03.15”);r.test(“2019-03/15”);多个括号嵌套按照开括号的顺序:let r = /^((\d)(\d(\d)))\1\2\3\4$/;let s = “1231231233”;r.test(s);console.log([RegExp.$1,RegExp.$2,RegExp.$3,RegExp.$4]);// [“123”, “1”, “23”, “3”]特殊情况\10 表示的是第 10 个分组,若要匹配 \ 和 0 时,使用 (?:\1)0 或 \1(?:0)。let r = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;let s = “123456789# #####";r.test(s); // true当引用不存在的分组如匹配 \2 是前面不存在,则匹配 \2 本身,即对 2 的转义,不同浏览器可能不同:let r = /\1\2\3\4/;r.test("\1\2\3\4”); // true”\1\2\3\4”.split(’’);// [””, “”, “”, “"]分组后面有量词当分组后面有量词的话,则捕获的是最后一次的匹配:“12345”.match(/(\d)+/); // [“12345”, “5”, index: 0, input: “12345”]/(\d)+ \1/.test(“12345 1”); // false/(\d)+ \1/.test(“12345 5”); // true3. 相关案例这里只写出核心代码。模拟字符串 trim 方法// 1 匹配首尾空白符,替换成空字符” aaa “.replace(/^\s+|\s+$/g, “”); // “aaa”// 2 匹配整个字符串,再用引用提取对应数据” aaa “.replace(/^\s*(.?)\s$/g, “$1”);// “aaa"每个单词首字母大写"hi leo hi boy!".toLowerCase().replace( /(?:^|\s)\w/g, c => c.toUpperCase());// “Hi Leo Hi Boy!“驼峰化 和 中划线化”-leo-and-pingan”.replace(/[-\s]+(.)?/g, (match, c) => c ? c.toUpperCase() : ‘’);// “LeoAndPingan"“LeoAndPingan”.replace(/([A-Z])/g, “-$1”).replace( /[-\s]+g/,”-”).toLowerCase();// “-leo-and-pingan"匹配成对HTML标签匹配成对标签 <h1>leo<\h1>,而不匹配不成对标签 <h1>leo<\h2>。let r = /<([^>]+)>[\d\D]</\1>/;r.test("<h1>leo leo leo</h1>”); // truer.test("<a>leo leo leo</a>”); // truer.test("<h1>leo leo leo</h2>"); // false四、正则表达式回溯法原理概念理解起来比较容易。 比如用 /ab{1,3}c/ 去匹配下面两个字符串。当匹配 abbbc,按顺序匹配,到了第 3 个 b 后,直接匹配 c,这样就没有回溯。当匹配 abbc,按顺序匹配,到了第 2 个 b 后,由于规则是 b{1,3} ,则会继续往下匹配,然后发现下一位是 c,于是回退到前一个位置,重新匹配,这就是回溯。另外像 /"."/ 来匹配 “abc"de 的话,就会有三个回溯情况,为了减少不必要的回溯,我们可以把正则修改为 /”[^"]"/。介绍 回溯法,也称试探法,本质上是深度优先探索算法,基本思路是:匹配过程中后退到之前某一步重新探索的过程。1. 常见的回溯形式贪婪量词多个贪婪量词挨着存在,并相互冲突时,会看匹配顺序,深度优先搜索:“12345”.match(/(\d{1,3})(\d{1,3})/);//  [“12345”, “123”, “45”, index: 0, input: “12345”]惰性量词有时候会因为回溯,导致实际惰性量词匹配到的不是最少的数量:“12345”.match(/(\d{1,3}?)(\d{1,3})/);// 没有回溯的情况 [“1234”, “1”, “234”, index: 0, input: “12345”]“12345”.match(/^\d{1,3}?\d{1,3}$/);// 有回溯的情况 [“12345”, index: 0, input: “12345”]分支结构分支机构,如果一个分支整体不匹配,会继续尝试剩下分支,也可以看成一种回溯。“candy”.match(/can|candy/); // [“can”, index: 0, input: “candy”]“candy”.match(/^(?:can|candy)$/); // [“candy”, index: 0, input: “candy”]2. 本章小结简单总结:一个个尝试,直到,要么后退某一步整体匹配成功,要么最后试完发现整体不匹配。贪婪量词:买衣服砍价,价格高了,便宜点,再便宜点。懒惰量词:卖衣服加价,价格低了,多给点,再多给点。分支结构:货比三家,一家不行换一家,不行再换。五、正则表达式的拆分拆分正则代码块,是理解正则的关键。 在 JavaScrip 正则表达式有以下结构:字面量: 匹配一个具体字符,如 a 匹配字符 a。字符组: 匹配一个有多种可能性的字符,如 [0-9] 匹配任意一个数字。量词: 匹配一个连续出现的字符,如 a{1,3} 匹配连续最多出现 3 次的a字符。锚: 匹配一个位置,如 ^ 匹配字符串的开头。分组: 匹配一个整体,如 (ab) 匹配 ab 两个字符连续出现。分支: 匹配一个或多个表达式,如 ab|bc 匹配 ab 或 bc 字符。另外还有以下操作符:优先级操作符描述操作符1转义符\2括号和方括号(…)/(?:…)/(?=…)/(?!…)/[…]3量词限定符{m}/{m,n}/{m,}/?//+4位置和序列^/$/\元字符/一般字符5管道符``Tips:优先级从上到下,由高到低。1. 注意要点匹配字符串整体不能写成 /^abc|bcd$/ ,而是要写成 /^(abc|bcd)$/。量词连缀问题需要匹配:每个字符是 a/b/c 中其中一个,并且字符串长度是 3 的倍数: 不能写成 /^[abc]{3}+$/ ,而是要写成 /([abc]{3})+/。元字符转义问题元字符就是正则中的特殊字符,当匹配元字符就需要转义,如: ^、$、.、、+、?、|、\、/、(、)、[、]、{、}、=、!、:、- 。// “[abc]” => /[abc]/ 或者 /[abc]/ // “{1,3}” => /{1}/ 或者 /{1}/ 因为不构成字符组2. 案例分析身份证号码/^(\d{15}|\d{17})[\dxX]$/.test(“390999199999999999”);// trueIPV4地址需要好好分析:let r = /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5]).){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/六、正则表达式的构建正则的构建需要考虑以下几点的平衡:匹配预期的字符串不匹配非预期的字符串可读性和可维护性效率我们还需要考虑这么几个问题:是否需要使用正则如能使用其他 API 简单快速解决问题就不需要使用正则:“2019-03-16”.match(/^(\d{4})-(\d{2})-(\d{2})/); // 间接获取 [“2019”, “03”, “16”]“2019-03-16”.split("-"); //  [“2019”, “03”, “16”]"?id=leo".search(/?/); // 0"?id=leo".indexOf("?"); // 0"JavaScript".match(/.{4}(.+)/)[1]; // “Script"“JavaScript”.substring(4); // “Script"是否需要使用复杂正则/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/ 将这个正则拆分成多个小块,如下:var regex1 = /^[0-9A-Za-z]{6,12}$/;var regex2 = /^[0-9]{6,12}$/;var regex3 = /^[A-Z]{6,12}$/;var regex4 = /^[a-z]{6,12}$/;function checkPassword (string) { if (!regex1.test(string)) return false; if (regex2.test(string)) return false; if (regex3.test(string)) return false; if (regex4.test(string)) return false; return true;}1. 准确性即需要匹配到预期目标,且不匹配非预期的目标。匹配固定电话如需要匹配下面固定电话号码,可以分别写出对应正则:055188888888 => /^0\d{2,3}[1-9]\d{6,7}$/0551-88888888 => /^0\d{2,3}-[1-9]\d{6,7}$/(0551)88888888 => /^0\d{2,3}-[1-9]\d{6,7}$/然后合并:let r = /^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^(0\d{2,3})[1-9]\d{6,7}$/然后提取公共部分:let r = /^(0\d{2,3}|0\d{2,3}-|(0\d{2,3}))[1-9]\d{6,7}$/再优化:let r = /^(0\d{2,3}-?|(0\d{2,3}))[1-9]\d{6,7}$/匹配浮点数先确定,符号部分([+-])、整数部分(\d+)和小数部分(.\d+)。1.23、+1.23、-1.23 => /^[+-]?\d+.\d+$/10、+10、-10 => /^[+-]?\d+$/.2、+.2、-.2 => /^[+-]?.\d+$/整理后:let r = /^[+-]?(\d+.\d+|\d+|.\d+)$/;// 考虑不匹配 +.2 或 -.2let r = /^([+-])?(\d+.\d+|\d+|.\d+)$/;// 考虑不匹配 012 这类 0 开头的整数let r = /^[+-]?(\d+)?(.)?\d+$/;2. 效率正则表达式运行过程:编译设定起始位置尝试匹配若匹配失败则返回前一步重新匹配返回匹配成功失败的结果我们常常优化对 3 和 4 步进行优化:使用具体字符组替代通配符,消除回溯如 /”[^”]"/ 代替 /".?"/。使用非捕获型分组当不需要使用分组引用和反向引用时,此时可以使用非捕获分组。如 /^[-]?(?:\d.\d+|\d+|.\d+)$/ 代替 /^[-]?(\d.\d+|\d+|.\d+)$/。独立出确定字符加快判断是否匹配失败,进而加快移位的速度。如 /aa/ 代替 /a+/。提取分支公共部分减少匹配过程中可消除的重复。如 /^(?:abc|def)/ 代替 /^abc|^def/。减少分支的数量,缩小它们的范围如 /rea?d/ 代替 /red|read/。七、正则表达式编程这里要掌握正则表达式怎么用,通常会有这么四个操作:验证切分提取替换1. 四种操作验证匹配本质上是查找,我们可以借助相关API操作:// 检查字符串是否包含数字let r = /\d/, s = “abc123”;!!s.search(r); // truer.test(s); // true!!s.match(r); // true!!r.exec(s); // true切分"leo,pingan".split(/,/); // [“leo”, “pingan”]let r = /\D/, s = “2019-03-16”;s.split(r); // [“2019”, “03”, “16”]s.split(r); // [“2019”, “03”, “16”]s.split(r); // [“2019”, “03”, “16”]提取// 提取日期年月日let r = /^(\d{4})\D(\d{2})\D(\d{2})$/;let s = “2019-03-16”;s.match(r); // [“2019-03-16”, “2019”, “03”, “16”, index: 0, input: “2019-03-16”]r.exec(s); // [“2019-03-16”, “2019”, “03”, “16”, index: 0, input: “2019-03-16”]r.test(s); // RegExp.$1 => “2019” RegExp.$2 => “03” RegExp.$3 => “16"s.search(r);// RegExp.$1 => “2019” RegExp.$2 => “03” RegExp.$3 => “16"替换// yyyy-mm-dd 替换成 yyyy/mm/dd"2019-03-16”.replace(/-/g, “/”);2. 相关API注意search 和 match 参数问题这两个方法会把字符串转换成正则,所以要加转义let s = “2019.03.16”;s.search(’.’); // 0s.search(’\.’); // 4s.search(/./); // 4s.match(’.’); // [“2”, index: 0, input: “2019.03.16”]s.match(’\.’); // [”.", index: 4, input: “2019.03.16”]s.match(/./); // [".", index: 4, input: “2019.03.16”]// 其他不用转义s.split(’.’);s.replace(’.’, ‘/’);match 返回结果的格式问题match 参数有 g 会返回所有匹配的内容,没有 g 则返回标准匹配格式:let s = “2019.03.16”;s.match(/\b(\d+)\b/); // [“2019”, “2019”, index: 0, input: “2019.03.16”]s.match(/\b(\d+)\b/g); // [“2019”, “03”, “16”]test 整体匹配时需要使用 ^ 和 $/123/.test(“a123b”); // true/^123$/.test(“a123b”); // false/^123$/.test(“123”); // truesplit 的注意点split 第二个参数是 结果数组的最大长度:“leo,pingan,pingan8787”.split(/,/, 2); // [“leo”, “pingan”]使用正则分组,会包含分隔符:“leo,pingan,pingan8787”.split(/(,)/); // [“leo”, “,”, “pingan”, “,”, “pingan8787”]修饰符修饰符描述g全局匹配,即找到所有匹配的,单词是 global。i忽略字母大小写,单词是 ingoreCase。m多行匹配,只影响 ^ 和 $,二者变成行的概念,即行开头和行结尾。单词是 multiline。文章到这结束,感谢阅读,也感谢老姚大佬的这本书Author王平安E-mailpingan8787@qq.com博 客www.pingan8787.com微 信pingan8787每日文章推荐https://github.com/pingan8787…ES小册js.pingan8787.com微信公众号