阅读 1809

趣谈js的call和apply两大召唤术

前言

《趣谈js的bind牌胶水》这篇文章中,我聊到了js的bind牌胶水,这篇文章我来聊聊bind牌胶水的升级版:call和apply方法。

Why? ——> 为什么会出现apply和call?

《趣谈js的bind牌胶水》中,我通过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语法
复制代码

具体的语法可以去MDN上看详情,这里关于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有以如下相同点:
  1. 第一个参数指明了宿主对象
  2. 指明了新宿主对象后,立即运行该函数
  • 唯一不同点: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); // 20
console.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指向
  • 不同点:
    1. call和applly会立即执行函数,bind只是绑定了函数,并不会立即执行函数
    2. call、apply因为要立即执行函数,所以第二个参数或之后的参数都是当前的真实参数,bind是“预设参数”(这里可以参考文章《趣谈js的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的“箭头函数”可以这样绑定:

<div
  onClick={(res) => {
    // 这里的this就是
    this.setState({
      name: 'jack'
    });
  }}
>
  Click Me
</div>

复制代码

比如在上面如何使用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作为一个“妥协方案”终将会慢慢的退出舞台,但在它们被遗忘之前理解设计者们的智慧和想法,我觉得是很有意思的。

结语

文章涉及内容很多,难免会有纰漏,望理性指正,一起进步哦。

关注下面的标签,发现更多相似文章
评论