JavaScript函数使用技巧
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)); //
function add(n1,n2) {
return n1 + n2;
}
var currAdd = curry(add,2,3);
alert(currAdd()); //
柯里化作为函数绑定的一部分包含在其中,构造更加复杂的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); //
以上
JavaScript函数使用技巧的更多相关文章
- 【JS小技巧】JavaScript 函数用作对象的隐藏问题
用户反馈 @消失的键盘 在论坛反馈了一个问题,在 AppBoxMvc 中的 Title 模型中,如果将 Name 属性改名为小写的 name 属性,就会报错: 因为这是一个 ASP.NET MVC 的 ...
- 【JS小技巧】JavaScript 函数用作对象的隐藏问题(F.ui.name)
用户反馈 @消失的键盘 在论坛反馈了一个问题,在 AppBoxMvc 中的 Title 模型中,如果将 Name 属性改名为小写的 name 属性,就会报错: 因为这是一个 ASP.NET MVC 的 ...
- 把多个JavaScript函数绑定到onload事件处理函数上的技巧
一,onload事件发生条件 用户进入页面且页面所有元素都加载完毕.如果在页面的初始位置添加一个JavaScript函数,由于文档没有加载完毕,DOM不完整,可能导致函数执行错误或者达不到我们想要的效 ...
- 第八章:Javascript函数
函数是这样一段代码,它只定义一次,但可能被执行或调用任意次.你可能从诸如子例程(subroutine)或者过程(procedure)这些名字里对函数概念有所了解. javascript函数是参数化的: ...
- javascript this 代表的上下文,JavaScript 函数的四种调用形式
JavaScript 是一种脚本语言,支持函数式编程.闭包.基于原型的继承等高级功能.其中JavaScript 中的 this 关键字,就是一个比较容易混乱的概念,在不同的场景下,this会化身不同的 ...
- 编写javascript的基本技巧一
自己从事前端编码也有两年有余啦,时间总是比想象中流逝的快.岁月啊,请给我把时间的 脚步停下吧.不过,这是不可能的,我在这里不是抒发时间流逝的感慨.而是想在这分享两 年来码农生活的一些javascrip ...
- 深入理解JavaScript函数
本篇文章主要介绍了"深入理解JavaScript函数",主要涉及到JavaScript函数方面的内容,对于深入理解JavaScript函数感兴趣的同学可以参考一下. JavaScr ...
- 初学者学习JavaScript的实用技巧!
Javascript是一种高级编程语言,通过解释执行.它是一门动态类型,面向对象(基于原型)的直译语言.它已经由欧洲电脑制造商协会通过ECMAScript实现语言标准化,它被世界上的绝大多数网站所使用 ...
- ABP(现代ASP.NET样板开发框架)系列之21、ABP展现层——Javascript函数库
点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之21.ABP展现层——Javascript函数库 ABP是“ASP.NET Boilerplate Project ...
随机推荐
- ActiveMQ安装与入门程序 & JMS的消息结构
1.Activemq安装 直接到官网下载:记住apache的官网是域名反过来,比如我们找activemq就是activemq.apache.org. 最新版本要求最低的JDK是8,所以最好在电脑装多个 ...
- UML入门[转]
访问权限控制 class Dummy { - private field1 # protected field2 ~ package method1() + public method2() } Al ...
- iptables实现网络防火墙及地址转换
iptables主机防火墙功能及常用命令 FSM:Finite State Machine 有限状态机 客户端:closed -->syn_sent -->established --&g ...
- 彻底理解this指向-----实例分析
this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁,一定要搞清楚这个. 情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是wind ...
- Socket 实现聊天功能
注:本文来自:简书:jianshu 作者:jijs链接:http://www.jianshu.com/p/7c0722a8b66f來源:简书著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...
- mysql命令行怎么清屏
例如: 怎么清屏? 哈哈 我也百度了半天,之后发现,这是个坑啊,dos(面向磁盘的操作命令)下面我们都是 cls 清屏,所以习惯性的用cls结果报错,打脸了吧.. mysql 命令行窗口不想看到那一堆 ...
- My Sql控制台命令
1.连接Mysql 格式: mysql -h主机地址 -u用户名 -p用户密码 1.连接到本机上的MYSQL.首先打开DOS窗口,然后进入目录mysql\bin,再键入命令mysql -u root ...
- 性能测试十七:liunx下jmeter结果报表、html报表
控制台日志,只能看出整体的数据,若只是测单接口,这个最好用,但若有多个接口时,无法分别展示,所以需要换另外一种报表 四种方式来获取Jmeter的结果报表 一.在GUI模式下跑Jmeter的脚本,用tp ...
- ubuntu装软件包
查找包 aptitude search pkgName pkgName可以有多个(空格分隔) ffmpeg sudo add-apt-repository ppa:kirillshkrogalev/f ...
- 【C++ Primer | 15】C++虚函数表剖析②
多重继承 下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系. 注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记 ...