本周一篇指摘 Vue 抄袭 Angular 的文章一石激起千层浪。为此,笔者作为中立吃瓜的 React 用户,分析了 13 个主流前端框架版本上万个变量的命名风格,应用自然语言处理中的文本相似度算法进行了分析,以对这一论点的有效性做出客观的评价。
思路
在分析书籍抄袭、论文查重等场景下,使用算法比较文本相似度的方法是一种有效的技术手段。那么,我们如何通过这一手段,来分析源码层面的抄袭呢?
在对比形如 我喜欢写代码,不喜欢撕逼
和 我不喜欢撕逼,喜欢写代码
的两个句子相似度时,大致的思路是首先分词,而后计算出词频,再将词频向量化,最后比较两个高维向量的夹角,夹角越小则越相似。
在【Vue 是否抄袭了 Angular】这一场景下,我们分析的对象从句子变成了代码。这时主要的区别是这两点:
- 代码是高度结构化的文本,分词已经通过词法分析器完成了。
- 某种编程语言的代码中,充斥着大量该语言的关键字。如
var
和function
的关键字,这些关键字的重复与相似度无关。
而对于是否抄袭与相似度的关系,我们给出这几个假设:
- 解决同样的开放性问题时,独立编写、不存在抄袭的代码,以变量名为代表的编码风格通常有巨大的差别,此时相似度是低的。
- 存在较大规模抄袭的代码,类似于同一个框架,未经大规模重构的不同版本代码。此时编码风格是类似的,此时相似度是高的。
- 前后调换模块声明顺序,不影响相似度。
给定这几个前提后,我们可以确定出这样的分析策略:
- 输入各大框架未经压缩的源码,解析出其语法树。
- 舍弃语法树中无关部分,提取出其变量声明以代表其编码风格。
- 使用文本相似度算法计算变量名间的相似度,分析结论。
变量名提取
在通过 Webpack 引用框架依赖时,通常导入的都是打包成单一文件且未经混淆的框架源码。这是一个非常好的特性。笔者编写了一个简单的 Webpack Loader 以在这个过程中实现变量名的提取:
// loader/index.js
// 为 loader 传入的 content 即为 JS 源码
module.exports = function (content) {
return demo(content)
}
在 demo
函数中获得框架源码后,解析语法树也不是一个困难的问题了。通过 acorn 这一 Parser 我们就能做到:
function demo (content) {
const ast = acorn.parse(content, { sourceType: 'module' })
walk.simple(ast, {
// 在 walk 遍历时,抽取全部变量声明语句中的变量名
VariableDeclaration (node) {
const name = node.declarations[0].id.name + '\n'
fs.appendFileSync(resolve('./result.txt'), name)
}
})
return content
}
这时候我们就能在 result.txt
内获得一个前端框架中的全部变量名了,形如:
p
i
resolved
c
segs
i
...
这都是什么乱七八糟的…这时候我们获得的文本并没有经过初步的处理,我们真正感兴趣的是各个框架变量名的词频。词频的计算是一道不错的面试题,不过在这里我们直接通过 Wordclouds 的服务来实现这一步。这一步中还包括基本的清洗,以去除 i
/ a
/ b
这些无意义的变量名。我们的结果是形如这样的格式:
29 value
19 arg
18 result
16 key
14 index
...
以上就是 React / Vue / Angular 三大框架中某一个的 Top 5 变量名,猜猜是哪一个?好吧这几个变量名都十分烂大街…暂时看不出什么端倪。让我们继续做相似度比较吧,答案在后面揭晓。
相似度算法
我们在上文中,实际上已经获得了这样的对象:
const a = {
'foo': 5,
'bar': 4,
'baz': 3
}
const b = {
'foo': 4,
'bar': 6,
'baz': 0
}
我们可以认为,每个变量名是一个独立的维度,每个框架中存在的所有类型变量名组成一个高维空间的向量。从而,我们的问题就简化为了如何比较 a 与 b 这两个向量的相似程度。在此引用阮一峰老师的介绍:
我们可以把它们想象成空间中的两条线段,都是从原点([0, 0, ...])出发,指向不同的方向。两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来判断向量的相似程度。夹角越小,就代表越相似。

假定 a 向量是 [x1, y1]
,b向量是 [x2, y2]
,那么可以将余弦定理改写成下面的形式,计算出的 cosθ
代表相似度:

推广到高维向量的一般情形:

根据算法编写出简化的示例代码:
function getTheta () {
let x = 0
Object.keys(dictAll).forEach(key => {
if (dictA[key] && dictB[key]) x += dictA[key] * dictB[key]
})
let yA = getY(dictA)
let yB = getY(dictB)
const result = x / (yA * yB)
console.log(result)
}
最后运行我们的分析算法处理上一步的变量名即可:
➜ node analyse vue@2.4.1 vue@2.4.2
0.9436438155995188
实验结果与总结
一系列铺垫以后,终于到了检验真理的时候了。我们首先基于【相似版本相似度高】的假设,验证 Vue 是否符合这一假设:
➜ node analyse vue@2.4.1 vue@2.4.2
0.9436438155995188
可以看到,目前最新的 vue 2.4.2 与 2.4.1 之间,确实存在着很高的相似度。接下来比较 vue 最新版与 2.0.0 同一个 Major 版本之间的相似度:
➜ node analyse vue@2.0.0 vue@2.4.2
0.8838059164881868
相似度有所降低,说明最新版比起去年的 V2,已经有了不小的改动了。再来比较 V2 与 V1 系列的相似度:
➜ node analyse vue@2.0.0 vue@1.0.28
0.5883193867742227
相似度明显降低,显然重构之言非虚。最后比较 Vue 的最新版与第一个版本:
➜ node analyse vue@2.4.2 vue@0.6.0
0.4590386014371645
这是 Vue 家族中最低的相似度,也达到了 0.45 的水平。接下来是正戏,比较 Vue 和 Angular 的最新版:
➜ node analyse vue@2.4.2 angular@4.3.3
0.19322280449484375
区区 0.19 的相似度!好吧,Angular 最新版也是重构过的,我们不妨直接比较最早【照着 Angular 抄的】的 Vue 和 Angular 1.x 系列:
➜ node analyse vue@0.6.0 angular@1.2.32
0.294527560626686
这个相似度也大幅低于 Vue 全系列纵向对比的相似度!为了更有效地对比,我们让隔壁 React 躺个枪(未加版本号代表最新版):
➜ node analyse vue@2.4.2 react
0.27592736925848194
0.27 与 0.29 的对比,说明即便是最早阶段(与 Angular 相似度最高)的 Vue,相似度也仅仅相当于现在的 Vue 和 React 而已!为了保证公平,我们让 jQuery 也来凑个热闹:
➜ node analyse jquery angular@1.2.32
0.2508302720623658
这也是不到 0.3 的相似度,据此我们甚至可以得出一个大胆的结论:Vue 和 Angular 的相似度,和 Angular 与 jQuery 之间的相似度接近!没有人会认为 jQuery 与 Angular 之间存在抄袭吧?
当然,Vue 和 Angular 的相似度是客观存在的。我们在前端领域,可以找到另一对这样的例子:jQuery VS Zepto,它们之间的相似度如何呢?
➜ node analyse jquery zepto
0.25994377334635854
这个相似度和 Angular VS jQuery 几乎相同,这说明即便设计理念相近,具体实现不同的原创框架之间,相似度也是很低的。Vue VS Angular 也完全符合这一结论。
hmmm 目前我们的论据已经比较充分了。最后,我们比较一种情形:设计理念完全不同的原创框架之间,相似度如何?我们拉出 jQuery 和 React:
➜ node analyse jquery react
0.1007248324385447
全场最低相似度…所以我们可以理解 jQuery 时代的前端转向 React 时有多么不习惯了吧😂
到此为止,我们的结论有:
- Vue 系列迭代间相似度较高。
- 即便是最早的 Vue,与经典 Angular 的相似度也很低。
- 最新 Vue 与最新 Angular 之间,相似度更低,说明二者的发展道路早已更加独立。
- 即便设计理念相近,具体实现不同的原创框架之间,相似度也很低。
- React 与 jQuery 的相似度特别特别低(离题了)。
据此,笔者有理由认为【Vue 抄袭了 Angular】的论点是站不住脚的。
本文的实验数据托管在 Github 上,欢迎感兴趣的同学验证并改进这些结论。最后,框架毕竟只是工具,相互撕逼并不利于社区的发展。引用我司 Boss 的观点:【一流的人做事,二流的人去评论,三流的人去评论别人的评论】,希望大家能把口水战的时间放在更务实的事情上,推动技术水平、社区氛围和平均工资的上升……