移除注释的完善思路:真的可以用正则实现?

开发 前端
网上有很多自称能实现移除JS注释的正则表达式,实际上存在种种缺陷。这使人多少有些愕然,也不禁疑惑到:真的可以用正则实现吗?而本篇文章以使用正则移除JS注释为目标,通过实践,由浅及深,遇到问题解决问题,一步步看看到底能否用正则实现!

移除注释的完善思路:真的可以用正则实现?

导语

网上有很多自称能实现移除JS注释的正则表达式,实际上存在种种缺陷。这使人多少有些愕然,也不禁疑惑到:真的可以用正则实现吗?而本篇文章以使用正则移除JS注释为目标,通过实践,由浅及深,遇到问题解决问题,一步步看看到底能否用正则实现!

移除注释的完善思路:真的可以用正则实现?

1 单行注释

单行注释要么占据一整行,要么处于某一行的***。

正常情况下不难,直接通过正则匹配,再用replace方法移除便可。 

  1. let codes = `  
  2.   let name = "Wmaker"; // This is name 
  3.   if (name) {  
  4.     // Print name 
  5.     console.log("His name is:"name);  
  6.   }  
  7. `;  
  8.  
  9.  
  10. console.log( codes.replace(/\/\/.*$/mg, '') );  
  11.  
  12. // 打印出:  
  13. // let name = "Wmaker";   
  14. // if (name) {  
  15. //     
  16. //   console.log("His name is:"name);  
  17. // } 

上面是成功的删除了注释,不过对于独占一整行的注释清理的不够彻底,会留下空白行。实际上,行尾注释前面的空白也被保留了下来。所以目标稍稍提高,清除这些空白。操作起来也并不难,思路大致这样:删除整行,实际上是删除本行末尾的换行符或上一行末尾的换行符。而换行符本身也属于空白符。所以只需操作正则,匹配到注释以及注释前面所有的空白符即可,一箭双雕。 

  1. let codes = `  
  2.   let name = "Wmaker"; // This is name 
  3.   if (name) {  
  4.     // Print name 
  5.     console.log("His name is:"name);  
  6.   }  
  7. `;   
  8.  
  9. console.log( codes.replace(/\s*\/\/.*$/mg, '') );  
  10.  
  11. // 打印出:  
  12. // let name = "Wmaker" 
  13. // if (name) {  
  14. //   console.log("His name is:"name);  
  15. // } 

如果在字符串中出现完整的URL地址,上面的正则会直接匹配而将其删除。网上大多会将URL的格式特征(http://xxx):双下划线前面有冒号,作为解决途径加以利用。但这只是治标不治本的做法,毕竟//以任何形式出现在字符串中是它的自由,我们无从干涉。

这样问题就转变成:如何使正则匹配存在于引号外的双下划线?

想匹配被引号包围,带有双下划线的代码块比较简单:/".*\/\/.*"/mg。难点在于如何实现这个否定,即当正则匹配到双下划线后,再判断其是否在引号里面?绞尽脑汁,也上网查了很多,都没有像样的结果。静心平气,洗把脸刷刷牙再冲个头冷静之后,觉得单纯使用正则的路已经走不通了,得跳出这个圈。

就在***关头,在那淫秽污浊的房间上方突然光芒万丈。我急忙护住了充满血丝的眼睛,静待其适应后定睛一看。只见那里显现出了一段文字(Chinese):孩儿啊,先将带有//被引号包围的字符串替换掉,去掉注释后再还原,不就行了吗? 

  1. let codes = `  
  2.   let name = "Wmaker"; // This is name 
  3.   if (name) {  
  4.     // Print name 
  5.     console.log("His name is:"name);  
  6.     console.log("Unusual situation, characters of // in quotation marks.");  
  7.   }  
  8. `;   
  9.  
  10. // 之前的方式。  
  11. console.log( codes.replace(/\s*\/\/.*$/mg, '') );  
  12. // 打印出:  
  13. // let name = "Wmaker"
  14. // if (name) {  
  15. //   console.log("His name is:"name);  
  16. //   console.log("Unusual situation, characters of  
  17. // }   
  18.  
  19. // 现在的方式。  
  20. console.log( removeComments(codes) );  
  21. // 打印出: 
  22. // let name = "Wmaker" 
  23. // if (name) {  
  24. //   console.log("His name is:"name);  
  25. //   console.log("Unusual situation, characters of // in quotation marks.");  
  26. // }  
  27.  
  28. function removeComments(codes) {  
  29.   let {replacedCodes, matchedObj} = replaceQuotationMarksWithForwardSlash(codes);  
  30.  
  31.   replacedCodes = replacedCodes.replace(/\s*\/\/.*$/mg, '');  
  32.   Object.keys(matchedObj).forEach(k => {  
  33.     replacedCodes = replacedCodes.replace(k, matchedObj[k]);  
  34.   });  
  35.  
  36.   return replacedCodes;  
  37.  
  38.   function replaceQuotationMarksWithForwardSlash(codes) {  
  39.     let matchedObj = {};  
  40.     let replacedCodes = ''     
  41.  
  42.     let regQuotation = /".*\/\/.*"/mg;  
  43.     let uniqueStr = 'QUOTATIONMARKS' + Math.floor(Math.random()*10000);  
  44.  
  45.     let index = 0;  
  46.     replacedCodes = codes.replace(regQuotation, function(match) {  
  47.       let s = uniqueStr + (index++);  
  48.       matchedObj[s] = match;  
  49.       return s;  
  50.     });  
  51.  
  52.     return { replacedCodes, matchedObj };  
  53.   }  

是的,目标达成了,老天眷顾啊!

另外,有一个需要优化的地方:定义字符串的方式有三种 ' " ` ,目前我们只匹配了双引号。

为了避免正则的记忆功能,都使用了正则字面量进行测试。

--- 之前 

  1. console.log( /".*\/\/.*"/mg.test(`'Unu//sual'`) ); // false  
  2. console.log( /".*\/\/.*"/mg.test(`"Unu//sual"`) ); // true  
  3. console.log( /".*\/\/.*"/mg.test(`\`Unu//sual\``) ); // false 

--- 之后 

  1. console.log( /('|"|`).*\/\/.*\1/mg.test(`'Unu//sual'`) ); // true  
  2. console.log( /('|"|`).*\/\/.*\1/mg.test(`"Unu//sual"`) ); // true  
  3. console.log( /('|"|`).*\/\/.*\1/mg.test(`\`Unu//sual\``) ); // true 

啊!问题到此结束了!

真的结束了吗?不!我看了看时间:02:17,然后将眼镜摘下,扯了张纸巾,拭去了几颗泪水。

以下是接连解决的两个问题:贪婪模式和转义字符。 

  1. --- STEP 1,由于正则的贪婪模式导致。 
  2. let codes = ` 
  3.   let str = 'abc//abc'; // abc' 
  4. `; 
  5. console.log( codes.match(/('|"|`).*\/\/.*\1/mg) ); // ["'abc//abc'; // abc'"] 
  6.  
  7. -- 解决  
  8. let codes = ` 
  9.   let str = 'abc//abc'; // abc' 
  10. `; 
  11. console.log( codes.match(/('|"|`).*?\/\/.*?\1/mg) ); // ["'abc//abc'"]  
  12.  
  13. --- STEP 2,由定义字符串时其中的转义字符导致。 
  14. let codes = ` 
  15.   let str = 'http://x\\'x.com'; // 'acs 
  16. `; 
  17. console.log( codes.match(/('|"|`).*?\/\/.*?\1/mg) ); // ["'http://x\'", "'; // '"] 
  18.  
  19. -- 解决  
  20. let reg = /(?<!\\)('|"|`).*?\/\/.*?(?<!\\)\1/mg; 
  21. let codes = ` 
  22.   let str = 'http://x\\'x.com'; // 'acs 
  23. `; 
  24. console.log( codes.match(reg) ); // ["'http://x\'x.com'"

事情到这里,虽然劳累,但多少有些成就感,毕竟成功了。

可是,可是,可是在测试时,竟然无意间发现一个无法逾越的障碍。就好比费劲千辛万苦花费无尽的财力物力之后,某某尤物终于愿意一同去情人旅馆时,却发现家家爆满,没有空余的房间。在强装欢笑,玩命的哄骗着她,一家接连一家的寻找直到终于定到房间后,却发现自己已然挺不起来了!

正则会将任意位置的引号作为查找的起始位置,它不在乎引号是成双的道理。下面是一个示例。 

  1. let reg = /(?<!\\)('|"|`).*?\/\/.*?(?<!\\)\1/mg;  
  2. let codes = `  
  3.   let str = "abc"; // "  
  4. `;  
  5. console.log( codes.match(reg) ); // [""abc"; // ""] 

不过,问题好歹在补过觉之后的 06:37 时得以解决。

思路是这样的:虽然不能正确实现匹配带有//被引号包围的代码块(可能有方法,但能力有限),但是简化成匹配单纯被引号包围的代码块,是简单而且能正确做到的,虽然耗费的内存多了一些。另外,两引号间也可能包含换行符,所以为其增加s模式:.代表全部字符。下面是去除单行注释的最终代码。 

  1. let codes = `  
  2.   let name = "Wmaker"; // This is name 
  3.   let str = 'http://x\\'x.com' + " / / " + '/"/"/'; // '; // " "  
  4.   if (name) {  
  5.     // Print name 
  6.     console.log("His name is:"name);  
  7.     console.log("Unusual situation, characters of // in quotation marks.");  
  8.   } 
  9. `;  
  10.  
  11. console.log(removeComments(codes));  
  12. // 打印出:  
  13. // let name = "Wmaker" 
  14. // let str = 'http://x\'x.com' + " / / " + '/"/"/' 
  15. // if (name) {  
  16. //   console.log("His name is:"name);  
  17. //   console.log("Unusual situation, characters of // in quotation marks.");  
  18. // } 
  19.  
  20. function removeComments(codes) {  
  21.   let {replacedCodes, matchedObj} = replaceQuotationMarksWithForwardSlash(codes);   
  22.   replacedCodes = replacedCodes.replace(/\s*\/\/.*$/mg, '');  
  23.   Object.keys(matchedObj).forEach(k => {  
  24.     replacedCodes = replacedCodes.replace(k, matchedObj[k]);  
  25.   });  
  26.  
  27.   return replacedCodes;  
  28.  
  29.   function replaceQuotationMarksWithForwardSlash(codes) {  
  30.     let matchedObj = {};  
  31.     let replacedCodes = ''    
  32.  
  33.     let regQuotation = /(?<!\\)('|"|`).*?(?<!\\)\1/smg;  
  34.     let uniqueStr = 'QUOTATIONMARKS' + Math.floor(Math.random()*10000);  
  35.  
  36.     let index = 0;  
  37.     replacedCodes = codes.replace(regQuotation, function(match) {  
  38.       let s = uniqueStr + (index++);  
  39.       matchedObj[s] = match;  
  40.       return s;  
  41.     });  
  42.  
  43.     return { replacedCodes, matchedObj };  
  44.   }  

***补充一点,单双引号虽然也可以多行显示,但其解析后实际是单行的。 

  1. let codes = "' \  
  2.   Wmaker \  
  3. '";  
  4. codes.match( /(?<!\\)('|"|`).*?(?<!\\)\1/smg ); // ["'   Wmaker '"] 

2 多行注释

啊!难点已经解决,现在就可以悠哉悠哉的往前推进了。

多行注释与单行思路相同,只需在删除注释时多加一个匹配模式。中和两者的最终代码如下。 

  1. let codes = `  
  2.   let name = "Wmaker"; // This is name 
  3.   let str = 'http://x\\'x.com' + " / / " + '/"/"/'; // '; // " "  
  4.   let str = 'http://x\\'x./*a*/com' + " / / " + '/"/"/'; // '; // "/*sad*/ "  
  5.   if (name) {  
  6.     // Print name 
  7.     /* Print name. */  
  8.     console.log("His name is:"name);  
  9.     console.log("Unusual situation, characters of // in quotation marks.");  
  10.     /*  
  11.      * Others test.  
  12.      */  
  13.     console.log("Unusual situation, characters of /* abc */ in quotation marks.");  
  14.   }  
  15. `;   
  16.  
  17. console.log(removeComments(codes));  
  18. // 打印出:  
  19. // let name = "Wmaker" 
  20. // let str = 'http://x\'x.com' + " / / " + '/"/"/' 
  21. // let str = 'http://x\'x./*a*/com' + " / / " + '/"/"/' 
  22. // if (name) {  
  23. //   console.log("His name is:"name);  
  24. //   console.log("Unusual situation, characters of // in quotation marks.");  
  25. //   console.log("Unusual situation, characters of /* abc */ in quotation marks."); 
  26. // }  
  27.  
  28. function removeComments(codes) {  
  29.   let {replacedCodes, matchedObj} = replaceQuotationMarksWithForwardSlash(codes);  
  30.  
  31.   replacedCodes = replacedCodes.replace(/(\s*\/\/.*$)|(\s*\/\*[\s\S]*?\*\/)/mg, '');  
  32.   Object.keys(matchedObj).forEach(k => {  
  33.     replacedCodes = replacedCodes.replace(k, matchedObj[k]);  
  34.   }); 
  35.  
  36.   return replacedCodes;  
  37.   function replaceQuotationMarksWithForwardSlash(codes) {  
  38.     let matchedObj = {};  
  39.     let replacedCodes = ''     
  40.  
  41.     let regQuotation = /(?<!\\)('|"|`).*?(?<!\\)\1/smg;  
  42.     let uniqueStr = 'QUOTATIONMARKS' + Math.floor(Math.random()*10000);  
  43.  
  44.     let index = 0;  
  45.     replacedCodes = codes.replace(regQuotation, function(match) {  
  46.     let s = uniqueStr + (index++);  
  47.     matchedObj[s] = match;  
  48.     return s;  
  49.     });  
  50.     return { replacedCodes, matchedObj };  
  51.   }  

3 总结

从以上可以得出结论,单纯使用正则表达式是不能达到目标的,需要配合其它操作才行。但现在得出的结果真的能覆盖全部的情况?会不会有其它的隐藏问题,比如多字节字符的问题。虽然作为一个码农,该有的自信不会少,但慢慢的也明白了自己的局限性。从网上的其它资料看,使用UglifyJS,或在正确的解析中去除注释,会更为稳妥。但有可能自己动手解决的,没理由不花费些精力试试!

问题更新记录

感谢热心同志找出的错误,我会将能改与不能改的都列于此地,并只会更新下面两个示例的代码。

1.没有考虑正则字面量中的转义字符。

出错示例:var reg=/a\//;。

修改方式:将删除注释的正则改为:/(\s*(?<!\\)\/\/.*$)|(\s*(?<!\\)\/\*[\s\S]*?(?<!\\)\*\/)/mg。

这里是工作于前端页面的代码及相应示例,下载链接。 

  1. <!DOCTYPE html>  
  2. <html> 
  3.  
  4. <head>  
  5.   <meta charset="UTF-8" 
  6.   <title>Remove Comments</title>  
  7. </head>  
  8.  
  9. <body>  
  10.   <p>输入:</p>  
  11.   <textarea id="input" cols="100" rows="12"></textarea>  
  12.  
  13.   <br /><br />  
  14.   <button onclick="transform()">转换</button>  
  15.  
  16.   <p>输出:</p>  
  17.   <textarea id="output" cols="100" rows="12"></textarea>    
  18.  
  19.   <script>  
  20.     let input = document.querySelector('#input');  
  21.     let output = document.querySelector('#output');  
  22.  
  23.     setDefaultValue();  
  24.  
  25.     function transform() {  
  26.       output.value = removeComments(input.value);  
  27.     } 
  28.  
  29.     function removeComments(codes) {  
  30.       let {replacedCodes, matchedObj} = replaceQuotationMarksWithForwardSlash(codes);  
  31.  
  32.       replacedCodes = replacedCodes.replace(/(\s*(?<!\\)\/\/.*$)|(\s*(?<!\\)\/\*[\s\S]*?(?<!\\)\*\/)/mg, '');  
  33.       Object.keys(matchedObj).forEach(k => { 
  34.        replacedCodes = replacedCodes.replace(k, matchedObj[k]);  
  35.       });  
  36.  
  37.       return replacedCodes;  
  38.  
  39.       function replaceQuotationMarksWithForwardSlash(codes) {  
  40.         let matchedObj = {};  
  41.         let replacedCodes = ''         
  42.  
  43.         let regQuotation = /(?<!\\)('|"|`).*?(?<!\\)\1/smg;  
  44.         let uniqueStr = 'QUOTATIONMARKS' + Math.floor(Math.random()*10000);  
  45.  
  46.         let index = 0;  
  47.         replacedCodes = codes.replace(regQuotation, function(match) {  
  48.           let s = uniqueStr + (index++);  
  49.           matchedObj[s] = match;  
  50.           return s;  
  51.         });  
  52.  
  53.         return { replacedCodes, matchedObj };  
  54.       }  
  55.     }  
  56.  
  57.     function setDefaultValue() {  
  58.       input.value = `let name = "Wmaker"; // This is name 
  59. let str = 'http://x\\'x.com' + " / / " + '/"/"/'; // '; // " "  
  60. let str = 'http://x\\'x./*a*/com' + " / / " + '/"/"/'; // '; // "/*sad*/ "  
  61. if (name) {  
  62.   // Print name 
  63.   /* Print name. */  
  64.   console.log("His name is:"name);  
  65.   console.log("Unusual situation, characters of // in quotation marks.");  
  66.   /*  
  67.    * Others test.  
  68.    */  
  69.   console.log("Unusual situation, characters of /* abc */ in quotation marks."); 
  70.   
  71. `;  
  72.     }  
  73.   </script>  
  74. </body>  
  75. </html> 

这里是工作于Node端的代码及相应示例,下载链接。运行命令:node 执行文件 待转译文件 转移后文件。 

  1. const fs = require('fs');  
  2. const path = require('path');  
  3. const process = require('process');  
  4.  
  5. let sourceFile = process.argv[2];  
  6. let targetFile = process.argv[3];  
  7. if (!sourceFile || !targetFile) {  
  8.   throw new Error('Please set source file and target file.');  
  9.  
  10. sourceFile = path.resolve(__dirname, sourceFile);  
  11. targetFile = path.resolve(__dirname, targetFile);  
  12.  
  13. fs.readFile(sourceFile, 'utf8', (err, data) => {  
  14.   if (err) throw err; 
  15. fs.writeFile(targetFile, removeComments(data), 'utf8', (err, data) => {  
  16.     if (err) throw err;  
  17.     console.log('Remove Comments Done!');  
  18.   });  
  19. });  
  20.  
  21. function removeComments(codes) {  
  22.   let {replacedCodes, matchedObj} = replaceQuotationMarksWithForwardSlash(codes);  
  23.  
  24.   replacedCodes = replacedCodes.replace(/(\s*(?<!\\)\/\/.*$)|(\s*(?<!\\)\/\*[\s\S]*?(?<!\\)\*\/)/mg, '');  
  25.   Object.keys(matchedObj).forEach(k => {  
  26.     replacedCodes = replacedCodes.replace(k, matchedObj[k]);  
  27.   }); 
  28.   
  29.   return replacedCodes; 
  30.  
  31.   function replaceQuotationMarksWithForwardSlash(codes) {  
  32.     let matchedObj = {};  
  33.     let replacedCodes = ''      
  34.  
  35.     let regQuotation = /(?<!\\)('|"|`).*?(?<!\\)\1/smg;  
  36.     let uniqueStr = 'QUOTATIONMARKS' + Math.floor(Math.random()*10000);  
  37.  
  38.     let index = 0;  
  39.     replacedCodes = codes.replace(regQuotation, function(match) {  
  40.       let s = uniqueStr + (index++);  
  41.       matchedObj[s] = match;  
  42.       return s;  
  43.     });  
  44.  
  45.     return { replacedCodes, matchedObj };  
  46.   }  
责任编辑:庞桂玉 来源: segmentfault
相关推荐

2022-09-20 15:33:35

JavaScriptCSS编程

2021-11-16 12:25:14

jsPPT前端

2020-07-24 09:40:04

C语言OOP代码

2018-07-31 10:20:54

WindowsDocker Linux

2023-10-24 08:25:20

TCC模式事务

2012-02-08 09:28:59

无线网络Wi-FiRuckus

2011-01-04 14:27:50

安装linux方法

2011-01-21 13:56:44

SendmailSolaris

2024-05-29 07:39:47

2011-06-17 14:36:50

Linux

2017-10-11 16:16:29

弱电pvc管穿线

2022-06-06 12:02:23

代码注释语言

2009-12-03 10:27:12

FreeBSD路由器Snort

2023-04-03 08:26:01

systemd运维

2020-07-16 08:32:16

JavaScript语言语句

2010-04-02 15:36:37

Oracle约束

2022-11-21 10:28:13

FlutterPython

2015-04-27 11:09:53

GoogleQUIC互联网协议

2023-08-22 09:00:00

人工智能Fashion-AI

2020-11-03 07:43:24

MQ版本号程序员
点赞
收藏

51CTO技术栈公众号