摘要:
博客开了几个月了也没写什么,想着还是随手写一点吧,也有助于自己的加深理解。最近又在看《JavaScript高级程序设计》,刚好看到第22章的高级技巧,就随手码下来吧。
本文内容由《JavaScript高级程序设计》书上的P596 - P625 和 本人对这些技巧的理解提供。
适合人群:如果你刚好在看这本书,或者想特高JS代码的性能优化,减少代码耦合,都可以看本文章。当然,你还需要有一定的JS语法基础。那么,开始吧:
一、高级函数
1、安全的类型检测
JavaScript内置的类型检测机制并非完全可靠。比如Safari(直至第4版)在对正则表达式应用typeof操作符时会返回function,instanceof操作符在存在多个全局作用域(像一个页面包含多个frame)的情况下,也是问题多多,比如:
|
|
如果arr是在另一个frame中定义的数组。那么这里就会返回false。在检测某个对象到底是原生对象还是开发人员自定义的对象的时候。也会有问题。上述问题解决方法都一样:
|
|
由于原生数组的构造函数名与全局作用域无关,因此使用toString就能保证返回一致的值,利用这点可以创建如下函数:
|
|
在开发中能够区分原生与非原生对象非常重要。只有这样才能确切知道某个对象到底有哪些功能。
2、作用域安全的构造函数
直接来看看作用域不安全的构造函数吧
|
|
上面问题出在当没有使用new操作符来调用该构造函数的情况上。由于该this对象是在运行时绑定的,所以直接调用Person(),this会映射到全局对象window上,导致错误对象属性的意外增加。
解决的思路是 作用域安全的构造函数在进行任何更改前,首先确认this对象是正确类型的实例。如果不是那么会创建新的实例并返回。如下:
|
|
这段代码中添加了一个检查并确保this对象是Person实例。最后的结果是,调用Person构造函数无论是否使用new操作符,都会返回一个Person的新实例。
这样处理也有坏处,就是构造函数窃取模式的继承该实例是无效的。比如:
|
|
在这里由于Person构造函数作用域是安全的,this对象并非Person实例,所以会创建并返回一个新的Person对象,所以People构造函数中的this对象并没有得到增长,也就不会有name属性。
解决上面的问题,使用原型链继承即可:
多个程序猿在同一个页面上写JavaScript代码的环境中,作用域安全构造函数就很有用了。推荐作用域安全的构造函数作为最佳实践。
3、惰性载入函数
因为浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,将执行引导到正确的代码中。
|
|
每次调用createXHR(),它都要对浏览器所支持的能力进行仔细检查。每次调用该函数都是这样,即使调用时分支的结果都不变,如果浏览器内置XHR,那么它就一直支持了,那么这种测试就变得没必要。即使只有一个if语句的代码,也肯定要比没有if语句的慢。
解决方案就是称为惰性载入的技巧。书上写法:
|
|
这是是把函数表达式写在函数内部,我认为把函数表达式写在外部即可:
|
|
这两种写法都可以在执行createXHR方法的时候执行第一遍后,后面执行就不会再进行if判断。如果有需要函数第一次执行也不产生if判断,那么就在函数声明的时候执行。
惰性载入函数的优点是只在执行分支代码的时候牺牲一点儿性能。至于那种方式更合适,就要看你的具体需求而定了。
4、函数绑定
函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境,看下面栗子:
|
|
上面代码问题看似点击按钮的时候会打印 “I am Tom”, 实际显示的是 “I am “。
这个问题在于没有保存person.talk()的环境,所以this对象最后是指向了Dom按钮(在IE8中,this指向window)。
解决这个问题,可以使用一个闭包来解决,看下面代码(以下代码只显示修改的部分):
|
|
这个解决方案在click事件处理程序内使用了一个闭包直接调用person.talk(); 这只是这段代码的解决方案,创建多个闭包可能会令代码变得难于理解和调试。
因此,很多JS库实现了一个可以将函数绑定到指定环境的函数。这个函数一般都叫bind()。
实现一个比较基本的bind()函数:
|
|
bind()中创建了一个必报,闭包使用apply()调用传入的函数,并给apply()传递context对象和参数。
结合上面的代码结合使用自定义bind(方法):
|
|
其实在ES5已经为所有函数定义了一个原生bind()方法,使用方法如下:
|
|
只要是将某个函数指针(环境)以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就突显出来了。
主要用于事件处理程序以及setTimeout()和setInterval()。
然而,被绑定函数与普通函数相比有更多的开销,需要更多的内存,同时也因为多重函数调用稍微慢一点,所以最好只在必要时使用。
5、函数柯里化
与函数绑定紧密相关的主题是函数柯里化,它用于创建已经设置好了一个或多个参数的函数。
函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数,看栗子:
|
|
这里创建第一个参数绑定为5的add()柯里化版本。你也可以像下面这样给出所有的函数参数:
|
|
函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更为复杂的bind()函数。以ES5的bind()方法举例:
|
|
这样每次单机按钮就会弹出相应参数的字符串。
PS:这里有个坑呀。btn.addEventListener(‘click’, talking(‘I am running’)); 这样点击按钮是不会有反应的。
总的来说,柯里化的作用可以使代码模块化,减少耦合增强其可维护性。柯里化函数和绑定函数提供了强大的动态函数创建功能,两者不应滥用,只在必要的时候用。
二、防篡改对象
在编写JavaScript库中,因为JS共享的特性,开发人员很可能会意外地修改别人代码。
ES5新增了几个方法,通过它们可以指定对象的行为。
1、不可拓展对象 Object.preventExtensions()
|
|
可以看出调用 Object.preventExtensions() 方法后,就不能给person1对象添加新的属性和方法。但是仍然可以修改和删除已有的成员。
2、密封的对象 Oject.seal()
密封对象不可拓展,而且已有的成员[[configurable]]特性将被设置为false。具体功能还是直接看代码:
|
|
除了不能增删外,只有修改操作是可以进行的。
3、冻结的对象 Object.freeze()
最严格的防篡改级别是冻结对象。冻结对象既不可拓展,又是密封的。而且每个对象数据属性[[Writable]]特性会被设置为false。
|
|
可以看出冻结的对象什么也无法修改删除,这样冻结(或密封)主要的库就能够防止这些问题的发生。
三、高级定时器
1、setTimeout()
首先,JavaScript是运行在单线程环境中,而定时器只是表示指定间隔多少时间把代码添加到队列内。
JS队列执行代码方式还是用代码体验一下吧:
|
|
点击按钮后可以发现timer1执行,再过一秒后timer2也执行,一切正常。因为在事件处理程序里面没有其他代码要添加到队列里,效果比较不明显。
那么我们添加点其他代码:
|
|
现在明显感受到定时器输出的晚了几秒。
解析一下:点击后,队列中执行for循环,等待for循环结束后,再把timer1和timer2的代码放入队列相隔设置好的时间后执行。这样一来,就好理解多了。
2、setInterval()
|
|
这个例子中第一个定时器是在2000ms处添加到队列中,但是代码块需要2.5s左右时间执行,所以第一个打印记录时间是4.5s。而在4s的时候本来是应该执行第二个定时器的代码,这时候就会没有延迟的立即执行第二个定时器代码在6s的时候显示”第2次定时器执行”。
书上有说会缺失间隔的代码,不知道是执行的代码时间不够长还是怎样,执行起来并没有。了解详情的还望指教一下。
3、Yielding Processes
JavaScript在浏览器中有一个限制是长时间运行脚本的制约,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长的时间执行,询问是允许其继续执行还是停止它。
脚本长时间运行问题通常是两个原因之一造成的:过深的嵌套、过长的函数调用或者是进行大量处理的循环。
其实就是当你发现某个循环占用了大量时间,如果这个数据的处理不会造成其他运行的阻塞。
那么你就可以用定时器分割这个循环。这是一种叫“数组分块”的技术,小块小块地处理数组。比如:
|
|
数组分块的重要性在于它可以将多个项目的处理在执行队列上分开。
一旦某个函数需要花50ms以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务。
4、函数节流
函数节流的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清楚前一次定时器并设置另一个。
以下是该模式的基本形式:
|
|
这段代码中,拟人化的创建了一个人,有两个方法:run()和readyRun()。前者是实际进行的动作,后者是初始化动作所必须调用的。
当调用readyRun(),第一步是清楚存好的runTime,来阻止之前的调用被执行。然后创建一个新的定时器调用run()。由于setTimeout中用到的函数环境总是window,所以有必要保存this的引用方便使用。
这样的好处是即使1000ms内调用了readRun(),run()也只会调用一次。(毕竟人不可能一秒跑那么多步的好嘛!)
节流在resize,scorll事件中是最常用的。如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会再极短的时间内进行过多的计算。
这里先设计一个函数封装一下这个方法,方便以后常用:
|
|
写一个滑动滚轮改变盒子大小的方法:
|
|
这里多数情况下用户使感觉不到变化的,可是这大大节省了浏览器的计算。
只要代码是周期性执行的,都应该使用节流,并且适当控制速率。
四、自定义事件
如果每个对象都有对其他所有对象的引用,那么整个代码就会紧密耦合,同时维护也变得很困难,因为对某个对象的修改也会影响到其他对象。使用自定义事件有助于解耦相关对象,保持功能的隔绝。
实际上,在很多情况下,触发事件的代码和舰艇事件的代码是完全分离的。
自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:
|
|
然后,使用EventTarget类型的自定义事件可以如下使用:
|
|
五、拖放
拖放是一种非常流行的用户界面模式。他的概念很简单:点击某个对象,并按住鼠标按钮不放,将鼠标移动到另一个区域,然后释放鼠标按钮将对象“放”在这里。
简单的拖放界面可用一下代码实现:
DragDrop对象封装了拖放的所有基本功能。拖放的时候回自动针对所有包含“draggable”类的元素启用。
为了元素能被拖放,它必须是绝对定位的。
结合前面的自定义事件完善该功能:
|
|
其实这里有个坑跟书上的不一样,就是new EventTarget()要在外一层new。
以上就是《JavaScript高级程序设计》书上的高级技巧和本人对这些知识点的一些理解,所码的文章。
本文经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。