前言
在这篇文章中,我聊到了js的bind牌胶水,这篇文章我来聊聊bind牌胶水的升级版:call和apply方法。
Why? ——> 为什么会出现apply和call?
在中,我通过js的相关历史,叙述了bind、call、apply三方法诞生的背景,同时也指出这三个方法出现的共同目的就是就是为js的一等公民Function函数找个门当户对的人家(指明Function函数的this指向),既然bind方法已经满足了目的,为什么还需要创造出call、apply两个方法呢?这两个方法和bind有哪些异同点?带着些许疑问,且随小生遨游前行。
What? ——> call和apply是啥玩意儿?
1、汉语释义:
call:召唤、呼叫、访问
apply:应用、适用、申请
在call和apply的中文释义中我们可以看出call、apply这两个方法带有明显的连接特性,比如“召唤call”:who召唤who?“应用apply”:who应用到who上?还有bind的中文释意义:“绑定”,从这三个中文释义中不难看出满足连接特性的动词需要三元素:1.主动连接方、2.被动连接方、3.连接二者的中介。对比这三个中文释义,可以看出bind和call、apply的释义略有不同,bind的中文释义带有明显的静态连接特性(只连接),call、apply的中文释义中带有明显的动态连接特性(连接之后还使用),所以在三个方法的使用上,bind只负责连接函数与相应的对象,call、apply在连接好函数与相应的对象后还主动把“连接了指定对象的函数”给当场运行了!
2、语法解析:
function.call(thisArg, arg1, arg2, ...); // call语法function.apply(thisArg, [argsArray]); // apply语法复制代码
具体的语法可以去上看详情,这里关于thisArg
说以下几个注意点:
- 不传,或者传null,undefined,this指向window对象(如果没有房子,那就只能露宿天地了,55555)
- 传递另一个函数的函数名fun2,this指向函数fun2的this指向(fun2随谁,俺就随谁,嫁鸡随鸡嫁狗随狗?)
- 值为原始值(数字,字符串,布尔值),this会指向该原始值的自动包装对象,如Number、 String、Boolean
- 传递一个对象,函数中的this指向这个对象
在上面的几种thisArg
参数例子中,我们发现一个共同的事实就是:thisArg
参数永远会是个对象,原始值就用原始值对应的包装对象,函数就用该引用该函数的对象,无对象时就是全局对象,那些看上去没对象的情况,其实也是有对象的,不难看出,js是一门面向对象编程的语言,处处都是对象,万物皆有对象,那你呢,你有没有对象?
3、详细叙述:
call和apply方法都是为了改变函数的this值而生,具体使用如下:
var obj = { age: 22 } function say(name) { console.log('我是:' + name + '|今年:' + this.age); } say.call(obj, 'jack'); // 我是:jack|今年:22 say.apply(obj, ['mike']); // 我是:mike|今年:22复制代码
- 通过代码可以看出call和apply有以如下相同点:
- 第一个参数指明了宿主对象
- 指明了新宿主对象后,立即运行该函数
- 唯一不同点:apply接收的是数组格式的参数,call接受的是若干个参数。关于两种传参形式,我是这样理解的:apply带有“授予”之意,类似皇帝的封赏(是一种自上而下的交接),皇帝的封赏会给你一个清单,有些啥子东西都在清单里,call带有“呼唤”之意(是一种比较亲密的交接),你呼唤一个朋友过来,给他讲些小秘密,你会一五一十的把这些秘密逐个讲出来。
How? ——> 怎样使用call和apply?
call技能 —— 北风骤起:
技能详解: “Master”从天地中召唤出一个强力风暴,逐一对多个目标造成60/85/135/160(+0.35)点魔法伤害。
技能演示:
var Master = { name: '召唤师'};var target1 = 'enemy1';var target2 = 'enemy2';var target3 = 'enemy3';var target4 = 'enemy4';var target5 = 'enemy5';function NorthernStorm(target1, target2, target3, target4, target5) { console.log(this.name + ' have slained an enemy ' + target1); console.log(this.name + ' have slained an enemy ' + target2); console.log(this.name + ' have slained an enemy ' + target3); console.log(this.name + ' have slained an enemy ' + target4); console.log(this.name + ' have slained an enemy ' + target5);}NorthernStorm.call(Master, target1, target2, target3, target4, target5);复制代码
apply技能 —— 末日风暴:
技能详解:“Master”从天地中召唤出一个强大的末日风暴,可以瞬间应用到一个目标群体上,造成200/250/300/444(+1)点AOE魔法伤害。
技能演示:
var Master = { name: '召唤师'};var target1 = 'enemy1';var target2 = 'enemy2';var target3 = 'enemy3';var target4 = 'enemy4';var target5 = 'enemy5';function PowerfulStorm(arr) { console.log(this.name + ' Penta Kill!');}PowerfulStorm.apply(Master, [target1, target2, target3, target4, target5]);复制代码
哈哈,上面我用游戏技能简单的演示了一下call和apply方法的使用,希望能帮助大家理解相关概念,为了加深理解这里我针对几个具体的使用场景做了几个示例:
1. 获取数组中的最大/小值
var nums = [11, 15, 2, 20, 10];var max = Math.max.apply(null, nums);var min = Math.min.apply(null, nums);console.log(max); // 20console.log(min); // 2复制代码
2. 将函数的arguments转换为数组
function func() { var args = Array.prototype.slice.call(arguments); console.log(args);}func('hello', 'world'); // ["hello", "world"]复制代码
3. 判断是否为数组格式
var arr = [];var res = Object.prototype.toString.call(arr); // 这里获取的是变量的 [[class]]属性,一般方法没有,只有借用Object原型上的toString方法才可以console.log(res); // [Object Array]复制代码
关于apply和call的使用例子不做过多叙述,因为网上一大把,之前一直觉得js的call、apply、bind三方法使用很别扭,很丑陋(现在也觉得),后来我学会换个角度看世界后就舒服了很多,以这个例子为例:
var nums = [11, 15, 2, 20, 10];var max = Math.max.apply(null, nums);复制代码
我们把不相关的剔除掉(1、为空时this指向的对象就是Window全局对象;2、Window对象取代Math对象使用max方法),代码如下:
Window.max(nums);复制代码
注意:上面的代码只是辅助理解,在实际运行时,Window对象上只会短暂的存在max方法,一次性的使用了max方法之后,就会从Window上delete掉max方法,所以通过call、apply绑定给指定对象的函数最终并不会存在于指定对象上。
总结
1. bind和apply、call的异同
- 相同点:都立足于改变函数的this指向
- 不同点:
- call和applly会立即执行函数,bind只是绑定了函数,并不会立即执行函数
- call、apply因为要立即执行函数,所以第二个参数或之后的参数都是当前的真实参数,bind是“预设参数”(这里可以参考文章中关于bind预设参数的阐述)
一些想法
我个人一直觉得bind、call、apply使用起来不舒服,感觉可有可无,但后来发现这三个方法还是有很多用武之地的,比如在dom对象中绑定事件就需要bind方法,比如想复用某些函数就可以用到call和apply,js出现这三个方法很大程度上是因为js用的是函数式编程的样子,但其实又是面向对象(DOM对象,数据对象等)的里子,两种编程思路参杂在了一起,参杂其实没问题,但二者的参杂没能很好融合,设计bind、apply、call就是为了讨好两方,融合二者,但这种带有临时性质的妥协方案,效果不咋地,因为一山不容二虎,总得有人做红花,有人甘当绿叶,不是吗?直到以Angular、React、Vue等为代表的MVVM架构和改进的ES6新标准出现,前端开发进入新的模式,MVVM架构能让前端开发较好的实现“面向对象”的编程模式,同时利用ES6的相关特性兼顾函数式编程的灵活性,以往很多问题都不需要bind、call、apply这三兄弟了,比如ES6的箭头函数就是解决bind的神器,在React的开发中,如果按照传统思路给事件的匿名函数绑定对象,需要手动用bind绑定,但利用ES6的“箭头函数”可以这样绑定:
{ // 这里的this就是 this.setState({ name: 'jack' }); }}> Click Me复制代码
比如在上面如何使用call、apply的例子中可以用ES6的扩展操作符...替代来处理:
// 将arguments转换为数组function func() { var args = ([...arguments]); console.log(args);}func('hello', 'world'); // ["hello", "world"]// 求数组最大值var res = Math.max(...[2,20,22]);console.log(res); // 22复制代码
JS在不断的升级,这三个方法在当前开发的某些场景中可能还会有用武之地,但在我看来,bind、apply、call作为一个“妥协方案”终将会慢慢的退出舞台,但在它们被遗忘之前理解设计者们的智慧和想法,我觉得是很有意思的。
结语
文章涉及内容很多,难免会有纰漏,望理性指正,一起进步哦。