本文是阅读《JS高级程序设计》第23章《高级技巧》的阅读分享。本文根据自己的理解和经验,根据书中的思路进行了扩展和延伸,并指出了书中存在的问题。将讨论安全类型检查、函数延迟加载、冻结对象、计时器等主题。1.安全类型检测这道题就是如何安全地检测变量的类型,比如判断一个变量是否是数组。通常的做法是使用instanceof,如下代码所示:letdata=[1,2,3];console.log(datainstanceofArray);//true但是在某些情况下上面的判断会失败——也就是判断iframe中的一个实例当父窗口的变量。写个demo验证一下,主页面的main.html如下:arrayDatainstanceofArray));判断iframe.html中父窗口的变量类型:在iframe中使用window.parent获取父窗口的全局窗口对象,不管跨域与否,都没有问题,然后你可以拿到父窗口的变量,然后用instanceof来判断。***运行结果如下:可以看到父窗口的判断是正确的,但是子窗口的判断是假的,所以一个变量明明是Array,但又不是Array。为什么?由于这是一个父子窗口,存在一些问题,所以尝试将Array改为父窗口的Array,即window.parent.Array,如下图所示:这次返回true,并且然后改其他的判断,如上图,可以知道根本原因是上图最后一个判断:Array!==window.parent.Array分别是两个函数,一个是为父窗口定义的,另一个是为子窗口定义的,内存地址不同,内存地址不同对象相等判断无效,但是window.parent.arrayData.constructor返回的是父窗口的Array。比较的时候是在子窗口,用的是子窗口的Array。两个Array不相等,所以判断不成立。那我们该怎么办呢?既然不能用Object的内存地址来判断,我们可以用string的方法,因为string是基本类型,string的比较只需要每个字符都相等即可。ES5提供了这样一个方法Object.prototype.toString,我们先试试,试试不同变量的返回值:可以看到如果是数组返回“[objectArray]”,ES5规定了这个函数:也就是也就是说,这个函数的返回值以“[object”开头,后面是变量类型的名称和右括号。所以既然它是一个标准的语法规范,你可以使用这个函数来安全地判断一个变量是否是一个数组。可以这样写:Object.prototype.toString.call([1,2,3])==="[objectArray]"注意要用call,不能直接调用。call的最后一个参数是上下文执行上下文。将一个数组作为执行上下文传递给它。一个比较有意思的现象是ES6的class也返回了function:所以我们可以知道class也是function实现的原型,也就是说class和function本质上是一样的,只是写法不同而已。那是不是就不能再用instanceof来判断变量类型了呢?不行,当你需要检测父页面的变量类型时,你必须使用这个方法。这个页面的变量还是可以通过instanceof或者constructor的方法来判断,只要你能保证Thisvariable不跨页面即可。因为对于大多数人来说,很少会写iframe代码,所以没必要用比较麻烦的方式,不如用简单的方式。2、懒加载函数有时需要在代码中做一些兼容性判断,或者做一些UA判断,如下代码所示://UAtypegetUAType:function(){letua=window.navigator.userAgent;if(ua.match(/renren/i)){return0;}elseif(ua.match(/MicroMessenger/i)){return1;}elseif(ua.match(/weibo/i)){return2;}return-1;}该函数的作用是判断用户是在什么环境下打开网页的,从而统计出哪个渠道的效果更好。这种判断有一个特点,就是它的结果是死的。无论判断执行多少次,都会返回相同的结果。比如用户的UA不能在这个网页上改变(调试设置除外)。所以为了优化,就有了lazy函数。上面的代码可以改成://UA类型getUAType:function(){letua=window.navigator.userAgent;if(ua.match(/renren/i)){pageData.getUAType=()=>0;return0;}elseif(ua.match(/MicroMessenger/i)){pageData.getUAType=()=>1;return1;}elseif(ua.match(/weibo/i)){pageData.getUAType=()=>2;return2;}return-1;}每次判断后,将getUAType函数重新赋值成为一个新的函数,而这个函数直接返回一定的Variables,这样后面的每次获取都不需要判断,这就是作用懒惰的功能。你可能会说这么几个判断能优化多少时间,这么少的时间对用户来说差不多。确实如此,但作为一个有抱负的编码员,你还是会想办法尽可能地优化你的代码,而不仅仅是为了完成功能的要求。而当你的优化积累到一定量的时候,就会发生质的变化。上大学的时候,我的C++老师举了一个例子,说有一个系统比较慢,让她看一下。她所做的优化之一是将十进制双精度更改为单精度,这样速度更快。.但实际上,我们对上面的例子有一个更简单的实现,就是创建一个变量,直接保存:letua=window.navigator.userAgent;letUAType=ua.match(/renren/i)?0:ua。match(/MicroMessenger/i)?1:ua.match(/weibo/i)?2:-1;连函数都不用写。缺点是即使不使用UAType这个变量也会执行一个判断,但是我们认为这个变量被使用的概率还是很高的。让我们再举一个有用的例子。由于Safari的隐私浏览会禁用本地存储,所以需要进行兼容性判断:Data.localStorageEnabled=true;//Safari的隐私浏览会禁用localStoragetry{window.localStorage.trySetData=1;}catch(e){Data.localStorageEnabled=false;}setLocalData:function(key,value){if(Data.localStorageEnabled){window.localStorage[key]=value;}else{util.setCookie("_L_"+key,value,1000);}}设置本地数据时,需要判断是否支持本地存储,支持则使用localStorage,否则使用cookie代替。您可以使用惰性函数对其进行修改:setLocalData:function(key,value){if(Data.localStorageEnabled){util.setLocalData=function(key,value){returnwindow.localStorage[key];}}else{util.setLocalData=function(key,value){returnutil.getCookie("_L_"+key);}}returnutil.setLocalData(key,value);}这里可以减少一个if/else的判断,不过好像不是特别实惠,毕竟,为了减少一个判断,引入了惰性函数的概念,所以你可能要权衡这个引入是否值得。如果有三五个判断,应该会更好。3.函数绑定有时需要将一个函数作为参数传递给另一个函数执行。这时候函数的执行上下文经常发生变化,如下代码所示:}init(){$map.on('click',this.handleMouseClick);}}在点击事件的执行回调中,this没有指向DrawTool的实例,所以里面的this.points会返回undefined。第一个解决方案是使用闭包,先缓存这个然后把它变成那个:){letthat=this;$map.on('click',event=>that.handleMouseClick(event));}}由于回调函数是用that执行的,that指向DrawTool的实例,所以没有问题。反之,如果没有that,就会用this,所以要看this指向哪里。因为我们使用了箭头函数,而箭头函数的this仍然指向parent的上下文,所以不需要自己创建闭包,直接使用this即可:init(){$map.on('click',event=>this.handleMouseClick(event));}这个方法比较简单,第二种方法是用ES5的bind函数绑定,代码如下:init(){$map.on('click',this.handleMouseClick.bind(this));}这个bind看起来很神奇,但实际上只需要一行代码就可以实现一个bind函数:Function.prototype.bind=function(context){return()=>this.call(context);}是Return一个函数,这个函数的this就是原来指向的函数,然后让它调用(context)绑定执行上下文。4.CurryingCurrying就是将一个函数和一个参数值结合起来生成一个新的函数,如下代码,假设有一个curry函数:functionadd(a,b){returna+b;}letadd1=add.curry(1);console.log(add1(5));//6console.log(add1(2));//3如何实现这样的curry函数?它的重点是返回一个函数,里面有一些闭包变量记录了创建时的默认参数,然后在执行返回函数的时候,将新传入的参数和默认参数组合成一个完整的参数列表来调整原始函数,所以下面的代码是可用的:因为参数不是数组,没有concat函数,所以需要把伪数组转换成伪数组,可以使用Array.prototype.slice:Function.prototype.curry=function(){letslice=Array.prototype.slice;letdefaultArgs=slice.call(arguments);letthat=this;returnfunction(){returnthat.apply(null,arguments.concat(slice.call(defulatArgs)));}}现在给出一个有用的柯里化示例。当需要对数组进行降序排序时,需要这样写:letdata=[1,5,2,3,10];data.sort((a,b)=>b-a);//[10,5,3,2,1]传一个函数参数进行排序,但是如果你的降序操作比较多,每次都写一个函数参数有点烦,所以可以用柯里化固化这个参数:Array。prototype.sortDescending=Array.prototype.sort.curry((a,b)=>b-a);这样就方便多了:letdata=[1,5,2,3,10];data.sortDescending();console.log(data);//[10,5,3,2,1]5.防止对象被篡改有时候你可能会害怕你的对象被误更改,所以你需要保护它(1)—对象。seal防止增删属性如下。当一个对象被密封时,不能添加或删除属性:当使用严格模式时,会抛出异常:(2)Object.:同时可以使用Object.isFrozen、Object.isSealed、Object.isExtensible来判断当前对象的状态。(3)defineProperty冻结单个属性如下图,设置enumable/writable为false,则该属性不会被遍历写入:6.timer如何实现一个JS版的sleep函数?因为在C/C++/Java等语言中有休眠功能,而JS没有。sleep函数的作用是让线程进入睡眠状态,然后到了指定的时间再唤醒。不能写一个while循环,不断判断当前时间和开始时间的差值是否达到指定时间,因为这样会占用CPU,所以不是休眠。这个实现比较简单,我们可以使用setTimeout+callback:但是使用回调可以防止我的代码像普通代码一样像瀑布一样被写下来。我必须创建一个回调函数作为传递值的参数。于是想到了Promise,现在用Promise重写:functionsleep(millionSeconds){returnnewPromise(resolve=>setTimeout(resolve,millionSeconds));}sleep(2000).then(()=>console.log("sleeprecover"));但是好像还是没有办法解决上面的问题,还是需要传递一个函数参数。虽然使用Promise本质上是一样的,但是它有一个resolve参数,方便你告诉它什么时候异步结束,然后它就可以执行了,尤其是回调比较复杂的时候,使用Promise会更方便.ES7新增了两个属性async/await来处理异步情况,让异步代码写的跟同步代码一样,异步版本的sleep如下:;}asyncfunctioninit(){awaitsleep(2000);console.log("sleeprecover");}init();与简单的Promise版本相比,sleep的实现保持不变。但是在调用sleep之前加一个await,这样下面的代码只有在sleep异步完成后才会执行。同时,您需要将代码逻辑包装在一个异步标记的函数中。该函数将返回一个Promise对象。当里面所有的异步函数都执行完之后,就可以:init().then(()=>console.log("initfinished"));ES7的新属性让我们的代码更加简洁优雅。关于定时器还有一个很重要的话题,那就是setTimeout和setInterval的区别。如下图所示:setTimeout在当前执行单元全部执行完后开始计时,setInterval在设置好定时器后立即开始计时。可以用一个实际的例子来解释。我在《JS与多线程》一文中提到了这个例子。下面我们用代码来实际运行一下,如下代码所示:letscriptBegin=Date.now();fun1();fun2();//需要执行20ms的工作单元,Date.now()-scriptBegin);letbegin=Date.now();while(Date.now()-begin<20);}functionfun1(){letfun3=()=>act("fun3");setTimeout(fun3,0);act("fun1");}functionfun2(){act("fun2-1");varfun4=()=>act("fun4");setInterval(fun4,20);act("fun2-2");}这段代码的执行模型如下:控制台输出:与上述模型分析一致。然后讨论最后一个话题,functionthrottling7.Functionthrottling函数throttling的目的是为了避免触发执行太快,比如:—监听input触发搜索—监听resize响应响应调整—监听mousemove调整position我们先来看看如何实现很多时候resize/mousemove事件可以在1s内触发,所以我们写了如下驱动代码:letbegin=0;letcount=0;window.onresize=function(){count++;letnow=Date.now();if(!begin){begin=now;return;}if((now-begin)%3000<60){console.log(now-begin,count/(now-begin)*1000);}};当窗口拉得越快时,resize事件在1秒内大约触发40次:需要注意的是,并不是说拉得越快,触发得越快。实际情况是拉的越快,触发的越慢,因为拉的时候需要重绘页面,变化越快,重绘的次数越多,所以触发的越少。mousemove事件在我电脑的Chrome上每秒触发60次左右:如果需要监听resize事件来调整DOM,这个调整比较耗时,需要每秒调整40次,可能不会响应好,不需要调整那么频繁,那么节流。如何实现节流?书中是这样实现的:functionthrottle(method,context){clearTimeout(method.tId);method.tId=setTimeout(function(){method.call(context);},100);每次执行都需要SetTimeout。如果触发的快,把上次的setTimeout清空,再setTimeout,这样执行就不会快了。但是这样做有个问题,就是这个回调函数可能永远不会执行,因为它一直在触发和清除tId,有点尴尬。上面代码的本意应该是100ms内最多触发一次,但实际情况有可能永远不会执行。稍微修改一下上面的代码:functionthrottle(method,context){if(method.tId){return;}method.tId=setTimeout(function(){method.call(context);method.tId=0;},100);}这个实现是正确的,回调最多100ms执行一次。原理是在setTimeout中将tId设置为0,这样才能执行下一个trigger。实际实验:大约每100ms执行一次,这样就可以达到我们的目的。但是这种方式有一个小问题,就是每次执行都会延迟100ms。有时用户可能会最大化窗口,只触发一个resize事件,但是这个时候还是需要延迟100ms来执行。假设你的时间是500ms,那么就得延迟半秒,所以这个实现并不理想。需要优化,如下代码所示:functionthrottle(method,context){//如果是第一个触发,则立即执行}if(method.tId){return;}method.tId=setTimeout(function(){method.call(context);method.tId=0;},100);}先判断是否是第一次触发,如果所以马上做。这样就解决了上面提到的问题,但是这个实现还是有问题,因为在全球范围内只是第一次。用户激活后,过一段时间又会延迟。,第一个触发器将被执行两次。所以我该怎么做?作者想到了一个方法:functionthrottle(method,context){if(!method.tId){method.call(context);method.tId=1;setTimeout(()=>method.tId=0,100);}}每次触发立即执行,然后设置一个定时器,将tId设置为0。实际效果如下:这个实现比之前的实现更简洁,可以解决延迟的问题。——通过节流,将执行次数减少到1秒10次。节流时间也可以控制,但同时失去了灵敏度。如果您需要高灵敏度,则不应使用节流,例如拖放应用程序。如果拖放节流会发生什么?用户会发现拖动是一键一键。笔者重新阅读了高诚的《高级技巧》章节,结合自己的理解和实践,总结出这样一篇文章。我的体会是,如果只是把书和博客当作睡前阅读,收获不是很大,也没有什么实际的好处。练习书中的代码。不结合自己的编码经验,就无法用自己的理解去整合这个知识点,转化为自己的知识点。你可能会说,我看了之后会有印象。有印象固然好,但你是花了那么多时间读那本书后才有印象的。多么可靠。如果有人问起这个印象,你可能会回答一些无法联系起来的片段,给人一种背书的感觉。还有,书上有时可能会出现一些错误或过时的东西,只有实践才能出真知。