高级函数
安全的类型检测
JavaScript 内置的类型检测机制并非完全可靠。一个众所周知的问题就是在运行typeof null
时,会返回object
,这往往不是我们想要的结果,这会使得真正的对象类型与null
得到的结果相同。解决该问题的方法之一是使用Object.prototype.toString.call(value)
,如果该表达式中value = null
,则会返回[object Null]
;如果value = {}
,则返回[object Object]
。
作用域安全的构造函数
当使用构造器方式构造对象时,构造器内部使用了 this 来指向新的对象实例,然而这仅仅是在正确调用构造函数时出现的结果(使用了 new 操作符)。如果忘记使用 new 操作符,与调用普通函数无异,此时构造器内部的 this 指向的是全局对象。为了避免这种错误的构造器调用方式,我们在构造器内部添加一个容错:1
2
3
4
5
6
7function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
return new Person(name);
}
}
通过在构造函数中增加this instanceof Person
的判断,可以确保作用域安全。
惰性载入函数
先来看以下伪代码,它想表达的意思就是在 IE、Chrome 和其它浏览器的情况下分别返回不同的对象。1
2
3
4
5
6
7
8
9function createObj() {
if (IE 浏览器) {
return IEObj;
} else if (Chrome 浏览器) {
return ChromeObj;
} else {
return otherObj;
}
}
假设该函数要调用多次,就意味着要进行多次的分支语句的判断,而这是不必要的:因为一旦确定是在某个浏览器中,每次运行结果都是一样的。那么如何避免判断多次这种不必要的分支呢?解决方案就是称之为惰性载入的技巧。有两种实现惰性载入的方式,第一种是在函数被调用时再处理函数。例如按照如下方式改写上例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function createObj() {
if (IE) {
createObj = function () {
return IEObj;
}
} else if (Chrome) {
createObj = function () {
return ChromeObj;
}
} else {
createObj = function () {
return otherObj;
}
}
return createObj();
}
在上面这种方式下,if 语句的每个分支都会为 createObj 变量赋值,有效覆盖了原有的函数。最后一步就是调用新赋的函数并返回。以后再调用 createObj() 的时候,就会直接调用被分配的函数,无需再次执行 if 语句。第二种惰性载入的方式是在声明函数时就指定适当的函数,这样,在第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。。例如按照如下方式改写上例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var createObj = (function() {
if (IE) {
return function() {
return IEObj;
}
} else if (Chrome) {
return function() {
return ChromeObj;
}
} else {
return function() {
return otherObj;
}
}
})();
函数柯里化
函数柯里化用于创建已经设置好了一个或多个参数的函数。一个熟悉的应用场景是 Function.prototype.bind(),MDN 上对该函数的定义是:bind() 方法创建一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。这句话中的斜体部分,就是使用的函数柯里化的思想。以下是 bind 函数的一种实现方式:1
2
3
4
5
6
7
8function bind(fn, context) {
var args = Array.prototype.slice.call(arguments, 2); //提取第三个及以后的参数
return function() {
var innerArgs = Array.prototype.slice.call(arguments); //该匿名函数的参数
var finalArgs = args.concat(innerArgs); //外层参数和内层参数连接
return fn.apply(context, finalArgs); //将 fn 上下文设置为 context,并传入 finalArgs 参数
};
}
防篡改对象
- 不可扩展对象:不能给对象添加新成员。涉及的方法有:Object.preventExtensions() 和 Object.isExtensible()
- 密封的对象:只能改变可写属性的值。涉及的方法有:Object.seal() 和 Object.isSealed()
- 冻结的对象:该对象永远不可变。涉及的方法有:Object.freeze() 和 Object.isFrozen()
高级定时器
我们常常使用 setTimeout()
和 setInterval()
函数来创建定时器,但是不难发现,假如你设定了 setInterval 的间隔时间是 1s,但是函数的执行间隔好像并不是 1s,这是很普遍的现象,它的原因主要在于:在 JavaScript 中没有任何代码是立刻执行的,但一旦线程空闲则尽快执行。由于 JavaScript 是单线程的,看似好像与它的异步机制有所冲突,实则没有。异步的本质实际上是计划代码在未来的某个时间执行,具体在哪个时间执行取决于它前面有多少任务需要执行,这是一种队列的机制。定时器对队列的工作方式是,当特定时间过去后将代码插入,等待执行。根据以下的代码想象一个场景:该例中的 onclick 事件处理程序执行了 300ms,那么定时器的代码至少要在定时器设置之后的 300ms 后才会被执行,见下图。1
2
3
4
5
6
7var btn = document.getElementById('my-btn');
btn.onclick = function() {
setTimeout(function() {
//一些操作
}, 250);
//其它代码
}
如图,尽管在 255ms 处添加了定时器代码,但这时候 JS 线程不空闲,onclick 事件处理程序还在运行。所以定时器代码最早能执行的时机是 300ms 处。
重复的定时器
setInterval 定时器确保代码能够规则地插入队列中。但是该方式的问题在于,定时器代码在再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次而之间没有任何停顿。幸好,JS 引擎够聪明,能避免这个问题:在使用 setInterval 时,仅当队列中没有该定时器的其它任何代码实例时,才将定时器代码添加到队列中,这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
这种定时器规则有两个问题:1)某些间隔会被跳过;2)多个定时器的代码执行之间间隔可能会比预期的小。我们改写上例代码为:1
2
3
4
5
6
7var btn = document.getElementById('my-btn');
btn.onclick = function() {
setInterval(function() {
//一些操作
}, 200);
//其它代码
}
设想场景:事件处理程序花了 300ms 多一点的时间完成,同时定时器也花了差不多的时间,就会同时出现跳过间隔且连续运行定时器代码的情况。参见下图:
205ms 时,第一个定时器代码被添加到队列中,到 300ms 处时才能执行。在执行过程中,405ms 时又给队列添加了定时器代码。在下个间隔,即 605ms 处,由于队列中存在一个定时器代码的实例,因而这个时间点的定时器代码不会被添加到队列中。当第一个定时器代码执行完之后,紧接着就执行第二个定时器代码。为了避免以上提到的两个缺点,可以采取以下方案代替 setInterval:1
2
3
4setTimeout(function() {
//一些操作
setTimeout(arguments.callee, interval);
}, interval)
这样做的好处时,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,这样不会跳过间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。
数组分块
当代码中存在进行大量处理的循环时,会导致脚本的长时间运行。如果该循环无需同步完成,也不必按顺序完成时,我们可以使用定时器分割这个循环,使其可以分散在不同的时间完成。基本模式如下:1
2
3
4
5
6
7setTimeout(function() {
var item = array.shift();
process(item);
if (array.length) {
setTimeout(arguments.callee, 100);
}
}, 100);
函数节流
先来看个例子:1
2
3
4window.onresize = function() {
var div = document.getElementById('myDiv');
div.style.height = div.offsetWidth + 'px';
}
就问你吓不吓人。当你改变 window 的大小的时候,浏览器连续不断地去执行上面的代码,可想而知会有多慢。为了解决这个问题,可以用定时器对该函数进行节流。函数节流背后的思想是指,某些代码不可以在没有间断的情况下连续重复运行。也就是说,在一段时间内,一段代码函数只能执行一次,这段代码就是被节流的代码。以下是函数节流的一种应用场景:
假设调用方式如下:1
window.addEventListener('resize', throttle(func, 100));
那么对应的函数节流形式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var isRun = true;
function throttle(func, duration) {
let me = this;
return function() {
if (!isRun) {
return;
}
let args = arguments;
isRun = false;
setTimeout(function() {
isRun = true;
func.apply(me, arguments);
}, duration);
}
}
上面的这段代码有效的控制了 resize 函数的执行频率,使其保持为 1 / duration。函数节流的要点是声明一个变量作为标志位,记录当前代码是否在执行。
函数防抖
有点类似于函数节流,函数防抖也往往用来解决频繁的事件触发所造成的性能问题。但是它与函数节流的理念不同,如果说节流是进行频率控制的话,那么防抖则是进行空闲控制,只有当调用动作 n 毫秒后,才会再次执行该动作,如果这 n 毫秒内又调用此动作则将重新计算执行时间。还有一种常见的防抖场景是验证用户输入:只有当用户输入完毕后,前端才需要检查格式是否正确。以下是函数防抖的一种应用场景:
假设调用方式如下:1
window.addEventListener('resize', debounce(func, 100));
那么对应的函数防抖形式如下:1
2
3
4
5
6
7
8
9
10
11var timeoutId = null;
function debounce(func, duration) {
let me = this;
return function() {
let args = arguments;
clearTimeout(timeoutId);
timeoutId = setTimeout(function() {
func.apply(me, args);
}, duration);
};
}
上面的这段代码同样有效控制了 resize 函数的执行频率,与函数节流有点不同的是,它在执行函数时,多了一个延迟时间。