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函数使用技巧的更多相关文章

  1. 【JS小技巧】JavaScript 函数用作对象的隐藏问题

    用户反馈 @消失的键盘 在论坛反馈了一个问题,在 AppBoxMvc 中的 Title 模型中,如果将 Name 属性改名为小写的 name 属性,就会报错: 因为这是一个 ASP.NET MVC 的 ...

  2. 【JS小技巧】JavaScript 函数用作对象的隐藏问题(F.ui.name)

    用户反馈 @消失的键盘 在论坛反馈了一个问题,在 AppBoxMvc 中的 Title 模型中,如果将 Name 属性改名为小写的 name 属性,就会报错: 因为这是一个 ASP.NET MVC 的 ...

  3. 把多个JavaScript函数绑定到onload事件处理函数上的技巧

    一,onload事件发生条件 用户进入页面且页面所有元素都加载完毕.如果在页面的初始位置添加一个JavaScript函数,由于文档没有加载完毕,DOM不完整,可能导致函数执行错误或者达不到我们想要的效 ...

  4. 第八章:Javascript函数

    函数是这样一段代码,它只定义一次,但可能被执行或调用任意次.你可能从诸如子例程(subroutine)或者过程(procedure)这些名字里对函数概念有所了解. javascript函数是参数化的: ...

  5. javascript this 代表的上下文,JavaScript 函数的四种调用形式

    JavaScript 是一种脚本语言,支持函数式编程.闭包.基于原型的继承等高级功能.其中JavaScript 中的 this 关键字,就是一个比较容易混乱的概念,在不同的场景下,this会化身不同的 ...

  6. 编写javascript的基本技巧一

    自己从事前端编码也有两年有余啦,时间总是比想象中流逝的快.岁月啊,请给我把时间的 脚步停下吧.不过,这是不可能的,我在这里不是抒发时间流逝的感慨.而是想在这分享两 年来码农生活的一些javascrip ...

  7. 深入理解JavaScript函数

    本篇文章主要介绍了"深入理解JavaScript函数",主要涉及到JavaScript函数方面的内容,对于深入理解JavaScript函数感兴趣的同学可以参考一下. JavaScr ...

  8. 初学者学习JavaScript的实用技巧!

    Javascript是一种高级编程语言,通过解释执行.它是一门动态类型,面向对象(基于原型)的直译语言.它已经由欧洲电脑制造商协会通过ECMAScript实现语言标准化,它被世界上的绝大多数网站所使用 ...

  9. ABP(现代ASP.NET样板开发框架)系列之21、ABP展现层——Javascript函数库

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之21.ABP展现层——Javascript函数库 ABP是“ASP.NET Boilerplate Project ...

随机推荐

  1. linux查看操作系统的版本

    内核信息 uname -a localhost.localdomain:所在主机的主机名,与主机配置文件/etc/hosts内容一致 2.4.20-8#1:内核版本号 Thu Mar 13 17:18 ...

  2. RabbitMQ运行机制

    AMQP中消息的路由过程和Java开发者熟悉的JMS存在一些差别,AMQP中增加了Exchange和Binding的角色,生产者把消息发布到Exchange上,Binding决定发布到Exchange ...

  3. SpringBoot修改Servlet相关配置

    第一种方式在配置文件中进行修改 server.port=8081 server.servlet.context-path=/springboot server.tomcat.uri-encoding= ...

  4. Linux下常见音频格式之间的转换方法

    Linux下常见音频格式之间的转换方法[转] 下面简单介绍下Linux环境常见音频格式之间的转换方法: MP3 相关工具: lameOGG 相关工具: vorbis-toolsAPE 相关工具: ma ...

  5. 03-MySql安装和基本管理

    本节掌握内容: MySQL的介绍安装.启动 windows上制作服务 MySQL破解密码 MySQL中统一字符编码 MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 O ...

  6. SonarQube代码质量管理工具的升级(sonarqube6.2 + sonar-scanner-2.8 + MySQL5.6+)

    SonarQube升级注意事项 0. 前提条件 如果之前是使用sonarqube5.2 + sonar-runner-2.4 +MySQL5.5版本或者类似的组合. 安装方法请参照SonarQube代 ...

  7. 1. let 和 const 命令

    一.简单认识 1. 用let来声明变量,变量作用域就在{}(块级作用域)中 2. 用const声明变量,变量值不可更改 3. 增加了let以后,在声明变量时应该多考虑一下变量的用途,是否希望只在当前代 ...

  8. python下图像读取方式以及效率对比

    https://zhuanlan.zhihu.com/p/30383580 opencv速度最快,值得注意的是mxnet的采用多线程读取的方式,可大大加速

  9. 洛谷P1970 花匠

    传送门 首先可以知道,如果一个序列是连续上升的,那么只需要取这一个序列中最高的元素即可,因为取其它的不能保证大于后面的.连续下降的序列同理.而这些恰好就是波峰和波谷. 所以遇到 $ j $ 比之前的 ...

  10. java 类字面常量,泛化的Class引用

    类名.class 就是字面常量,代表的就是该类的Class对象引用.常量需要赋值给变量 简单,安全. 编译期接受检查,不需要像forName一样置于try/catch块中. 加载后不会进行初始化,初始 ...