本文中的内容来自于 《JavaScript 忍者秘籍》。
函数存储
利用以下代码可以完成函数存储功能。
var store = {
nextId: 1,
cache: {},
add: function(fn) {
if (!fn.id) {
fn.id = store.nextId++;
return !!(store.cache[fn.id] = fn);
}
}
};
使用场景:可以用来存储事件的回调函数,由于在 addEventListener
和 attachEvent
的解绑过程中都需要原样传入绑定的函数,所以我们可以将绑定的函数存储下来,以供解绑事件时使用。
自记忆函数
所谓自记忆函数,就是说函数自己能够记住先前计算的结果,这样就能避免相同的计算执行两次,可以显著的提高性能。比如说下面这个检测是否为素数的函数。
function isPrime(value) {
if (!isPrime.results) {
isPrime.results = {};
}
if (isPrime.results[value] !== undefined) {
return isPrime.results[value];
}
var prime = value !== 1;
for(var i = 2; i < value; i++) {
if (value % i === 0) {
prime = false;
break;
}
}
return isPrime.results[value] = prime;
}
缓存记忆有两个优点:
- 在函数调用获取之前计算结果的时候,最终用户享有性能优势。
- 发生在幕后,完全无缝,最终用户和页面开发人员都无需特殊操作或者为此做任何额外的初始化工作。
将缓存记忆用在 DOM
的获取操作上,可以获得 5 倍的性能提升,如下所示。
function getElements(name) {
if (!getElements.cache) {
getElements.cache = {};
}
return getElements.cache[name] =
getElements.cache[name] ||
document.getElementsByTagName(name);
}
上面我们求素数的例子中,其实是在函数中对结果进行了缓存,不过值得注意的一点是,这种实现只有在我们能获取到函数体的时候才可以使用。下面我们就对上面的函数进行改写。
Function.prototype.memoized = function(key) {
this._values = this._values || {};
return this._values[key] !== undefined ?
this._values[key] :
this._values[key] = this.call(this, key);
};
function isPrime(num) {
var prime = num != 1;
for(var i = 2; i < num; i++) {
if (num % i === 0) {
prime = false;
break;
}
}
return prime;
}
console.log(isPrime.memoized(5));
console.log(isPrime._values[5]);
这种写法可以解决刚才我们提出的无法获取函数体的问题,不过又出现了一个问题,因为上面的函数要求调用者在使用 isPrime()
的时候必须要跟上 .memoized()
,不过调用者不可能时刻都能记得这一点,所以对于这个函数我们还可以改写,如下所示:
Function.prototype.memoized = function(key) {
this._values = this._values || {};
return this._values[key] !== undefined ?
this._values[key] :
this._values[key] = this.call(this, key);
};
Function.prototype.memoize = function(key) {
var fn = this;
return function() {
return fn.memoized.call(fn, key);
}
};
var isPrime = (function(num) {
var prime = num !== 1;
for(var i = 2; i < num; i++) {
if (num % i === 0) {
prime = false;
break;
}
}
return prime;
}).memoize();
console.log(isPrime(5));
不过,上面的功能都被添加在 Function
上,由所有函数实例共享,如果感觉这么做有所不妥,可以使用下面这种方式。
function memoize(fn) {
var cache = {};
return function(key) {
console.log("before: " + cache[key]);
return cache[key] !== undefined ?
cache[key] :
cache[key] = fn.call(this, key);
}
}
只需要对要缓存的函数进行包装即可。
函数判断
一般而言,要判断一个函数类型,只需要利用 typeof functionName
即可(会返回字符串 function
)。不过会有一些特殊情况让我们的判断失效,比如下面几种:
Opera
: 在HTML
的<object>
元素上使用typeof
的话,会返回function
,而不是我们期望的object
。(书中说在Firefox
中会出现这个问题,不过我亲自检测之后,发现我电脑上的Firefox
并没有出现上述问题,反而是Opera
出现了这个问题 )。Safari
:Safari
认为DOM
的NodeList
是一个function
,所以typeof document.body.childNodes == function
。(本人未亲自尝试)
基于以上情况,我们需要寻找一种完美的解决方案,不过事实上并不存在完美的解决方案,倒是有一种接近完美的方案,那就是利用 Object.toString()
方法。代码如下:
function isFunction(fn) {
return Object.prototype.toString.call(fn) === "[object Function]";
}
利用这项技术,还可以判断 String, RegExp, Date
等其它对象。
这里我们不直接调用 fn.toString()
的原因有两个:
不同的对象可能有自己的
toString()
方法实现。JavaScript
中的大多数类型都已经有一个预定义的toString()
方法覆盖了Object.prototype
提供的toString()
方法。
从下面可以看出 String
和 Array
重写了 Object
的 toString()
方法。
var sContent = "Hello World";
console.log(sContent.toString()); // "Hello World"
var aContent = [1, 2, 3];
console.log(aContent.toString()); // "[1, 2, 3]"
刚才已经提及,上面这个检测的方法只是接近完美,这说明它也有失误的情况,比如在 IE
中会将 DOM
元素的方法报告成 object
。
伪造数组
出于某种目的(我也不知道),我们可以将对象伪造成一个数组,具体操作如下:
var eles = {
length: 0,
add: function(ele) {
Array.prototype.push.call(this, ele);
}
};
为 eles
对象添加了一个 length
属性,当调用 push
方法时,length
属性会自动增加。