【优雅代码】深入浅出 妙用Javascript中apply、call、bind

这篇文章实在是很难下笔,因为网上相关文章不胜枚举。

巧合的是前些天看到阮老师的一篇文章的一句话:

“对我来说,博客首先是一种知识管理工具,其次才是传播工具。我的技术文章,主要用来整理我还不懂的知识。我只写那些我还没有完全掌握的东西,那些我精通的东西,往往没有动力写。炫耀从来不是我的动机,好奇才是。"

对于这句话,不能赞同更多,也让我下决心好好写这篇,网上文章虽多,大多复制粘贴,且晦涩难懂,我希望能够通过这篇文章,能够清晰的提升对apply、call、bind的认识,并且列出一些它们的妙用加深记忆。

   apply、call 

在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。

JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

先来一个栗子:

function fruits() {}fruits.prototype = {color: "red",say: function() {console.log("My color is " + this.color);}
}var apple = new fruits;
apple.say();	//My color is red

但是如果我们有一个对象banana= {color : "yellow"} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:

banana = {color: "yellow"
}
apple.say.call(banana);		//My color is yellow
apple.say.apply(banana);	//My color is yellow

所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。

 

apply、call 的区别

对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:

var func = function(arg1, arg2) {};

就可以通过如下方式来调用:

func.call(this, arg1, arg2); 
func.apply(this, [arg1, arg2])

其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。  

JavaScript 中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call 。
而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个伪数组来遍历所有的参数。
 
为了巩固加深记忆,下面列举一些常用用法:

数组之间追加

var array1 = [12 , "foo" , {name "Joe"} , -2458];  
var array2 = ["Doe" , 555 , 100];  
Array.prototype.push.apply(array1, array2);  
/* array1 值为  [12 , "foo" , {name "Joe"} , -2458 , "Doe" , 555 , 100] */

获取数组中的最大值和最小值

var  numbers = [5, 458 , 120 , -215 ];  
var maxInNumbers = Math.max.apply(Math, numbers),	//458maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215);	//458

number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。

验证是否是数组(前提是toString()方法没有被重写过)

functionisArray(obj){  return Object.prototype.toString.call(obj) === '[object Array]' ;
}

类(伪)数组使用数组方法

var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));

Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop 等方法。

但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。

 

深入理解运用apply、call

下面就借用一道面试,来更深入的去理解下 apply 和 call 。

定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:

function log(msg) {console.log(msg);
}
log(1);    //1
log(1,2);    //1

上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:

function log(){console.log.apply(console, arguments);
};
log(1);    //1
log(1,2);    //1 2

接下来的要求是给每一个 log 消息添加一个"(app)"的前辍,比如:

log("hello world");    //(app)hello world

该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift,像这样:

function log(){var args = Array.prototype.slice.call(arguments);args.unshift('(app)');console.log.apply(console, args);
};

 

   bind 详解

说完了 apply 和 call ,再来说说bind。bind() 方法与 apply 和 call 很相似,也是可以改变函数体内 this 的指向。

MDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

直接来看看具体如何使用,在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:

var foo = {bar : 1,eventBind: function(){var _this = this;$('.someClass').on('click',function(event) {/* Act on the event */console.log(_this.bar);		//1});}
}

由于 Javascript 特有的机制,上下文环境在 eventBind:function(){ } 过渡到 $('.someClass').on('click',function(event) { }) 发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind() 可以更加优雅的解决这个问题:

var foo = {bar : 1,eventBind: function(){$('.someClass').on('click',function(event) {/* Act on the event */console.log(this.bar);		//1}.bind(this));}
}

在上述代码里,bind() 创建了一个函数,当这个click事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的栗子:

var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
bar(); // undefined
var func = bar.bind(foo);
func(); // 3

这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。

有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:

var bar = function(){console.log(this.x);
}
var foo = {x:3
}
var sed = {x:4
}
var func = bar.bind(foo).bind(sed);
func();	//? var fiv = {x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func();	//? 

答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。

  

   apply、call、bind比较

那么 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:

var obj = {x: 81,
};var foo = {getX: function() {return this.x;}
}console.log(foo.getX.bind(obj)());	//81
console.log(foo.getX.call(obj));	//81
console.log(foo.getX.apply(obj));	//81

三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。

也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。

 

再总结一下:

  • apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
  • apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
  • apply 、 call 、bind 三者都可以利用后续参数传参;
  • bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

 

本文实例出现的所有代码,在我的github上可以下载。

原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

转载于:https://www.cnblogs.com/coco1s/p/4833199.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/397471.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PHP笔记随笔

1.CSS控制页面文字不能复制: body{-webkit-user-select:none;} 2.【php过滤汉字和非汉字】 $sc"aaad....##--__i汉字过滤"; //iconv("UTF-8","GB2312",$sc);utf-8转码 echo $temperegi_replace("[^\x80-\xff]",""…

qt linux 添加库文件路径,Linux下Qt调用共享库文件.so

jvm--4垃圾收集6. 垃圾收集GC (1)当需要排查各种内存溢出,内存泄漏等问题,当GC成为系统达到更高性能的瓶颈时,我们就需要对这些自动化的GC进行监控和调节. (2)PC计数器.本地方法栈.虚拟机栈,随方法或者线 ...GET和POSTAjax与Comet 1. Ajax Asynchronous Javascriptxml :能够向服…

js进阶 14-8 表单序列化函数serializeArray()和serialize()的区别是什么

js进阶 14-8 表单序列化函数serializeArray()和serialize()的区别是什么 一、总结 一句话总结:两者都是对表单进行序列化,serializeArray()返回的是json对象,serialize()返回的是json形式的字符串,使用起来都是一样的 1、$&#x…

HDU 2842 Chinese Rings(矩阵高速功率+递归)

职务地址:HDU 2842 这个游戏是一个九连环的游戏。 如果当前要卸下前n个环。由于要满足前n-2个都卸下,所以要先把前n-2个卸下。须要f(n-2)次。然后把第n个卸下须要1次,然后这时候要卸下第n-1个。然后此时前n-2个都已经被卸下了。这时候把前n-2…

硬链接与软连接

linux系统硬链接和软连接: 1文件都由文件名和数据组成,在linux中文件被分为两个部分:用户数据和元数据。用户数据:即文件数据块,记录真实数据的地方。元数据:文件的附加属性,记录文件的大小&…

linux 7.2中文命令,CentOS7如何支持中文显示

1.查看系统是否安装有中文语言包locale -a | grep "zh_CN" 命令含义:列出所有可用的公共语言环境的名称,包含有"zh_CN"若出现图中所示几项,那么说明系统中已经安装了语言包,不需要在安装。含义是:…

html-拖拽

html-拖拽(draggable"true")拖拽的7个事件:> 拖拽块.οndragstartfunction(){console.log("拖拽开始");}> 拖拽块.οndragfunction(){console.log("拖拽中");}> 拖拽块.οndragendfunction(){console…

大道至简

道在中国哲学中,是一个重要的概念,表示“终极真理”。此一概念,不单为哲学流派诸子百家所重视,也被宗教流派道教等所使用。大道至简是指大道理(基本原理、方法和规律)是极其简单的,简单到一两句…

别人7天乐,运维还苦逼值班?

你被点名值班了吗?或者你的朋友、隔壁七大姑八大姨的侄子被点名值班了吗? 国庆将至,大家都开始研究各种度假攻略了,国内游、国外游、地球游、外星游。。。然而总有一票人,默默地职守着 -- tIT 公司运营支撑组/运维组。…

【常用损失函数】

一、Smooth L1 Loss 1.公式: 2.原因: L1损失使权值稀疏但是导数不连续,L2损失导数连续可以防止过拟合但对噪声不够鲁棒,分段结合两者优势。 二、Focal Loss 1.公式: 2.作用: 使得正负样本平衡的同时&#x…

ORA-01940: cannot drop a user that is currently connected解决方法

我们在删除数据库用户时候会碰到如下错误 SQL> DROP USER sys_xj cascade; DROP USER sys_xj cascade*ERROR at line 1:ORA-01940: cannot drop a user that is currently connected 解决方法: 1.查询出还在连接的此用户会话进程 SQL> SELECT SID,SERIAL# FR…

实现对象克隆

实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下 package com.lovo; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; i…

linux 读取内存颗粒,linux查看主板内存槽与内存信息的命令dmidecode怎么用

在Linux中,我们常常使用命令来实现许多操作,比如查看内存信息等,下面小编就为大家带来一篇linux查看主板内存槽与内存信息的命令dmidecode方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来…

python 图像处理(从安装Pillow开始)

python 图像处理(从安装Pillow开始) python2.x及以下用的是PIL(图像处理库是 PIL(Python Image Library)),最新版本是 1.1.7 可在http://www.pythonware.com/products/pil/index.htm 下载和学习。 不过从该网站可看出它不支持python3.x Pillow由PIL而来(支持3.x)&…

手机还是不要随便更新的好

新入mate9pro 不到一个月,手贱升级了系统版本,出现导航搜索不到卫星的情况,软件下载了高德地图、腾讯地图、百度地图,逐一卸载安装重试,没一个能成功的,后来又下载了专业搜星软件,还是搜不到卫星…

Java对象容器——List

为什么80%的码农都做不了架构师?>>> 在Java中,我们可以用数组来存放同类型的变量或对象,但是数组有一个缺陷,它的长度不可变,必须在定义时给定其长度,所以说在一些场合下不适用。例如我们要存放…

STL学习笔记(数值算法)

运用数值算法之前必须先加入头文件<numeric> 加工运算后产生结果 1.对序列进行某种运算 T accumulate(InputIterator beg,InputIterator end, T initValue) T accumulate(InputIterator beg,InputIterator end, T initValue,BinaryFunc op) 1.第一种形式计算InitValue和…

angualejs

为什么80%的码农都做不了架构师&#xff1f;>>> http://segmentfault.com/a/1190000000347412 http://www.xker.com/page/e2015/06/199141.html http://www.runoob.com/angularjs/angularjs-application.html http://blog.csdn.net/lglgsy456/article/details/3690…

linux函数地址获取函数名,函数名/函数地址/函数指针

函数指针&#xff1a;1。指针变量 2。指针变量指向函数这正如用指针变量可指向整型变量、字符型、数组一样。在编译时&#xff0c;每一个函数都有一个入口地址&#xff0c;该入口地址就是函数指针所指向的地址。可利用该指针变量调用函数&#xff0c;就如同用指针变量可引用其他…

SPOJ SORTBIT Sorted bit squence (数位DP,入门)

题意&#xff1a; 给出一个范围[m,n]&#xff0c;按照二进制表示中的1的个数从小到大排序&#xff0c;若1的个数相同&#xff0c;则按照十进制大小排序。求排序后的第k个数。注意&#xff1a;m*n>0。 思路&#xff1a; 也是看论文的。一开始也能想到是这种解法&#xff0c;枚…