Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
henry-fun opened this issue May 21, 2018 · 1 comment
Open

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

henry-fun opened this issue May 21, 2018 · 1 comment

Comments

@henry-fun
Copy link
Owner

henry-fun commented May 21, 2018

前言

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

结语

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

@qgqceo
Copy link

qgqceo commented Oct 26, 2021

谢谢。学到东西了,感谢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants