首页 文章 精选 留言 我的

精选列表

搜索[javascript],共9004篇文章
优秀的个人博客,低调大师

JavaScript小游戏,键盘英雄!

该游戏仅供探讨交流。 该游戏可随意传播,请保留声明与出处。 几年前的一个夜晚孤枕难眠,突然来了灵感。创作游戏的初衷是巩固js基础,有朋友问我,为什么取这个名字呢? 可以想象你是电影的主角,打字的速度已达到独孤求败的境界,狂风呼啸电闪雷鸣,键盘声响起,键盘在你指尖灵动肆意纷飞,眨眼的功夫一部20万字撰写完毕。 生活不止眼前的苟且,还有诗和代码!当然这也是需要有想象力的。 欢迎有更多想象力的朋友分享思维,在这款游戏上面继续扩展。 html部分可以用js来生成,其中加入了H5中的audio元素,按键音效,后续还可以增加背景音乐,或者再来一个升级音效,一直没有增加的原因是,没有找到满意的音乐,看来想做一个全能大神还是有难度的,哈哈!css建议用BEM或OOCSS设计模式便于维护和模块化功能扩展。js注释的很明白,还可以添加很多功能,得分也可以更加复杂,可以生成随机数0~99,或增加生成黄色按键几率10%得分10,或增加生成蓝色按键几率5%得分30,增加趣味性,只需要调整升级机制。越来越有趣了,有时候开发要保持童心不是么? 完整项目地址:https://github.com/af66666/typing_hero.git 上菜: html↓ <div id="pg"> <div> <div id="esc">Esc</div> </div> <div id="key"> <div class="white">q</div> <div class="white">w</div> <div class="white">e</div> <div class="white">r</div> <div class="white">t</div> <div class="white">y</div> <div class="white">u</div> <div class="white">i</div> <div class="white">o</div> <div class="white">p</div> <div class="white">[</div> <div class="white">]</div> <div class="white">a</div> <div class="white">s</div> <div class="white">d</div> <div class="white">f</div> <div class="white">g</div> <div class="white">h</div> <div class="white">j</div> <div class="white">k</div> <div class="white">l</div> <div class="white">;</div> <div class="white">'</div> <div class="white">z</div> <div class="white">x</div> <div class="white">c</div> <div class="white">v</div> <div class="white">b</div> <div class="white">n</div> <div class="white">m</div> <div class="white">,</div> <div class="white">.</div> <div class="white">/</div> </div> <p id="score">-</p> <p id="level">-</p> <i id="go"></i> </div> <!--<audio id="music" src="18839369087.mp3"></audio>--> css↓ #pg { width: 800px; height: 600px; margin: 0 auto; position: relative; background-image:linear-gradient(60deg,#62C292 0%,#F8CBAD 25%,#62C292 50%,#F8CBAD 75%,#62C292 100%); } #pg:before { content: ""; display: table; } #pg>div { margin: 20px; overflow: hidden; } #pg>div+div { margin: 80px 0; padding: 20px 0; } #pg div div { float: left; width: 40px; height: 40px; border: 5px solid #000; border-radius: 5px; text-align: center; line-height: 40px; font-size: 22px; font-family: helvetica; font-weight: bold; margin-right: 2px; margin-bottom: 2px; cursor: pointer; } #key div:first-child { margin-left: 88px; } #key div:nth-child(13) { margin-left: 108px; } #key div:nth-child(24) { margin-left: 128px; } #pg p { position: absolute; top: 390px; left: 280px; font-family: helvetica; font-size: 36px; font-weight: bold; color: #fff; opacity: 0.5; cursor: pointer; } #pg p+p { top: 430px; } .red { background: #f00; opacity: 0.5; color: #fff; } #go { display: none; width: 800px; height: 600px; position: absolute; top: 0; left: 0; text-align: center; line-height: 600px; font-size: 160px; font-weight: bold; font-family: helvetica; color: red; cursor: pointer; transition: font-size 0.5s ease-out; } #go:hover { font-size: 200px; } js↓ var destroy = { state: 0, //游戏状态 timer: null, //游戏引擎,主定时器 level: 1, //等级 sc: 0, //积分 many: 0, //消除的个数 MANYS: 20, //升级需要消除的个数 interval: 500, //红色生成的速度 LINTERVAL: 50, //红色生成速度加快 MIN: 100, //生成红色的最快速度 READY: 0, //就绪状态 RUNNING: 1, //正在游戏中 PAUSE: 2, //暂停状态 GAMEOVER: 3, //游戏结束 //就绪状态 ready: function() { this.state = this.READY; //就绪状态 this.sc = 0; //游戏初始化 this.many = 0; //游戏初始化 this.level = 1; //游戏初始化 this.interval = 500; //游戏初始化 esc.style.color = '#00ffff'; go.innerHTML = 'ready'; go.style.background = '#f7f7f7'; go.style.display = 'block'; go.onclick = function() { go.innerHTML = ' '; go.style.display = 'none'; destroy.start(); //destroy.playAudio(); } score.innerHTML = 'SCORE:' + this.sc; level.innerHTML = 'LEVEL:' + this.level; }, //游戏进行中 start: function() { this.state = this.RUNNING; var ds = key.querySelectorAll('div'); this.timer = setInterval(function() { var white = key.querySelectorAll('div.white'); var tmp = white.length; var n = parseInt(Math.random() * tmp); if(tmp > 0){ white[n].className = "red"; }else{ destroy.gameOver(); } for(var r = 0; r < ds.length; r++) { ds[r].style.transform = ''; } }, this.interval); esc.onclick = function() { destroy.pause(); //destroy.playAudio(); } document.onkeydown = function(e) { switch(e.keyCode) { case 27: esc.style.color = '#f00'; destroy.pause(); //destroy.playAudio(); break; case 219: if(ds[10].className == 'red') { ds[10].className = 'white'; ds[10].style.transform = 'rotate(30deg)'; destroy.createLevel(); //destroy.playAudio(); } break; case 221: if(ds[11].className == 'red') { ds[11].className = 'white'; ds[11].style.transform = 'rotate(30deg)'; destroy.createLevel(); //destroy.playAudio(); } break; case 186: if(ds[21].className == 'red') { ds[21].className = 'white'; ds[21].style.transform = 'rotate(30deg)'; destroy.createLevel(); //destroy.playAudio(); } break; case 222: if(ds[22].className == 'red') { ds[22].className = 'white'; ds[22].style.transform = 'rotate(30deg)'; destroy.createLevel(); //destroy.playAudio(); } break; case 188: if(ds[30].className == 'red') { ds[30].className = 'white'; ds[30].style.transform = 'rotate(30deg)'; destroy.createLevel(); //destroy.playAudio(); } break; case 190: if(ds[31].className == 'red') { ds[31].className = 'white'; ds[31].style.transform = 'rotate(30deg)'; destroy.createLevel(); //destroy.playAudio(); } break; case 191: if(ds[32].className == 'red') { ds[32].className = 'white'; ds[32].style.transform = 'rotate(30deg)'; destroy.createLevel(); //destroy.playAudio(); } break; } for(var i = 0; i < ds.length; i++) { if(String.fromCharCode(e.which).toLowerCase() == ds[i].innerHTML) { if(ds[i].className == 'red') { ds[i].className = 'white'; ds[i].style.transform = 'scale(1.8)'; destroy.createLevel(); //destroy.playAudio(); } } } var num = e.keyCode; //检测 var en = String.fromCharCode(e.which); //检测 console.log(num); //检测 console.log(String(en)); //检测 } }, //暂停状态 pause: function() { this.state = this.PAUSE; //暂停状态 go.innerHTML = 'pause'; go.style.opacity = 0.5; go.style.display = 'block'; go.style.background = '#fff'; clearInterval(this.timer); document.onkeydown = function(e) { switch(e.keyCode) { case 27: esc.style.color = '#00ffff'; go.style.display = 'none'; destroy.start(); //destroy.playAudio(); } } }, //游戏结束 gameOver: function() { this.state = this.GAMEOVER; clearInterval(this.timer); go.style.opacity = 1; go.innerHTML = 'OVER'; go.style.display = 'block'; go.style.background = '#000'; this.sc = 0; //游戏初始化 this.level = 1; //游戏初始化 this.interval = 500; //游戏初始化 this.many = 0; //游戏初始化 document.onkeydown = function(e) { switch(e.keyCode) { case 83: go.style.display = 'none'; var ds = key.querySelectorAll('div'); for(var i = 0; i < ds.length; i++) { ds[i].className = 'white'; } score.innerHTML = 'SCORE:' + 0; level.innerHTML = 'LEVEL:' + 1; destroy.start(); //destroy.playAudio(); } }; go.onclick = function(){ go.style.display = 'none'; var ds = key.querySelectorAll('div'); for(var i = 0; i < ds.length; i++) { ds[i].className = 'white'; } score.innerHTML = 'SCORE:' + 0; level.innerHTML = 'LEVEL:' + 1; destroy.start(); //destroy.playAudio(); }; }, //等级得分机制 createLevel: function() { this.sc += 5; this.many++; if(this.many == this.MANYS) { this.many = 0; this.level++; if(this.interval >= this.MIN) { this.interval -= this.LINTERVAL; clearInterval(this.timer); //清除定时器 destroy.start(); //重新启动定时器 } } score.innerHTML = 'SCORE:' + this.sc; level.innerHTML = 'LEVEL:' + this.level; if(this.level >= 10) { level.innerHTML = '您已打破世界纪录'; } }, //音频 playAudio: function() { music.pause(); music.load(); music.play(); }, } //运行 window.onload = function() { destroy.ready(); } html部分可以用js来生成,其中加入了H5中的audio元素,按键音效,后续还可以增加背景音乐,或者再来一个升级音效,一直没有增加的原因是,没有找到满意的音乐,看来想做一个全能大神还是有难度的,哈哈!css建议用BEM或OOCSS设计模式便于维护和模块化扩展。js注释的很明白,还可以添加很多功能,得分也可以更加复杂,可以生成随机数0~99,或增加生成黄色按键几率10%得分10,或增加生成蓝色按键几率5%得分30,增加趣味性,只需要调整升级机制。越来越有趣了,有时候开发要保持童心不是么? 后续将继续改进扩展,欢迎指导交流!

优秀的个人博客,低调大师

JavaScript尾递归优化探索

尾调优化 在知道尾递归之前,我们要直到什么是尾调用优化,因为尾调用优化是尾递归的基础。尾调用就是:在函数的最后一步调用另一个函数。 function f() { return g() } ps:最后一步必须是之久调用另一函数,而不能是一个常量或是一个表达式,如 return y 或 return g() + 1 尾调用优化的原理是什么? 按照阮一峰老师在es6的函数扩展中的解释就是:函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。 这里的“调用帧”和“调用栈”,说的应该就是“执行环境”和“作用域链”。因为尾调用时函数的最后一部操作,所以不再需要保留外层的调用帧,而是直接取代外层的调用帧,所以可以起到一个优化的作用。 尾递归优化 我们知道,递归虽然使用起来方便,但是递归是在函数内部调用自身,当递归次数达到一定数量级的时候,他形成的调用栈的深度是很可怕的,很可能会发生“栈溢出”错误。尾递归优化,就是利用尾调用优化的原理,对递归进行优化。举一个很常见的例子: 求斐波那契数值 function fibonacci (n) { return n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2); } 此函数没有进行任何的优化,当我们在控制台去执行此函数的时候,fibonacci(40)就已经出现了明显的响应慢的问题,fibonacci(50)的时候浏览器卡死。 2. 优化 function fibonacci (n, ac1, ac2) { (ac1 = ac1 || 1), (ac2 = ac2 || 1); return n <= 1 ? ac2 :fibonacci(n - 1, ac2, ac1 + ac2); } 此优化有两个点:首先进行了算法上的优化,减少了很多重复的计算,时间复杂度大大降低;第二进行了尾递归优化,按理说不会发生“栈溢出”。我们可以到控制台中再尝试,发现速度的提升不是一般的快,证明第一个优化生效了,但是当我们允许fibonacci(10000)的时候,报错了:Uncaught RangeError: Maximum call stack size exceeded,这就说明我们的尾递归优化并没有生效。为什么呢? 局限性 上面说到,我们直接再浏览器的控制台中执行fibonacci(10000)的时候,发生了栈溢出,这是为什么呢?关于这一点,我目前查阅资料之后的理解就是,虽然es6已经提出了要实现尾递归优化,但是真正落地实现了尾递归优化的浏览器并不多。所以当我们使用尾递归进行优化的时候,依旧发生了“栈溢出”的错误。 蹦床函数 那怎么办呢?我们还有另一个方法去达到尾递归优化的效果,那就是使用蹦床函数(trampoline)。 function trampoline(f) { while (f && f instanceof Function) { f = f(); } return f; } 代码修改为返回一个新函数。 function fibonacci (n, ac1, ac2) { (ac1 = ac1 || 1), (ac2 = ac2 || 1); return n <= 1 ? ac2 :fibonacci.bind(null, n - 1, ac2, ac1 + ac2); } 两个函数结合就可以将递归状态为循环,栈溢出的问题也就解决了。 trampoline(fibonacci (100000)) // Infinity 原文发布时间为:2018年07月04日 原文作者: 掘金 本文来源: 掘金 如需转载请联系原作者

优秀的个人博客,低调大师

Javascript装饰器的妙用

最近新开了一个Node项目,采用TypeScript来开发,在数据库及路由管理方面用了不少的装饰器,发觉这的确是一个好东西。装饰器是一个还处于草案中的特性,目前木有直接支持该语法的环境,但是可以通过 babel 之类的进行转换为旧语法来实现效果,所以在TypeScript中,可以放心的使用@Decorator。 什么是装饰器 装饰器是对类、函数、属性之类的一种装饰,可以针对其添加一些额外的行为。通俗的理解可以认为就是在原有代码外层包装了一层处理逻辑。个人认为装饰器是一种解决方案,而并非是狭义的@Decorator,后者仅仅是一个语法糖罢了。 装饰器在身边的例子随处可见,一个简单的例子,水龙头上边的起泡器就是一个装饰器,在装上以后就会把空气混入水流中,掺杂很多泡泡在水里。但是起泡器安装与否对水龙头本身并没有什么影响,即使拆掉起泡器,也会照样工作,水龙头的作用在于阀门的控制,至于水中掺不掺杂气泡则不是水龙头需要关心的。 所以,对于装饰器,可以简单地理解为是非侵入式的行为修改。 为什么要用装饰器 可能有些时候,我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。 函数中的作用 可以想像一下,我们有一个工具类,提供了一个获取数据的函数: class Model1 { getData() { // 此处省略获取数据的逻辑 return [{ id: 1, name: 'Niko' }, { id: 2, name: 'Bellic' }] } } console.log(new Model1().getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ] console.log(Model1.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ] 现在我们想要添加一个功能,记录该函数执行的耗时。因为这个函数被很多人使用,在调用方添加耗时统计逻辑是不可取的,所以我们要在Model1中进行修改: class Model1 { getData() { + let start = new Date().valueOf() + try { // 此处省略获取数据的逻辑 return [{ id: 1, name: 'Niko' }, { id: 2, name: 'Bellic' }] + } finally { + let end = new Date().valueOf() + console.log(`start: ${start} end: ${end} consume: ${end - start}`) + } } } // start: XXX end: XXX consume: XXX console.log(new Model1().getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ] // start: XXX end: XXX consume: XXX console.log(Model1.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ] 这样在调用方法后我们就可以在控制台看到耗时的输出了。但是这样直接修改原函数代码有以下几个问题: 统计耗时的相关代码与函数本身逻辑并无一点关系,影响到了对原函数本身的理解,对函数结构造成了破坏性的修改 如果后期还有更多类似的函数需要添加统计耗时的代码,在每个函数中都添加这样的代码显然是低效的,维护成本太高 所以,为了让统计耗时的逻辑变得更加灵活,我们将创建一个新的工具函数,用来包装需要设置统计耗时的函数。通过将Class与目标函数的name传递到函数中,实现了通用的耗时统计: function wrap(Model, key) { // 获取Class对应的原型 let target = Model.prototype // 获取函数对应的描述符 let descriptor = Object.getOwnPropertyDescriptor(target, key) // 生成新的函数,添加耗时统计逻辑 let log = function (...arg) { let start = new Date().valueOf() try { return descriptor.value.apply(this, arg) // 调用之前的函数 } finally { let end = new Date().valueOf() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } // 将修改后的函数重新定义到原型链上 Object.defineProperty(target, key, { ...descriptor, value: log // 覆盖描述符重的value }) } wrap(Model1, 'getData') wrap(Model2, 'getData') // start: XXX end: XXX consume: XXX console.log(new Model1().getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ] // start: XXX end: XXX consume: XXX console.log(Model2.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ] 接下来,我们想控制其中一个Model的函数不可被其他人修改覆盖,所以要添加一些新的逻辑: function wrap(Model, key) { // 获取Class对应的原型 let target = Model.prototype // 获取函数对应的描述符 let descriptor = Object.getOwnPropertyDescriptor(target, key) Object.defineProperty(target, key, { ...descriptor, writable: false // 设置属性不可被修改 }) } wrap(Model1, 'getData') Model1.prototype.getData = 1 // 无效 可以看出,两个wrap函数中有不少重复的地方,而修改程序行为的逻辑,实际上依赖的是Object.defineProperty中传递的三个参数。所以,我们针对wrap在进行一次修改,将其变为一个通用类的转换: function wrap(decorator) { return function (Model, key) { let target = Model.prototype let dscriptor = Object.getOwnPropertyDescriptor(target, key) decorator(target, key, descriptor) } } let log = function (target, key, descriptor) { // 将修改后的函数重新定义到原型链上 Object.defineProperty(target, key, { ...descriptor, value: function (...arg) { let start = new Date().valueOf() try { return descriptor.value.apply(this, arg) // 调用之前的函数 } finally { let end = new Date().valueOf() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } }) } let seal = function (target, key, descriptor) { Object.defineProperty(target, key, { ...descriptor, writable: false }) } // 参数的转换处理 log = wrap(log) seal = warp(seal) // 添加耗时统计 log(Model1, 'getData') log(Model2, 'getData') // 设置属性不可被修改 seal(Model1, 'getData') 到了这一步以后,我们就可以称log和seal为装饰器了,可以很方便的让我们对一些函数添加行为。而拆分出来的这些功能可以用于未来可能会有需要的地方,而不用重新开发一遍相同的逻辑。 Class 中的作用 就像上边提到了,现阶段在JS中继承多个Class是一件头疼的事情,没有直接的语法能够继承多个 Class。 class A { say () { return 1 } } class B { hi () { return 2 } } class C extends A, B {} // Error class C extends A extends B {} // Error // 这样才是可以的 class C {} for (let key of Object.getOwnPropertyNames(A.prototype)) { if (key === 'constructor') continue Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(A.prototype, key)) } for (let key of Object.getOwnPropertyNames(B.prototype)) { if (key === 'constructor') continue Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(B.prototype, key)) } let c = new C() console.log(c.say(), c.hi()) // 1, 2 所以,在React中就有了一个mixin的概念,用来将多个Class的功能复制到一个新的Class上。大致思路就是上边列出来的,但是这个mixin是React中内置的一个操作,我们可以将其转换为更接近装饰器的实现。在不修改原Class的情况下,将其他Class的属性复制过来: function mixin(constructor) { return function (...args) { for (let arg of args) { for (let key of Object.getOwnPropertyNames(arg.prototype)) { if (key === 'constructor') continue // 跳过构造函数 Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key)) } } } } mixin(C)(A, B) let c = new C() console.log(c.say(), c.hi()) // 1, 2 以上,就是装饰器在函数、Class上的实现方法(至少目前是的),但是草案中还有一颗特别甜的语法糖,也就是@Decorator了。能够帮你省去很多繁琐的步骤来用上装饰器。 @Decorator的使用方法 草案中的装饰器、或者可以说是TS实现的装饰器,将上边的两种进一步地封装,将其拆分成为更细的装饰器应用,目前支持以下几处使用: Class 函数 get set访问器 实例属性、静态函数及属性 函数参数 @Decorator的语法规定比较简单,就是通过@符号后边跟一个装饰器函数的引用: @tag class A { @method hi () {} } function tag(constructor) { console.log(constructor === A) // true } function method(target) { console.log(target.constructor === A, target === A.prototype) // true, true } 函数tag与method会在class A定义的时候执行。 @Decorator 在 Class 中的使用 该装饰器会在class定义前调用,如果函数有返回值,则会认为是一个新的构造函数来替代之前的构造函数。 函数接收一个参数: constructor 之前的构造函数 我们可以针对原有的构造函数进行一些改造: 新增一些属性 如果想要新增一些属性之类的,有两种方案可以选择: 创建一个新的class继承自原有class,并添加属性 针对当前class进行修改 后者的适用范围更窄一些,更接近mixin的处理方式。 @name class Person { sayHi() { console.log(`My name is: ${this.name}`) } } // 创建一个继承自Person的匿名类 // 直接返回并替换原有的构造函数 function name(constructor) { return class extends constructor { name = 'Niko' } } new Person().sayHi() 修改原有属性的描述符 @seal class Person { sayHi() {} } function seal(constructor) { let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHi') Object.defineProperty(constructor.prototype, 'sayHi', { ...descriptor, writable: false }) } Person.prototype.sayHi = 1 // 无效 使用闭包来增强装饰器的功能 在TS文档中被称为装饰器工厂 因为@符号后边跟的是一个函数的引用,所以对于mixin的实现,我们可以很轻易的使用闭包来实现: class A { say() { return 1 } } class B { hi() { return 2 } } @mixin(A, B) class C { } function mixin(...args) { // 调用函数返回装饰器实际应用的函数 return function(constructor) { for (let arg of args) { for (let key of Object.getOwnPropertyNames(arg.prototype)) { if (key === 'constructor') continue // 跳过构造函数 Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key)) } } } } let c = new C() console.log(c.say(), c.hi()) // 1, 2 多个装饰器的应用 装饰器是可以同时应用多个的(不然也就失去了最初的意义)。用法如下: @decorator1 @decorator2 class { } 执行的顺序为decorator2->decorator1,离class定义最近的先执行。可以想像成函数嵌套的形式: decorator1(decorator2(class {})) @Decorator 在 Class 成员中的使用 类成员上的 @Decorator 应该是应用最为广泛的一处了,函数,属性,get、set访问器,这几处都可以认为是类成员。在TS文档中被分为了Method Decorator、Accessor Decorator和Property Decorator,实际上如出一辙。 关于这类装饰器,会接收如下三个参数: 如果装饰器挂载于静态成员上,则会返回构造函数,如果挂载于实例成员上则会返回类的原型 装饰器挂载的成员名称 成员的描述符,也就是Object.getOwnPropertyDescriptor的返回值 Property Decorator不会返回第三个参数,但是可以自己手动获取前提是静态成员,而非实例成员,因为装饰器都是运行在类创建时,而实例成员是在实例化一个类的时候才会执行的,所以没有办法获取对应的descriptor 静态成员与实例成员在返回值上的区别 可以稍微明确一下,静态成员与实例成员的区别: class Model { // 实例成员 method1 () {} method2 = () => {} // 静态成员 static method3 () {} static method4 = () => {} } method1和method2是实例成员,method1存在于prototype之上,而method2只在实例化对象以后才有。作为静态成员的method3和method4,两者的区别在于是否可枚举描述符的设置,所以可以简单地认为,上述代码转换为ES5版本后是这样子的: function Model () { // 成员仅在实例化时赋值 this.method2 = function () {} } // 成员被定义在原型链上 Object.defineProperty(Model.prototype, 'method1', { value: function () {}, writable: true, enumerable: false, // 设置不可被枚举 configurable: true }) // 成员被定义在构造函数上,且是默认的可被枚举 Model.method4 = function () {} // 成员被定义在构造函数上 Object.defineProperty(Model, 'method3', { value: function () {}, writable: true, enumerable: false, // 设置不可被枚举 configurable: true }) 可以看出,只有method2是在实例化时才赋值的,一个不存在的属性是不会有descriptor的,所以这就是为什么TS在针对Property Decorator不传递第三个参数的原因,至于为什么静态成员也没有传递descriptor,目前没有找到合理的解释,但是如果明确的要使用,是可以手动获取的。 就像上述的示例,我们针对四个成员都添加了装饰器以后,method1和method2第一个参数就是Model.prototype,而method3和method4的第一个参数就是Model。 class Model { // 实例成员 @instance method1 () {} @instance method2 = () => {} // 静态成员 @static static method3 () {} @static static method4 = () => {} } function instance(target) { console.log(target.constructor === Model) } function static(target) { console.log(target === Model) } 函数,访问器,和属性装饰器三者之间的区别 函数 首先是函数,函数装饰器的返回值会默认作为属性的value描述符存在,如果返回值为undefined则会忽略,使用之前的descriptor引用作为函数的描述符。所以针对我们最开始的统计耗时的逻辑可以这么来做: class Model { @log1 getData1() {} @log2 getData2() {} } // 方案一,返回新的value描述符 function log1(tag, name, descriptor) { return { ...descriptor, value(...args) { let start = new Date().valueOf() try { return descriptor.value.apply(this, args) } finally { let end = new Date().valueOf() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } } } // 方案二、修改现有描述符 function log2(tag, name, descriptor) { let func = descriptor.value // 先获取之前的函数 // 修改对应的value descriptor.value = function (...args) { let start = new Date().valueOf() try { return func.apply(this, args) } finally { let end = new Date().valueOf() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } } 访问器 访问器就是添加有get、set前缀的函数,用于控制属性的赋值及取值操作,在使用上与函数没有什么区别,甚至在返回值的处理上也没有什么区别。只不过我们需要按照规定设置对应的get或者set描述符罢了: class Modal { _name = 'Niko' @prefix get name() { return this._name } } function prefix(target, name, descriptor) { return { ...descriptor, get () { return `wrap_${this._name}` } } } console.log(new Modal().name) // wrap_Niko 属性 对于属性的装饰器,是没有返回descriptor的,并且装饰器函数的返回值也会被忽略掉,如果我们想要修改某一个静态属性,则需要自己获取descriptor: class Modal { @prefix static name1 = 'Niko' } function prefix(target, name) { let descriptor = Object.getOwnPropertyDescriptor(target, name) Object.defineProperty(target, name, { ...descriptor, value: `wrap_${descriptor.value}` }) } console.log(Modal.name1) // wrap_Niko 对于一个实例的属性,则没有直接修改的方案,不过我们可以结合着一些其他装饰器来曲线救国。 比如,我们有一个类,会传入姓名和年龄作为初始化的参数,然后我们要针对这两个参数设置对应的格式校验: const validateConf = {} // 存储校验信息 @validator class Person { @validate('string') name @validate('number') age constructor(name, age) { this.name = name this.age = age } } function validator(constructor) { return class extends constructor { constructor(...args) { super(...args) // 遍历所有的校验信息进行验证 for (let [key, type] of Object.entries(validateConf)) { if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`) } } } } function validate(type) { return function (target, name, descriptor) { // 向全局对象中传入要校验的属性名及类型 validateConf[name] = type } } new Person('Niko', '18') // throw new error: [age must be number] 首先,在类上边添加装饰器@validator,然后在需要校验的两个参数上添加@validate装饰器,两个装饰器用来向一个全局对象传入信息,来记录哪些属性是需要进行校验的。然后在validator中继承原有的类对象,并在实例化之后遍历刚才设置的所有校验信息进行验证,如果发现有类型错误的,直接抛出异常。这个类型验证的操作对于原Class来说几乎是无感知的。 函数参数装饰器 最后,还有一个用于函数参数的装饰器,这个装饰器也是像实例属性一样的,没有办法单独使用,毕竟函数是在运行时调用的,而无论是何种装饰器,都是在声明类时(可以认为是伪编译期)调用的。 函数参数装饰器会接收三个参数: 类似上述的操作,类的原型或者类的构造函数 参数所处的函数名称 参数在函数中形参中的位置(函数签名中的第几个参数) 一个简单的示例,我们可以结合着函数装饰器来完成对函数参数的类型转换: const parseConf = {} class Modal { @parseFunc addOne(@parse('number') num) { return num + 1 } } // 在函数调用前执行格式化操作 function parseFunc (target, name, descriptor) { return { ...descriptor, value (...arg) { // 获取格式化配置 for (let [index, type] of parseConf) { switch (type) { case 'number': arg[index] = Number(arg[index]) break case 'string': arg[index] = String(arg[index]) break case 'boolean': arg[index] = String(arg[index]) === 'true' break } return descriptor.value.apply(this, arg) } } } } // 向全局对象中添加对应的格式化信息 function parse(type) { return function (target, name, index) { parseConf[index] = type } } console.log(new Modal().addOne('10')) // 11 使用装饰器实现一个有趣的Koa封装 比如在写Node接口时,可能是用的koa或者express,一般来说可能要处理很多的请求参数,有来自headers的,有来自body的,甚至有来自query、cookie的。所以很有可能在router的开头数行都是这样的操作: router.get('/', async (ctx, next) => { let id = ctx.query.id let uid = ctx.cookies.get('uid') let device = ctx.header['device'] }) 以及如果我们有大量的接口,可能就会有大量的router.get、router.post。以及如果要针对模块进行分类,可能还会有大量的new Router的操作。 这些代码都是与业务逻辑本身无关的,所以我们应该尽可能的简化这些代码的占比,而使用装饰器就能够帮助我们达到这个目的。 装饰器的准备 // 首先,我们要创建几个用来存储信息的全局List export const routerList = [] export const controllerList = [] export const parseList = [] export const paramList = [] // 虽说我们要有一个能够创建Router实例的装饰器 // 但是并不会直接去创建,而是在装饰器执行的时候进行一次注册 export function Router(basename = '') { return (constrcutor) => { routerList.push({ constrcutor, basename }) } } // 然后我们在创建对应的Get Post请求监听的装饰器 // 同样的,我们并不打算去修改他的任何属性,只是为了获取函数的引用 export function Method(type) { return (path) => (target, name, descriptor) => { controllerList.push({ target, type, path, method: name, controller: descriptor.value }) } } // 接下来我们还需要用来格式化参数的装饰器 export function Parse(type) { return (target, name, index) => { parseList.push({ target, type, method: name, index }) } } // 以及最后我们要处理的各种参数的获取 export function Param(position) { return (key) => (target, name, index) => { paramList.push({ target, key, position, method: name, index }) } } export const Body = Param('body') export const Header = Param('header') export const Cookie = Param('cookie') export const Query = Param('query') export const Get = Method('get') export const Post = Method('post') Koa服务的处理 上边是创建了所有需要用到的装饰器,但是也仅仅是把我们所需要的各种信息存了起来,而怎么利用这些装饰器则是下一步需要做的事情了: const routers = [] // 遍历所有添加了装饰器的Class,并创建对应的Router对象 routerList.forEach(item => { let { basename, constrcutor } = item let router = new Router({ prefix: basename }) controllerList .filter(i => i.target === constrcutor.prototype) .forEach(controller => { router[controller.type](controller.path, async (ctx, next) => { let args = [] // 获取当前函数对应的参数获取 paramList .filter( param => param.target === constrcutor.prototype && param.method === controller.method ) .map(param => { let { index, key } = param switch (param.position) { case 'body': args[index] = ctx.request.body[key] break case 'header': args[index] = ctx.headers[key] break case 'cookie': args[index] = ctx.cookies.get(key) break case 'query': args[index] = ctx.query[key] break } }) // 获取当前函数对应的参数格式化 parseList .filter( parse => parse.target === constrcutor.prototype && parse.method === controller.method ) .map(parse => { let { index } = parse switch (parse.type) { case 'number': args[index] = Number(args[index]) break case 'string': args[index] = String(args[index]) break case 'boolean': args[index] = String(args[index]) === 'true' break } }) // 调用实际的函数,处理业务逻辑 let results = controller.controller(...args) ctx.body = results }) }) routers.push(router.routes()) }) const app = new Koa() app.use(bodyParse()) app.use(compose(routers)) app.listen(12306, () => console.log('server run as http://127.0.0.1:12306')) 上边的代码就已经搭建出来了一个Koa的封装,以及包含了对各种装饰器的处理,接下来就是这些装饰器的实际应用了: import { Router, Get, Query, Parse } from "../decorators" @Router('') export default class { @Get('/') index (@Parse('number') @Query('id') id: number) { return { code: 200, id, type: typeof id } } @Post('/detail') detail ( @Parse('number') @Query('id') id: number, @Parse('number') @Body('age') age: number ) { return { code: 200, age: age + 1 } } } 很轻易的就实现了一个router的创建,路径、method的处理,包括各种参数的获取,类型转换。将各种非业务逻辑相关的代码统统交由装饰器来做,而函数本身只负责处理自身逻辑即可。这里有完整的代码:GitHub。安装依赖后npm start即可看到效果。 这样开发带来的好处就是,让代码可读性变得更高,在函数中更专注的做自己应该做的事情。而且装饰器本身如果名字起的足够好的好,也是在一定程度上可以当作文档注释来看待了(Java中有个类似的玩意儿叫做注解)。 总结 合理利用装饰器可以极大的提高开发效率,对一些非逻辑相关的代码进行封装提炼能够帮助我们快速完成重复性的工作,节省时间。但是糖再好吃,也不要吃太多,容易坏牙齿的,同样的滥用装饰器也会使代码本身逻辑变得扑朔迷离,如果确定一段代码不会在其他地方用到,或者一个函数的核心逻辑就是这些代码,那么就没有必要将它取出来作为一个装饰器来存在。 参考资料 typescript | decorators koa示例的原版,简化代码便于举例 One more thing 我司现在大量招人咯,前端、Node方向都有HC公司名:Blued,坐标帝都朝阳双井主要技术栈是React,也会有机会玩ReactNative和ElectronNode方向8.x版本+koa 新项目会以TS为主有兴趣的小伙伴可以联系我详谈:email: jiashunming@blued.comwechat: github_jiasm

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册