JavaScript提供了apply和call两种调用方式来确定函数体中this的指向,表现出来的特征就是:对象可以'借用'其他对象的方法。
之前的几篇博客回顾了一些Web控件的一些开发方法,我们聊了如何实现一个自定义的组合框,也聊了一个相对复杂一点的地址控件的开发,从上一篇开始,开始聊一些JavaScript语言本身的话题,回顾了闭包和原型继承,今天我们就一起来聊聊apply和call这两种调用方式的前世今生。
当然,尽管主题在变,但是基于业务场景来剖析理论知识的写作风格不会变。
我们还是从一个生活中的例子说起:
小明家有水果,也有一台'果汁机',小红家也有水果,但是没有果汁机。有一天,小红也想把水果榨成果汁来喝,这时候,小红会怎么做呢?当然就是小红可以"借"小明家的果汁机用一下,用完之后再回去,因为不用时放在自己家里还占地方,下次要用,再去借就是了,因为'互助'是JavaScript社区的美德。
我们看看如何用JavaScript展示这种情况:

var xiaoming = {
name:'小明',
fruit:'橙子',
makeJuice:function(){
console.log( '正在榨:' + this.name + ' 家的' + this.fruit + '汁!');
}
}
var xiaohong = {
name:'小红',
fruit:'苹果'
}
xiaoming.makeJuice( ); //输出:正在榨:小明 家的橙子汁!
xiaoming.makeJuice.apply( xiaohong ); //输出:正在榨:小红 家的苹果汁!

apply方法最核心的意义就是这样,显然,如果某个函数体当中根本没有引用this,那是不是也就失去了调用apply的意义?也并非如此,有时候还需要处理传入的参数。

进一步来看,如果调用的函数需要传递参数,那么调用apply时要如何处理呢?我们改进一下上面的例子,假设榨果汁的时候,需要传入参数:加水的量以及要榨多长时间。
这时候该如何使用apply呢?

var xiaoming = {
name:'小明',
fruit:'橙子',
makeJuice:function( water, time ){
console.log( '正在榨:' + this.name + ' 家的' + this.fruit + '汁,加水:' + water + ' mL,用时:' + time + ' 分钟。');
}
}
var xiaohong = {
name:'小红',
fruit:'苹果'
}
var task_info = [ 500 , 1 ] ; //把要传入的参数放到一个数组里
xiaoming.makeJuice.apply( xiaohong , task_info ) ; //输出:正在榨:小红 家的苹果汁,加水:500 mL,用时:1 分钟。

【分析】
在使用apply方式使用一个函数时:

  • 第1个参数为thisObject,调用时采用传入的thisObject代替函数体中this的指向。
  • 第2个参数传入一个数组,函数会用数组的值取代"参数列表"。

回到上面的例子,相当于这样的场景:

小红:小明,你帮我榨一下苹果汁吧。
小明:可以啊,你把我家的榨汁机拿去用就可以了。
用之前你得先想清楚'准备加多少水,要榨几分钟'。
小红:好的。

小红把榨汁机拿来之后,就先把'加 500mL 水,榨 1 分钟'的内容写到'纸'上,准备好原材料之后,就按'纸'上的信息操作榨汁机,避免手忙脚乱。用来写任务相关信息的'纸'就相当于在apply方式调用时用来传递参数列表信息的数组

说到参数列表,自然就想到了arguments,在调用函数时,函数的运行时环境会自动产生一个变量arguments指向实参列表。很多资料上都会说,arguments是具有数组某些特性的'类数组'(伪数组)。那么,当使用apply方式调用函数时,传入的第2个参数是否可以是一个像arguments这样的'类数组'(伪数组)呢?
我们再构造一个场景来验证一下,最近小区又搬来了一位王奶奶,有一天王奶奶也想喝果汁,她知道小明家有榨汁机,本来想找小明帮忙,但是小明出差了。小明跟王奶奶说,你想好了要多少量的果汁以及想打多长时间,找小红帮忙就可以了。
现在,我们就来实现这样的场景,重点在于王奶奶求助小红的函数。

var wang = {
name: '王奶奶',
helpFromXiaohong: function( water , time ){
//小红自己没有榨汁机,还是要使用小明的榨汁机,使用apply方式调用函数
//王奶奶的要求 与 使用榨汁机时要准备的'任务内容'完全一样,
//所以,这里直接传入arguments看看
//至于水果嘛,小红当然不会向王奶奶要了,就用自己家的
//于是,调用方式如下:
xiaoming.makeJuice.apply( xiaohong , arguments ) ;
}
}
wang.helpFromXiaohong( 400 , 2 ) ; //老人家喝的量不多,但是希望把水果打烂一点 //输出:正在榨:小红 家的苹果汁,加水:400 mL,用时:2 分钟。

发现和我们预期的内容是完全一样的,这就意味着,传入apply中的第二个参数,
也可以是一个'类数组',最常见的当然就是直接将arguments传入作为第2个参数。
'类数组'的特征:

  • 具有一个length成员,'表示'包含的'元素个数'。
  • 能够用1,2,3等数字来检索它的成员。

到现在我们已经对apply调用方式有了一些认识,再回到我们日常的工作当中。我们经常会看到这样的调用方式:

var w = Array.prototype.shift.apply( arguments );

这行代码表示什么意思也许大家都很清楚,就是将隐含的'类数组'arguments的第一个参数值取出来,然后赋给变量w。

【思考】

1. 为什么不直接用arguments调用shift函数呢?

因为arguments不是真正的'数组',从JavaScript的语言特征来说,arguments仅仅是具有某些'数组特征'的对象。它不是通过new Array()的方式创建,它的原型链也没有链向'Array.prototype',所以不能直接使用shift()函数。
"靠,既然是语言自带的东东,为什么不直接设计成数组呢?搞得老子每次想用一下数组的相关方法还得拐个弯。"
兄台息怒,其实这样想的人并不是您一个人,包括JavaScript的大师老道(Douglas Crockfod)也是这么想的,正所谓英雄所见略同。

2. 如何理解var w = Array.prototype.shift.apply( arguments );这一个语句呢?

我们了解到,根据apply的调用模式,它会用传入的第1个参数代替函数体中的this。从这里来看,就是用arguments这个对象(具有'数组'特征的特殊对象)代替了Array.prototype.shift中的this。

我们知道,如果用一个数组对象去调用shift是没有问题的。
例如:

console.log( ['A','B','C'].shift() ) ; //输出: A

因为在['A','B','C'].shift()的调用过程中,没有传入任何参数,所以,可以推断Array.prototype.shift的函数体中,肯定引用了this。通过对this.length 以及 this[0] 这种方式的处理来计算运算结果,

显然,这个特殊的对象arguments进行arguments.length 以及 arguments[0] 这样的使用方式是没有问题的,是能体现出它的'数组特征'的,所以,通过调用Array.prototype.shift.apply( arguments );能够获得传入的第1个参数值。

为了增加'画面感',我们把它放入前面王奶奶求助的函数中:

var wang = {
name: '王奶奶',
helpFromXiaohong: function( water , time ){
var w = Array.prototype.shift.apply( arguments );
console.log( '王奶奶想喝 ' + w + ' mL的果汁。' );
xiaoming.makeJuice.apply( xiaohong , arguments ) ;
}
}
wang.helpFromXiaohong( 400 , 2 ) ;

>期望输出:
王奶奶想喝 400 mL的果汁。
正在榨:小红 家的苹果汁,加水:400 mL,用时:2 分钟。
>实际输出:
王奶奶想喝 400 mL的果汁。
正在榨:小红 家的苹果汁,加水:2 mL,用时:undefined 分钟。

【分析】

1. 对于我们刚才想验证的结论,发现我们的假设是正确的。arguments对象成功'借用'了数组的shift函数,所以输出:王奶奶想喝 400 mL的果汁。
2. 但是,在下面的调用中,居然输出的是:正在榨:小红 家的苹果汁,加水:2 mL,用时:undefined 分钟。

这个很好理解,shift函数的作用是:'弹出'数组的'第1个元素'并返回。这就意味着,经过var w = Array.prototype.shift.apply( arguments );调用之后,arguments中的内容也发生了变化,arguments[0]的值已经不是400!

这也再一次说明:

  • apply的调用方式,除了替换函数体中的this的指向之外,函数的其他逻辑没有发生任何变化。'借用'的函数的效果,就跟对象自己拥有这个函数一样。
  • arguments这个'类似数组',除了不是'原型继承自'Array.prototype之外,其他的特征和数组也是一样一样的。

3. 在刚才的场景下,如果你确实需要在调用xiaoming.makeJuice.apply( xiaohong , arguments ) ;之前显示一下王奶奶想喝多少mL的果汁,直接调用var w = arguments[0]; 就可以了,何必要'弹'人家呢。

完整的例子如下:

var xiaoming = {
name:'小明',
fruit:'橙子',
makeJuice:function( water, time ){
console.log( '正在榨:' + this.name + ' 家的' + this.fruit + '汁,加水:' + water + ' mL,用时:' + time + ' 分钟。');
}
}
var xiaohong = {
name:'小红',
fruit:'苹果'
}
var wang = {
name: '王奶奶',
helpFromXiaohong: function( water , time ){
var w = arguments[0];
console.log( '王奶奶想喝 ' + w + ' mL的果汁。' );
xiaoming.makeJuice.apply( xiaohong , arguments ) ;
}
}
wang.helpFromXiaohong( 400 , 2 ) ;

最后,我们再补充说明一个特性:如果我们在使用apply调用方式时,第1个参数传入的是null,但是,被'借用'函数的函数体本身又使用了this,那么,会不会报异常呢?因为直接写null.name这样的方式肯定是不行的。这里不卖关子,先把答案搁这里。答案是:不会报异常。
话说过了几天,王奶奶又想要喝果汁了,于是又给小明打电话,但是小明还在出差啊,于是小明又叫王奶奶去找小红帮忙,另外,小明也给小红打了个电话解释情况。这回接到小明的电话,小红可有点不乐意了,心想:'小明你是什么人啊?好名声你来拿,麻烦事我去做。'尽管心里有些不爽,但是毕竟是热心肠的好女孩,王奶奶的忙还是帮了,不过,这一回,她可就没有拿自己家的水果去榨了,而是直接用了社区的水果。

回到王奶奶的求助函数,既然不用xiaohong家的水果,就不用传入xiaohong这个对象了,传一个null试试看。

xiaoming.makeJuice.apply( xiaohong , arguments ) ;

修改为:

xiaoming.makeJuice.apply( null , arguments ) ;

并且在代码的开头增加以下变量:

var name = '社区';
var fruit = '西瓜' ;

这时候,输出的内容为:
王奶奶想喝 400 mL的果汁。
正在榨:社区 家的西瓜汁,加水:400 mL,用时:2 分钟。

也就是说,如果第一个传入的参数是null,那么,在函数体内的this会指向全局对象,在浏览器中就是window(在Chrome浏览器中是这样的效果)。很显然,如果函数体内用到了this,而你在用apply方式调用它时,给它传一个null过去,这是一种'小红同学赌气'的行为,不是一个好的习惯。

所以,对于apply调用方式,我们可以总结如下:

  1. 如果某个函数的函数体中明显使用了this,那么,就应该传入一个明确的thisObj对象,并且这个对象应该包含相关的属性。类似于人家把'榨汁机'借给你用已经不错了,你不能自己连水果也不准备吧?
  2. 如果某个函数就是'工具'类型的,那么,在使用apply调用方式时,可以传入一个null作为thisObject。类似于'水果刀',使用时放在跕板上的水果是什么,它就切什么,跟拿着水果刀的人有什么没有半毛钱关系。

第二种情况的一个常见的使用场景是:函数的接口要求传入一个'参数列表',但你手头只有一个数组。
例如:希望你这样调用Math.max( 2 , 10 , 6 , 1 ); 但是,你的手头只有[2,10,6,1]这样的一个数组。
-如果直接调用Math.max([2,10,6,1]); 会输出NaN。因为Math.max会认为第一个参数[2,10,6,1]根本就不是一个number。
-或者你可以这样:

var test_array = [2,10,6,1];
Math.max( test_array[0] , test_array[1] , test_array[2] , test_array[3] );

显然,这种方式是在练习打字,而不是在编程序^_^~~

[参数列表]和[数组],[数组]和[参数列表].......apply调用方式不就可以实现转换嘛,所以,我们可以这样操作:

 var test_array = [2,10,6,1];
console.log( Math.max.apply( null , test_array ) ); //输出:10

输出了我们期望的结果:10,是的,当时就是这样。

如果小红也不用社区的水果,直接用小明家的水果呢?那就不需要用apply方式调用了,直接用如下方式就可以了:

xiaoming.makeJuice( arguments ) ; 

我们再来看一下这时候的王奶奶的求助函数:

var wang = {
name: '王奶奶',
helpFromXiaohong: function( water , time ){
var w = arguments[0];
console.log( '王奶奶想喝 ' + w + ' mL的果汁。' );
xiaoming.makeJuice( arguments ) ;
}
}

看函数名称是helpFromXiaohong(从小红处获得帮助),实际上却是xiaoming.makeJuice(小明在提供帮助)。我们隐约感受到了某些'设计模式'的感觉。关于设计模式,我们下次有空再聊。

今天的话题就聊到这里...

什么?还没有讲到call呢!

差点搞忘了,call方式和apply方式的差别主要体现在传入的形式参数的不一样,当采用call调用的时候,第1个参数传入thisObject,第2个参数以及后面的参数组成'实参列表'传递给函数。
例如:
用apply方式调用时,我们会这样写:

var task_info = [400,2];
xiaoming.makeJuice.apply( xiaohong , task_info ) ;

而用call方式调用时,我们会这样写:

xiaoming.makeJuice.call( xiaohong , 400 , 2 ) 

有人说,其实call调用方式就是使用apply方式实现的,类似于给Function.prototype定义了一个成员函数call。JavaScript引擎到底如何实现,我们无从知晓,不过从JavaScript大神老道在他的书中只字未提call方式,可见我们只要理解了apply调用方式,并且知道apply和call在形式参数上的不同,也许就可以了。

形式参数上的差异,我们可以再用一个生活中的例子理解一下:

小明经常去韩国出差,小红呢,经常委托小明帮忙在韩国带一些化妆品。
往常在小明出差前,小红都会把要买的化妆品写在一个清单里(相当于:apply模式把参数值放到一个数组里),
小明到了韩国的商场之后,只要拿出购物清单对照着买就可以了,相当于应用(apply)一下这个购物清单,这就是apply调用方式。
但是,有一次,小明因为出差时间短,就没有告诉小红,快回国到机场的时候,接到了小红的电话呼叫(call),没错,是小红call过来的。
"死鬼,去韩国出差也不说一声!是存心躲着本姑奶奶吗?!"
"你听我解释,这不时间太短了,我马上就回国了,现在已经快到釜山机场了。"
"现在在机场了也得给我买!你听好了我要买什么!"
"好吧,我现在去给你买,你说一下你要买什么。"
显然,小明手里头没有一个明确的购物清单,时间又那么紧,不可能先整理一张清单出来再去商场购买。
于是,小明就到商场里面,听着小红的电话,小红说一个,小明就把对应商品往购物车里放,这就是call方式。

【总结】

在本文当中,我们解释了函数的apply调用方式的价值和使用方式,并结合常见的使用场景,解释了函数体中隐含的arguments对象的一些特征,也解释了apply调用方式的一些注意事项,最后指出了apply调用方式和call调用方式的差异。
今天的话题就聊到这里,感谢诸位捧场。

兄台息怒,关于arguments,您的想法和大神是一样一样的----闲聊JS中的apply和call的更多相关文章

  1. 闲聊js中的apply、call和arguments

    JavaScript提供了apply和call两种调用方式来确定函数中的this的指向,在现实编码中,我确实 很少接触到这两个方法.但很无奈,很多面试题都要考这两种方法,我又没怎么用到,所以我们先来 ...

  2. js中的arguments用法

    //arguments对象并不是一个数组,但是访问单个参数的方式与访问数组元素的方式相同 function show(){ console.log(arguments); //arguments.pu ...

  3. js中arguments的用法

    了解这个对象之前先来认识一下javascript的一些功能: 其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载.Javascrip中国每个函数都会有一个Argume ...

  4. js中的原型、继承的一些想法

    最近看到一个别人写的js类库,突然对js中的原型及继承产生了一些想法,之前也看过其中的一些内容,但是总不是很清晰,这几天利用空闲时间,对这块理解了一下,感觉还是有不通之处,思路上没那么条理,仅作为分享 ...

  5. js中arguments的应用

    Javascrip中的每个函数都会有一个Arguments对象实例arguments,它引用着函数的实参,可以用数组下标的方式"[]"引用arguments的元素.argument ...

  6. js中arguments

    arguments 每天一对象,JS天天见,今天我们来看看arguments对象及属性.arguments对象不能显式创建,arguments对象只有函数开始时才可用.函数的 arguments 对象 ...

  7. js 中arguments,call,apply,bind的使用

    //对于 arguments和this, 每个函数都有自己独有的arguments和this, 且不进行链式查找 //arguments是什么? //答:1:arguments是收到的实参副本 //2 ...

  8. js中arguments详解

    在js中一切都是对象,连函数也是对象,函数名其实是引用函数定义对象的变量. 什么是arguments? 这个函数体内的arguments非常特殊,实际上是所在函数的一个内置类数组对象,可以用数组的[i ...

  9. js中的arguments

    了解这个对象之前先来认识一下javascript的一些功能: 其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载.Javascrip中国每个函数都会有一个Argume ...

随机推荐

  1. 即时通信(RPC)的Rtmp实现--代码实现篇

    实现的一般步骤是: step 1: 定义NetConnection对象连接rtmp,并监听NetStatusEvent.NET_STATUS事件 step 2: 在NetStatusEvent.NET ...

  2. 【React Native 实战】商品分类

    1.前言 商品分类是各种app常见的一种操作,一般都是左右两栏构成,左边栏是商品的分类,右边栏是商品的展示,同时左右两栏都可以滑动.今天我们就用React Native来实现这种效果. 实现内容:1) ...

  3. iOS开发——开发必备OC篇&彩票实战之精华讲解

    彩票实战之精华讲解 在这段时间自己研究并学习关于彩票项目开发的时候遇到各种坑,各种技术点以前或许之前用过但是却用起来不是那么熟悉,所以没遇到一个重点的地方我就会记录一下,希望不会再有下次. 本文主要讲 ...

  4. iOS开发——网络编程OC篇&使用WebView构建HyBird应用

    使用WebView构建HyBird应用 HyBird是一种本地技术与Web相结合,能过实现跨平台的移动应用开发,最常用的一个框架:PhoneGap 一:首先,写好html代码 <!DOCTYPE ...

  5. Category目录

    Category目录 目录 概述——对Category的理解 创建Category Category的用途 概述——对Category的理解 当我们想往原有的类中添加新的成员方法但又不想改变原有的类和 ...

  6. Js 数组——filter()、map()、some()、every()、forEach()、lastIndexOf()、indexOf()

    filter():   语法: var filteredArray = array.filter(callback[, thisObject]); 参数说明: callback: 要对每个数组元素执行 ...

  7. 疑难杂症:org.hibernate.MappingException: Unknown entity,annotation配置Entity类报错

    引言: 夜声人静,外面下着稀里哗啦的雨,周末的晚上,还在键盘上舞动手指. 此刻很感激一个人一篇随笔,感谢xiaochao以及他的<org.hibernate.MappingException: ...

  8. 1.5.1 Analyzers,Tokenizers,Filters概述

    字段分析器(Analyzers)即用于文档索引也用于查询.一个分析器检查字段的文本,并生成一个token流.分析器可能是一个单独的类,也可能是一系列的tokenizer和filter的组合. 分词器把 ...

  9. 杂乱无章之Oracle(二)

    六.IMPDP用法 1.导入表 impdp hsiufo/hsiufo directory=dump_dir dumpfile=full.dmp tables=scott.emp remap_sche ...

  10. 【Linux/Ubuntu学习4】ubuntu 下面安装 vim 的问题

    ubuntu 下面安装 vim 的问题 1.输入vim时,显示: 程序“vim”已包含在以下软件包中: * vim * vim-gnome * vim-tiny * vim-gtk * vim-nox ...