知乎sign加密算法反混淆
源码来自 新版知乎x-zse-86加密破解分析 ,在添加了jsdom之后就可以通过nodejs运行了,但这在使用非js语言编写爬虫时肯定不是一个很好的调用方法,也有很大的局限性,在简单分析后,jsdom应该是提供一些属性变量如window,加密算法可能与之无关,不能运行可能是因为某些代码做了检测然后被反爬了,如今日头条的signature算法,仅能通过nodejs运行而无法通过execjs等js引擎运行,因此尝试对源码进行解析和反混淆,试试精简后能不能去掉jsdom,首先贴一下源码以便读者查看
1 const jsdom = require("jsdom"); 2 const {JSDOM} = jsdom; 3 const { window } = new JSDOM('<!doctype html><html><body></body></html>'); 4 global.window = window; 5 6 7 function t(e) { 8 return (t = "function" == typeof Symbol && "symbol" == typeof Symbol.A ? function(e) { 9 return typeof e 10 } 11 : function(e) { 12 return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e 13 } 14 )(e) 15 } 16 Object.defineProperty(exports, "__esModule", { 17 value: !0 18 }); 19 var A = "2.0" 20 , __g = {}; 21 function s() {} 22 function i(e) { 23 this.t = (2048 & e) >> 11, 24 this.s = (1536 & e) >> 9, 25 this.i = 511 & e, 26 this.h = 511 & e 27 } 28 function h(e) { 29 this.s = (3072 & e) >> 10, 30 this.h = 1023 & e 31 } 32 function a(e) { 33 this.a = (3072 & e) >> 10, 34 this.c = (768 & e) >> 8, 35 this.n = (192 & e) >> 6, 36 this.t = 63 & e 37 } 38 function c(e) { 39 this.s = e >> 10 & 3, 40 this.i = 1023 & e 41 } 42 function n() {} 43 function e(e) { 44 this.a = (3072 & e) >> 10, 45 this.c = (768 & e) >> 8, 46 this.n = (192 & e) >> 6, 47 this.t = 63 & e 48 } 49 function o(e) { 50 this.h = (4095 & e) >> 2, 51 this.t = 3 & e 52 } 53 function r(e) { 54 this.s = e >> 10 & 3, 55 this.i = e >> 2 & 255, 56 this.t = 3 & e 57 } 58 s.prototype.e = function(e) { 59 e.o = !1 60 } 61 , 62 i.prototype.e = function(e) { 63 switch (this.t) { 64 case 0: 65 e.r[this.s] = this.i; 66 break; 67 case 1: 68 e.r[this.s] = e.k[this.h] 69 } 70 } 71 , 72 h.prototype.e = function(e) { 73 e.k[this.h] = e.r[this.s] 74 } 75 , 76 a.prototype.e = function(e) { 77 switch (this.t) { 78 case 0: 79 e.r[this.a] = e.r[this.c] + e.r[this.n]; 80 break; 81 case 1: 82 e.r[this.a] = e.r[this.c] - e.r[this.n]; 83 break; 84 case 2: 85 e.r[this.a] = e.r[this.c] * e.r[this.n]; 86 break; 87 case 3: 88 e.r[this.a] = e.r[this.c] / e.r[this.n]; 89 break; 90 case 4: 91 e.r[this.a] = e.r[this.c] % e.r[this.n]; 92 break; 93 case 5: 94 e.r[this.a] = e.r[this.c] == e.r[this.n]; 95 break; 96 case 6: 97 e.r[this.a] = e.r[this.c] >= e.r[this.n]; 98 break; 99 case 7: 100 e.r[this.a] = e.r[this.c] || e.r[this.n]; 101 break; 102 case 8: 103 e.r[this.a] = e.r[this.c] && e.r[this.n]; 104 break; 105 case 9: 106 e.r[this.a] = e.r[this.c] !== e.r[this.n]; 107 break; 108 case 10: 109 e.r[this.a] = t(e.r[this.c]); 110 break; 111 case 11: 112 e.r[this.a] = e.r[this.c]in e.r[this.n]; 113 break; 114 case 12: 115 e.r[this.a] = e.r[this.c] > e.r[this.n]; 116 break; 117 case 13: 118 e.r[this.a] = -e.r[this.c]; 119 break; 120 case 14: 121 e.r[this.a] = e.r[this.c] < e.r[this.n]; 122 break; 123 case 15: 124 e.r[this.a] = e.r[this.c] & e.r[this.n]; 125 break; 126 case 16: 127 e.r[this.a] = e.r[this.c] ^ e.r[this.n]; 128 break; 129 case 17: 130 e.r[this.a] = e.r[this.c] << e.r[this.n]; 131 break; 132 case 18: 133 e.r[this.a] = e.r[this.c] >>> e.r[this.n]; 134 break; 135 case 19: 136 e.r[this.a] = e.r[this.c] | e.r[this.n]; 137 break; 138 case 20: 139 e.r[this.a] = !e.r[this.c] 140 } 141 } 142 , 143 c.prototype.e = function(e) { 144 e.Q.push(e.C), 145 e.B.push(e.k), 146 e.C = e.r[this.s], 147 e.k = []; 148 for (var t = 0; t < this.i; t++) 149 e.k.unshift(e.f.pop()); 150 e.g.push(e.f), 151 e.f = [] 152 } 153 , 154 n.prototype.e = function(e) { 155 e.C = e.Q.pop(), 156 e.k = e.B.pop(), 157 e.f = e.g.pop() 158 } 159 , 160 e.prototype.e = function(e) { 161 switch (this.t) { 162 case 0: 163 e.u = e.r[this.a] >= e.r[this.c]; 164 break; 165 case 1: 166 e.u = e.r[this.a] <= e.r[this.c]; 167 break; 168 case 2: 169 e.u = e.r[this.a] > e.r[this.c]; 170 break; 171 case 3: 172 e.u = e.r[this.a] < e.r[this.c]; 173 break; 174 case 4: 175 e.u = e.r[this.a] == e.r[this.c]; 176 break; 177 case 5: 178 e.u = e.r[this.a] != e.r[this.c]; 179 break; 180 case 6: 181 e.u = e.r[this.a]; 182 break; 183 case 7: 184 e.u = !e.r[this.a] 185 } 186 } 187 , 188 o.prototype.e = function(e) { 189 switch (this.t) { 190 case 0: 191 e.C = this.h; 192 break; 193 case 1: 194 e.u && (e.C = this.h); 195 break; 196 case 2: 197 e.u || (e.C = this.h); 198 break; 199 case 3: 200 e.C = this.h, 201 e.w = null 202 } 203 e.u = !1 204 } 205 , 206 r.prototype.e = function(e) { 207 switch (this.t) { 208 case 0: 209 for (var t = [], n = 0; n < this.i; n++) 210 t.unshift(e.f.pop()); 211 e.r[3] = e.r[this.s](t[0], t[1]); 212 break; 213 case 1: 214 for (var r = e.f.pop(), o = [], i = 0; i < this.i; i++) 215 o.unshift(e.f.pop()); 216 e.r[3] = e.r[this.s][r](o[0], o[1]); 217 break; 218 case 2: 219 for (var a = [], s = 0; s < this.i; s++) 220 a.unshift(e.f.pop()); 221 e.r[3] = new e.r[this.s](a[0],a[1]) 222 } 223 } 224 ; 225 var k = function(e) { 226 for (var t = 66, n = [], r = 0; r < e.length; r++) { 227 var o = 24 ^ e.charCodeAt(r) ^ t; 228 n.push(String.fromCharCode(o)), 229 t = o 230 } 231 return n.join("") 232 }; 233 function Q(e) { 234 this.t = (4095 & e) >> 10, 235 this.s = (1023 & e) >> 8, 236 this.i = 1023 & e, 237 this.h = 63 & e 238 } 239 function C(e) { 240 this.t = (4095 & e) >> 10, 241 this.a = (1023 & e) >> 8, 242 this.c = (255 & e) >> 6 243 } 244 function B(e) { 245 this.s = (3072 & e) >> 10, 246 this.h = 1023 & e 247 } 248 function f(e) { 249 this.h = 4095 & e 250 } 251 function g(e) { 252 this.s = (3072 & e) >> 10 253 } 254 function u(e) { 255 this.h = 4095 & e 256 } 257 function w(e) { 258 this.t = (3840 & e) >> 8, 259 this.s = (192 & e) >> 6, 260 this.i = 63 & e 261 } 262 function G() { 263 this.r = [0, 0, 0, 0], 264 this.C = 0, 265 this.Q = [], 266 this.k = [], 267 this.B = [], 268 this.f = [], 269 this.g = [], 270 this.u = !1, 271 this.G = [], 272 this.b = [], 273 this.o = !1, 274 this.w = null, 275 this.U = null, 276 this.F = [], 277 this.R = 0, 278 this.J = { 279 0: s, 280 1: i, 281 2: h, 282 3: a, 283 4: c, 284 5: n, 285 6: e, 286 7: o, 287 8: r, 288 9: Q, 289 10: C, 290 11: B, 291 12: f, 292 13: g, 293 14: u, 294 15: w 295 } 296 } 297 Q.prototype.e = function(e) { 298 switch (this.t) { 299 case 0: 300 e.f.push(e.r[this.s]); 301 break; 302 case 1: 303 e.f.push(this.i); 304 break; 305 case 2: 306 e.f.push(e.k[this.h]); 307 break; 308 case 3: 309 e.f.push(k(e.b[this.h])) 310 } 311 } 312 , 313 C.prototype.e = function(A) { 314 switch (this.t) { 315 case 0: 316 var t = A.f.pop(); 317 A.r[this.a] = A.r[this.c][t]; 318 break; 319 case 1: 320 var s = A.f.pop() 321 , i = A.f.pop(); 322 A.r[this.c][s] = i; 323 break; 324 case 2: 325 var h = A.f.pop(); 326 A.r[this.a] = eval(h) 327 } 328 } 329 , 330 B.prototype.e = function(e) { 331 e.r[this.s] = k(e.b[this.h]) 332 } 333 , 334 f.prototype.e = function(e) { 335 e.w = this.h 336 } 337 , 338 g.prototype.e = function(e) { 339 throw e.r[this.s] 340 } 341 , 342 u.prototype.e = function(e) { 343 var t = this 344 , n = [0]; 345 e.k.forEach(function(e) { 346 n.push(e) 347 }); 348 var r = function(r) { 349 var o = new G; 350 return o.k = n, 351 o.k[0] = r, 352 o.v(e.G, t.h, e.b, e.F), 353 o.r[3] 354 }; 355 r.toString = function() { 356 return "() { [native code] }" 357 } 358 , 359 e.r[3] = r 360 } 361 , 362 w.prototype.e = function(e) { 363 switch (this.t) { 364 case 0: 365 for (var t = {}, n = 0; n < this.i; n++) { 366 var r = e.f.pop(); 367 t[e.f.pop()] = r 368 } 369 e.r[this.s] = t; 370 break; 371 case 1: 372 for (var o = [], i = 0; i < this.i; i++) 373 o.unshift(e.f.pop()); 374 e.r[this.s] = o 375 } 376 } 377 , 378 G.prototype.D = function(e) { 379 for (var t = new Buffer(e,"base64").toString("binary"), n = t.charCodeAt(0) << 8 | t.charCodeAt(1), r = [], o = 2; o < n + 2; o += 2) 380 r.push(t.charCodeAt(o) << 8 | t.charCodeAt(o + 1)); 381 this.G = r; 382 for (var i = [], a = n + 2; a < t.length; ) { 383 var s = t.charCodeAt(a) << 8 | t.charCodeAt(a + 1) 384 , c = t.slice(a + 2, a + 2 + s); 385 i.push(c), 386 a += s + 2 387 } 388 this.b = i 389 } 390 , 391 G.prototype.v = function(e, t, n) { 392 for (t = t || 0, 393 n = n || [], 394 this.C = t, 395 "string" == typeof e ? this.D(e) : (this.G = e, 396 this.b = n), 397 this.o = !0, 398 this.R = Date.now(); this.o; ) { 399 var r = this.G[this.C++]; 400 if ("number" != typeof r) 401 break; 402 var o = Date.now(); 403 if (500 < o - this.R) 404 return; 405 this.R = o; 406 try { 407 this.e(r) 408 } catch (e) { 409 this.U = e, 410 this.w && (this.C = this.w) 411 } 412 } 413 } 414 , 415 G.prototype.e = function(e) { 416 var t = (61440 & e) >> 12; 417 new this.J[t](e).e(this) 418 } 419 , 420 "undefined" != typeof window && (new G).v("AxjgB5MAnACoAJwBpAAAABAAIAKcAqgAMAq0AzRJZAZwUpwCqACQACACGAKcBKAAIAOcBagAIAQYAjAUGgKcBqFAuAc5hTSHZAZwqrAIGgA0QJEAJAAYAzAUGgOcCaFANRQ0R2QGcOKwChoANECRACQAsAuQABgDnAmgAJwMgAGcDYwFEAAzBmAGcSqwDhoANECRACQAGAKcD6AAGgKcEKFANEcYApwRoAAxB2AGcXKwEhoANECRACQAGAKcE6AAGgKcFKFANEdkBnGqsBUaADRAkQAkABgCnBagAGAGcdKwFxoANECRACQAGAKcGKAAYAZx+rAZGgA0QJEAJAAYA5waoABgBnIisBsaADRAkQAkABgCnBygABoCnB2hQDRHZAZyWrAeGgA0QJEAJAAYBJwfoAAwFGAGcoawIBoANECRACQAGAOQALAJkAAYBJwfgAlsBnK+sCEaADRAkQAkABgDkACwGpAAGAScH4AJbAZy9rAiGgA0QJEAJACwI5AAGAScH6AAkACcJKgAnCWgAJwmoACcJ4AFnA2MBRAAMw5gBnNasCgaADRAkQAkABgBEio0R5EAJAGwKSAFGACcKqAAEgM0RCQGGAYSATRFZAZzshgAtCs0QCQAGAYSAjRFZAZz1hgAtCw0QCQAEAAgB7AtIAgYAJwqoAASATRBJAkYCRIANEZkBnYqEAgaBxQBOYAoBxQEOYQ0giQKGAmQABgAnC6ABRgBGgo0UhD/MQ8zECALEAgaBxQBOYAoBxQEOYQ0gpEAJAoYARoKNFIQ/zEPkAAgChgLGgkUATmBkgAaAJwuhAUaCjdQFAg5kTSTJAsQCBoHFAE5gCgHFAQ5hDSCkQAkChgBGgo0UhD/MQ+QACAKGAsaCRQCOYGSABoAnC6EBRoKN1AUEDmRNJMkCxgFGgsUPzmPkgAaCJwvhAU0wCQFGAUaCxQGOZISPzZPkQAaCJwvhAU0wCQFGAUaCxQMOZISPzZPkQAaCJwvhAU0wCQFGAUaCxQSOZISPzZPkQAaCJwvhAU0wCQFGAkSAzRBJAlz/B4FUAAAAwUYIAAIBSITFQkTERwABi0GHxITAAAJLwMSGRsXHxMZAAk0Fw8HFh4NAwUABhU1EBceDwAENBcUEAAGNBkTGRcBAAFKAAkvHg4PKz4aEwIAAUsACDIVHB0QEQ4YAAsuAzs7AAoPKToKDgAHMx8SGQUvMQABSAALORoVGCQgERcCAxoACAU3ABEXAgMaAAsFGDcAERcCAxoUCgABSQAGOA8LGBsPAAYYLwsYGw8AAU4ABD8QHAUAAU8ABSkbCQ4BAAFMAAktCh8eDgMHCw8AAU0ADT4TGjQsGQMaFA0FHhkAFz4TGjQsGQMaFA0FHhk1NBkCHgUbGBEPAAFCABg9GgkjIAEmOgUHDQ8eFSU5DggJAwEcAwUAAUMAAUAAAUEADQEtFw0FBwtdWxQTGSAACBwrAxUPBR4ZAAkqGgUDAwMVEQ0ACC4DJD8eAx8RAAQ5GhUYAAFGAAAABjYRExELBAACWhgAAVoAQAg/PTw0NxcQPCQ5C3JZEBs9fkcnDRcUAXZia0Q4EhQgXHojMBY3MWVCNT0uDhMXcGQ7AUFPHigkQUwQFkhaAkEACjkTEQspNBMZPC0ABjkTEQsrLQ=="); 421 var b = function(e) { 422 console.log(encodeURIComponent(e)); 423 return __g._encrypt(encodeURIComponent(e)); 424 }; 425 426 exports.ENCRYPT_VERSION = A, 427 exports.default = b; 428 429
不得不说,随着技术的发展,各种各样的稀奇古怪的混淆层出不穷,堪称卷积云,笔者的想要了解一下混淆的细节,并尽可能地将源码变成人话,并让更多人了解常见的混淆法,于是撰写了本文,如有错误,欢迎指出和讨论,谢谢
如读者也尝试执行以下的每一步更改,强烈建议每修改一部分就执行一次代码以确认是否能正常运行
逗号表达式是能够被正确编译的,因为可以把相邻表达式全部写到一行里面,因此即便写成多行,也无法分开断点调试,这是为了加大调试难度而做的一个混淆操作,因此建议先全部改为语法建议的分号表达式
改为
通览全文,发现存在三大类主要符号
第一类为函数,例如【function i()】中的【i】,为了便于区分,笔者将所有函数重命名为【Func_i】的格式,可以在【function G()】中的【this.J】找到大部分的函数,这时候,需要手动替换所有函数!!!不能借助IDE的替换功能替换,因为IDE替换会导致变量也被替换了,也就是说IDE会识别错误,因此这步替换对后面的分析有很大的帮助,能更清晰地看懂关系,第一次肯定是替换不全的,运行程序然后通过报错再逐个修改
替换完会得到如下图所示效果
第二类为局部变量,如第一类图中的【e】、【t】、【s】、【i】和【h】,这些变量暂时不需要去变动,因为已经能和函数区分开了,不过因为原型prototype的字母也是e,因此将形参e改为data,如下图所示
第三类为原型prototype,这段代码中所有的prototype的命名都是【e】,如果使用IDE,原型e的颜色和局部变量【e】的颜色是不同的,因为只有一个特定的字母,注意即可,不需变动,那么肯定有人会问,原型是什么啊,文末给出的链接中很清晰地说明了原型的意义和用法,不过简单来说,可以将function理解为class,那么原型就是方法,如果用python来实现,就如下图所示,效果完全相同
既然没有区别,那么就将原型都放入函数中方便理解,同时在调试中不会反复跳跃(因为代码中是分散开的),调整后如下图所示
这时,看下代码建议,会发现代码中使用了大量的【var】,建议改为【let】即局部变量,这个修改对理解变量的作用范围也很有帮助,在替换的同时,很多地方可以做改进,读者可以自行判断后修改,比如
上图中的【var t】、【var o】和【var a】的作用是完全相同的,因此可以提出来并重命名,变成如下所示
替换完,还有少部分的代码建议提示,比如将【==】更改为【===】,将【!=】更改为【!==】,同时,有一些简单的混淆也可以替换一下,比如【!0】改为【true】、【!1】改为【false】,这时,再回顾一下代码,会发现有个函数叫【k】
这个函数的写法和其他的不一样,但是,效果是一样的,那么,也调整过来
然后还有个函数叫【t】,初看很复杂,分析后发现,只要【"function" == typeof Symbol】不成立,整个函数就返回【false】,当然等式应该是成立的,不过作为尝试,直接将函数改为下图所示
居然一切正常,这就非常有意思了,减少了一大段代码,不过没有进一步分析,读者有兴趣可以尝试进一步分析一下
此时还会有一些未被引用的变量,去除掉即可,比如【Func_e】中的【this.n】和【Func_G】中的【this.R】
首先,调试程序遇到的第一个问题,入口在哪里,在简单地分析和打断点之后发现,计算加密的入口在【Func_u】中的【r】处,但是,如何直接调用这个函数却不触发外层函数,笔者水平有限,暂无法解释,不过,在到达入口的时候,程序已经完成了一次初始化,即【(new G).v()】处,里面有非常长的一个字符串,这个就是初始化用的数据,在反复请求网页后发现,这是一个定值,那么,就打断点逐步分析
在【v】函数里面,又用了和前面相同的方式即用【for】初始化变量,同时,读者可能会注意到一个很有意思的地方,js的传参数量对不上了,【v】仅有一个参数,怎么对应【(e, t, n)】呢,实参和形参中,以数量少的一方为准,多的就被舍弃,空缺的就是undefined,因此,初始化的时候会进到【D】函数,之后计算则将 【e、t、n】 赋值给 【this.G、this.c、this.b】,其中有一个【this.R】记录了当前时间,如果在500ms内没有到计算完,则会直接结束计算,只是一个反爬的手段,因此直接去掉即可,而【this.o】为true,放在for的第二个参数位,即等价于while(true),替换即可,这时因为还需要初始化,所以设置了一个字符串【start!】,后面可以去掉
之后就进到了【D】函数,第一行就开幕雷击,连续的base64转换、二进制转换、移位操作、Unicode码转换,不过目的是获得【Func_G.G】,之后又获得了【Func_G.b】,也就是【D】函数执行了一个初始化的操作,那直接打断点提取出两个的值然后写定即可,省去了不少计算量,如下图所示
那么肯定会有疑惑,这些数有意义还是只是纯粹的随机数,本想说自己打完断点就知道了,不过就不卖关子了,【G】应该叫做无意义数,先设定好执行的顺序,然后用移位反推出一个合适的数据写入,执行的时候移位还原出结果然后在【J】中调用,而【b】生成了很多函数或者说变量名,在【Func_k】处打断点就可以看到,长的数组都对应着一些变量名,其中包括【window】和【_encrypt】等,【_encrypt】也是加密计算的入口,这个在源码中如何都找不到的函数是如何加入并执行的,笔者才疏学浅,就不深究了,不过应该和【Func_u】中的【native code】有一定关系,不过好的是,后面都能去掉
分析完初始化,就进入到正在的计算流程了,即上文提到的【while(true)】,提到【while】,那必定有结束信号,即判断【r】是否越界,笔者尝试直接用for遍历,但发现是不行的,因为在计算过程中,【C】的值会变动,因此还是只能用【while】,然后就是将【r】代入原型【e】通过【J】选取一个函数并计算,这时会有一个try,观察可发现catch后的内容不影响结果,直接删除,同时【Func_g】中有throw,也可以去掉,最终得到下图的结果
上文提到了【native code】和入口【Func_u.r】,于是尝试对这一部分打断点分析一下,上文提到过是直接跳转到【r】函数的,那么这意味着前面这一段是否意味着不需要呢
尝试删掉后一切正常,有意思,然后注意到这一段
return有好几个值,用quokka测试后发现,这种写法等价于执行最后一个逗号之前的代码,然后返回最后一项,同时,【o.v】的形参只有三项,那么可以直接去掉【data.F】,然后就是【toString】,抱着去掉试试的想法删掉了,一切正常,那就更有意思了,不过全部去掉之后的【data】就是【e】的形参了,其中有这两行代码【o.k = n, o.k[0] = r】,通过断点可以知道就是【o.k = [r]】,而这时的【r】是无法传进来的,因此在【e】的后面增加一个形参【val】,然后代码就变成了下图所示
这时还缺少的就是val的初始值,在修改前打断点可得知,是7,于是直接写死,得到
全部修改完了,这时还需要jsdom吗,去掉试试,居然能直接运行了,善哉,虽然不确定是哪一步使得这个可以被省去了,但是能运行就是好事了
那么还需要初始化那一遍吗,也不需要了啊,直接走起了,也就是可以把【start】删掉了
把程序跑起来,会发现,生成的signature多了个后缀【_XUX】,应该是一个反爬机制或者是简化程序的时候某一步出现了差错,不过不是很重要了,直接去掉即可,有兴趣的读者可以自行研究一下如何不产生这个后缀
至此,程序已经变得非常通俗易懂了,也不存在晦涩的混淆代码了,笔者尽量不跳步骤地解释了格式化的步骤,不过文章是代码完成后写的,所以可能有部分地方顺序存在问题,读者需要自行思考下
笔者尝试将代码迁移到python,以尝试完全理解加密算法的细节,但是迁移完成后发现源码使用了大量的js特性,python实现难度非常大,需要自己实现很多底层操作,于是咕咕咕,比如,向数组中新增元素的时候,js可以直接向任意越界的位置新增,空缺的位置自动用undefined填充
如有读者有兴趣,可以尝试迁移和修改,代码在github上开源,此代码仅供学习和研究反混淆使用,请勿用于其他用途或对他人造成困扰,非常感谢!
https://github.com/Pyrokine/zhihu_encrypt
感谢: 新版知乎x-zse-86加密破解分析
https://blog.csdn.net/weixin_40352715/article/details/107546166
JS 中 __proto__ 和 prototype 存在的意义是什么? https://www.zhihu.com/question/56770432
你可以不会 class,但是一定要学会 prototype
https://zhuanlan.zhihu.com/p/35279244