JavaScript中的函数是整个语言中最有趣的一部分,它们强大而且灵活。接下来,我们来讨论JavaScript中函数的一些常用技巧:
一、函数绑定
函数绑定是指创建一个函数,可以在特定的this环境中已指定的参数调用另一个函数。
var handler = { message: "handled", handleClick: function(event) { console.log(this.message + ":" + event.type); } }; var btn = document.getElementById("btn"); btn.onclick = handler.handleClick; //undefined:click
此处,message为undefined,因为没有保存handler.handleClick的环境。
接下来我们实现一个将函数绑定到制定环境中的函数。
function bind(fn,context) { return function() { return fn.apply(context,arguments); } }
bind函数按如下方式使用:
var handler = { message: "handled", handleClick: function(event) { console.log(this.message + ":" + event.type); } }; function bind(fn,context) { return function() { return fn.apply(context,arguments); } } var btn = document.getElementById("btn"); btn.onclick = bind(handler.handleClick,handler); //handled:click
ECMAScript为所有函数定义了一个原生的bind函数
var handler = { message: "handled", handleClick: function(event) { console.log(this.message + ":" + event.type); } }; function bind(fn,context) { return function() { return fn.apply(context,arguments); } } var btn = document.getElementById("btn"); btn.onclick = handler.handleClick.bind(handler); //handled:click
支持原生bind方法的浏览器有IE9+、Firefox 4+和chrome。
被绑定函数与普通函数相比有更多的开销,消耗更多内存,同时也因为多重函数调用稍微慢一点,所以最好只在必要时调用。
二、函数柯里化
函数柯里化(function currying)用于创建已经设置好了一个或多个参数的函数。其思想是使用一个闭包返回一个函数。
柯里化函数创建步骤:调用另一个函数并传入要柯里化的函数和必要参数。创建柯里化函数的通用方式如下:
function curry(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null,finalArgs); }; }
我们可以按如下方式使用curry()函数:
function add(n1,n2) { return n1 + n2; } var currAdd = curry(add,5); alert(currAdd(3)); //8
function add(n1,n2) { return n1 + n2; } var currAdd = curry(add,2,3); alert(currAdd()); //5
柯里化作为函数绑定的一部分包含在其中,构造更加复杂的bind()函数:
function 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); }; }
使用bind时,它会返回绑定到给定环境的函数,并且其中的某些函数参数已经被设置好。当你想除了event对象再额外给事件处理函数传递参数时是很有用的。
var handler = { message: "handled", handleClick: function(name,event) { console.log(this.message + ":" + name +":" + event.type); } }; var btn = document.getElementById("btn"); btn.onclick = bind(handler.handleClick,handler,"btn");
三、函数尾调用与尾递归
3.1尾调用
尾调用就是指某个函数的最后一步调用另一个函数
function fn() { g(1); }
尾调用不一定在函数尾部,只要是最后一步操作即可。
function f(x) { if (x > 0) { return m(x) } return n(x); }
m、n都是尾调用,它们都是函数f的最后一步操作。
我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
尾调用优化:只保留内层函数的调用记录。如果所有函数都是尾调用,那么可以做到每次执行时,调用记录只有一项,这样可以大大节省内存。注意:ES5中还没有这个优化机制。
3.2尾递归
尾递归就是指在函数的最后一步调用自己。
在JS的递归调用中,JS引擎将为每次递归开辟一段内存用以储存递归截止前的数据,这些内存的数据结构以“栈”的形式存储,这种方式开销非常大,并且一般浏览器可用的内存非常有限。所以递归次数多的时候,容易发生栈溢出。但是对于尾递归来说,由于我们只需要保存 一个调用的记录,所以不会发生错误。因此,尾调用优化是很重要的。ES6规定,所有ECMAScript的实现,都必须部署尾调用优化。
函数递归改写为尾递归:
下面是一个求阶乘的函数:
function factorial(n) { if(n === 1) { return 1; } return n * factorial(n - 1); }
function tFactorial(n,total) { if(n === 1) { return total; } return tFactorial(n - 1, n * total); } function factorial(n) { return tFactorial(n,1); } factorial(10);
另外,我们也可以借助上面提到的柯里化来实现改写:
function curry(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null,finalArgs); }; } function tailFactorial(total, n) { if (n === 1) { return total; } return tailFactorial(n * total, n - 1); } const factorial = curry(tailFactorial, 1); alert(factorial(10));
使用ES6中函数的默认值:
function factorial(n, total = 1) { if (n === 1) { return total }; return factorial(n - 1, n * total); } factorial(10);
最后,我们要注意:ES6中的尾调用优化只是在严格模式下开启的。这是因为正常模式下函数内部的两个变量arguments和fn.caller可以跟踪函数的调用栈。尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
四、函数节流
浏览器中进行某些计算或处理要比其它操作消耗更多的CPU时间和内存,譬如DOM操作。如果我们尝试进行过多的DOM相关的操作可能会导致浏览器挂起,甚至崩溃。例如,如果我们在onresize事件处理程序内部进行DOM操作,很可能导致浏览器崩溃(尤其是在IE中)。为此,我们要进行函数节流。
函数节流是指某些代码不能在没有间断的情况连续重复进行。实现方法:函数在第一次被调用的时候,会创建一个定时器,指定时间间隔之后执代码。之后函数被调用的时候,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行,那么这个操作没有任何意义。如果前一个定时器没有执行,那么就相当于将它替换成一个新的定时器。基本形式如下:
var processor = { tmID: null, exeProcess: function() { }, process: function() { clearTimeout(this.tmID); var that = this; this.tmID = setTimeout(function() { that.exeProcess(); },100); } } processor.process();
我们可以简化如下:
function throttle(fn,context) { clearTimeout(fn.tid); fn.tid = setTimeout(function() { fn.call(context); },100); }
接下来,我们看一下上面的函数的应用。如下是一个resize事件的事件处理函数:
window.onresize = function() { var div = document.getElementById("myDiv"); div.style.height = div.offsetWidth + "px"; }
上面的代码为window添加了一个resize事件处理函数,但是这可能会造成浏览器运行缓慢。这时,我们就用到了函数节流了。
function resizeDiv() { var div = document.getElementById("myDiv"); div.style.height = div.offsetWidth + "px"; } window.onresize = function() { throttle(resizeDiv); }
五、函数惰性载入
因为浏览器之间的差异,我们在使用某些函数的时候需要检查浏览器的能力,这样就可能存在很多条件判断的代码。例如,添加事件的代码
var addEvent = function(el,type,handle) { if(el.addEventListener) { el.addEventListener(type,handle,false); } else if(el.attachEvent) { el.attachEvent("on"+type,handle); } else { el["on" + type] = handle; } }
然而,能力检测只需要进行一次就可以了。没必要调用函数的时候都需要进行一次判断,这样显然造成没必要的浪费。我们可以用函数的惰性载入技巧来解决上述问题。
惰性载入表示函数执行的分支只会发生一次,实现方式有两种。
第一种就是在函数第一次被调用时,自身会被覆盖成另一个更合适的函数,如下:
var addEvent = function(el,type,handle) { if(el.addEventListener) { addEvent = function(el,type,handle){ el.addEventListener(type,handle,false); } } else if(el.attachEvent) { addEvent = function(el,type,handle){ el.attachEvent("on"+type,handle); } } else { addEvent = function(el,type,handle){ el["on" + type] = handle; } } addEvent(el,type,handle); }
或者简单一点:
var addEvent = function(el,type,handle){ addEvent = el.addEventListener ? function(el,type,handle){ el.addEventListener(type,handle,false); } : function(el,type,handle){ el.attachEvent("on"+type,handle); }; addEvent(el,type,handle); }
第二种是在声明函数时就指定适合的函数:
var addEvent = (function(el,type,handle) { if(addEventListener) { return function(el,type,handle){ el.addEventListener(type,handle,false); } } else if(attachEvent) { return function(el,type,handle){ el.attachEvent("on"+type,handle); } } else { return function(el,type,handle){ el["on" + type] = handle; } } })();
当我们在使用构造函数创建实例的时候,如果我们忘记使用new,那么该函数就相当于普通的函数被调用。由于this是在运行时才绑定的,所以this会映射到全局对象window上。也就是说,调用该函数相当于为全局对象添加属性,这会污染全局空间,造成不必要的错误。
function Person(name,age) { this.name = name; this.age = age; } var Marco = Person('Marco',22); console.log(name); // Marco
function Person(name,age) { if(this instanceof Person) { this.name = name; this.age = age; } else { return new Person(name,age); } } var Marco = Person('Marco',22); console.log(name); //undefined
这样,调用Person构造函数时,无论是否使用new操作符,都会返回一个Person的实例,这就避免了在全局对象上意外设置属性。
七、惰性实例化
惰性实例化避免了在页面中js初始化执行的时候就实例化了类。如果在页面中没有使用到这个实例化的对象,那么这就造成了一定的内存浪费和性能消耗,那么如果将一些类的实例化推迟到需要使用它的时候才开始去实例化,那么这就避免了刚才说的问题,做到了“按需供应”。惰性实例化应用到资源密集、配置开销较大、需要加载大量数据的单体时是很有用的。如下:
var myNamespace2 = function(){ var Configure = function(){ var privateName = "someone's name"; var privateReturnName = function(){ return privateName; } var privateSetName = function(name){ privateName = name; } //返回单例对象 return { setName:function(name){ privateSetName(name); }, getName:function(){ return privateReturnName(); } } } //储存configure的实例 var instance; return { init:function(){ //如果不存在实例,就创建单例实例 if(!instance){ instance = Configure(); } //将Configure创建的单例 for(var key in instance){ if(instance.hasOwnProperty(key)){ this[key]=instance[key]; } } this.init = null; return this; } } }(); //使用方式: myNamespace2.init(); myNamespace2.getName();
八、函数劫持
JavaScript函数劫持即javascript hijacking,通俗来讲就是通过替换js函数的实现来达到劫持该函数的目的。我们可以这样实现函数劫持:保存原函数的实现,替换为我们自己的函数实现。添加我们的处理逻辑之后调用原来的函数实现。如下:
var _alert = alert; window.alert = function(str) { // 我们的处理逻辑 console.log('ending...'); _alert(str); } alert(111);
反劫持
1)首先我们要判断某个函数是否被劫持
var _alert = alert; window.alert = function(str) { // 我们的处理逻辑 console.log('ending...'); _alert(str); } console.log(alert); console.log(_alert);
结果:
function (str) { // 我们的处理逻辑 console.log('ending...'); _alert(str); } function alert() { [native code] }
可以发现内置的函数体为[native code],那我们就可以根据这个判断函数是否被劫持了。
2)如何反劫持
我们要回复被劫持的函数,可以通过创建个新的环境,然后用新环境里的干净的函数来恢复我们这里被劫持的函数,怎么创建新环境?创建新的iframe好了,里面就是个全新的环境。
var _alert = alert; window.alert = function(str) { // 我们的处理逻辑 console.log('ending...'); _alert("呵呵"); } function unHook() { var f = document.createElement("iframe"); f.style.border = "0"; f.style.width = "0"; f.style.height = "0"; document.body.appendChild(f); var d = f.contentWindow.document; d.write(""); d.close(); } unHook(); alert(111); // 11
以上