博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
前端战五渣学JavaScript——call、apply以及bind
阅读量:5952 次
发布时间:2019-06-19

本文共 9929 字,大约阅读时间需要 33 分钟。

写这篇博客之前,我想先说下今天(2019年3月28日)一直关注的一件事吧(出于凑热闹的心情——尴尬)。在昨天,全球最大交友网站Github上悄然出现一个名为996.ICU的文档项目,整个项目没有代码,只是列了一些《劳动法》的条款和最近表明实行996工作制的公司。本来以为是一个小打小闹的抱怨,结果今天中午再看的时候star数已经有30k以上,并且issues达到5000+。下午更是势如破竹,在Github的star排行榜上,一路过五关斩六将,截止目前,这个出现不到24小时的项目,坐拥63k的star,并且排行榜第21名。为什么一个这么简单的项目会异军突起,伴着屠榜的架势,一发不可收拾。也许这只是触动了被强行996工作的朋友们,以及无休止的加班没有回报的程序员们心中那最敏感的神经,可能迫于生计问题,现实生活中只能忍气吞声,但当出现一个虚拟的世界可以让你尽情发泄的时候,心中的苦水倾泻而出,造就了这个怪异的项目。我们不是不能接受996,是要实行996工作制公司得付的出相应的报酬,这让员工感觉自己的付出是有回报的,既没有相应的酬劳,又没有自己的时间,怨气只会越攒越多。我们现在能做什么:一、尽量不去996的公司,让996的公司无人可招;二、提高自己的技术水平,让自己拥有议价的主导权,非要实行996,能谈出你可以接受的薪酬。以上是我个人看法,不喜勿喷。(还是那句。。。钱给到位,住公司都行)

What is this?

What is this?这是什么?this是什么?(黑人问号脸)

今天的主题(??)是call、apply以及bind,这里这个以及我觉得用的很好,后面我会解释为什么不把bindcall、apply归为一类。

this对象是在运行时基于函数的执行环境绑定的(抛开箭头函数)

当函数被作为某个对象的方法调用时,this等于那个对象
this等于最后调用函数的对象

让我们来for example ⬇️

var name = 'Jack Sparrow';function sayWhoAmI() {  console.log(this.name)}sayWhoAmI(); // Jack Sparrowvar onePiece = {  name: 'Monkey·D·Luffy',  sayWhoAmI: function () {    console.log(this.name)  }};onePiece.sayWhoAmI(); // Monkey·D·Luffy复制代码

上面的代码我们可以看出,不管定义在哪的sayWhoAmI()方法,函数体是一样的,onePiece.sayWhoAmI()根据上面说的可以理解:

∵(因为,下同)调用方法的最后那个对象就是onePiece
∴(所以,下同)thisonePiecethis.name就是onePiece.name
但是为什么全局定义的sayWhoAmI方法输出的是Jack Sparrow,那我换种写法可能大家就明白了 ⬇️

var name = 'Jack Sparrow';function sayWhoAmI() {  console.log(this.name)}- sayWhoAmI(); // Jack Sparrow+ window.sayWhoAmI(); // Jack Sparrow复制代码

这样是不是清晰明了了

∵ 在全局声明的变量或者函数,都是在window或者globle这个对象里的
∴ 在window全局下声明的sayWhoAmI可以输出同是window全局下声明的name

小进阶

简单的我们已经明白了,现在我们来看看加入return的方法,我觉得算是有点难度的了,大佬请飘过 ⬇️

var area = 'East Ocean';var onePiece = {  area: 'New World',  tellMeWhereAreYou: function () {    return function () {      console.log(this.area);    }  }};onePiece.tellMeWhereAreYou()(); // East Ocean// 如果看不懂这里为什么执行两次,或者不明白为什么输出的全局变量// 那我引入一个中间变量,让过程多一步就能看懂了var grandLine = onePiece.tellMeWhereAreYou();// 这时候的 grandLine = function() { console.log(this.area); },等于onePiece.tellMeWhereAreYou();返回的函数// 因为grandLine是一个全局变量,所以this.area返回的是East OceangrandLine(); // East Ocean复制代码

上面我觉得用了言简意赅的方法解释了一下这个问题,因为这个涉及到闭包的知识,以及函数的活动对象,不明白的可以看我的另一篇博客,如果还不懂,还想更深入的了解可以自行翻阅《JavaScript高级程序设计》有关闭包的7.2章节,弄明白7.2章节中的两张图。

那么现在问题来了,我怎么才能让这个函数输出我对象内部的area: 'New World' ⬇️

var area = 'East Ocean';var onePiece = {  area: 'New World',  tellMeWhereAreYou: function () {    var that = this;    // 我们通过声明一个变量来保存this所指向的对象,然后再闭包中,就是返回的函数中使用    // 一个典型的闭包结构就完成了    return function () {      console.log(that.area);    }  }};onePiece.tellMeWhereAreYou()(); // New World复制代码

可能大家之前工作中会用到中间变量来保存this的这种方法,而且我感觉也不难,那我就跳过了。

我们现在应该大体搞明白了this指向的问题了。但是我们就是变态,我们有病,我们终于搞明白了this的指向问题,那我们现在又想改变this指向,?人生处处是艰难啊

这时候我们就需要用到标题中提到的callapply

Apply nothing and just call me

call()方法与apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。————————《JavaScript高级程序设计》

书里面说的很清楚,它们两个的作用是一样的,只是接收参数的方式不同,那到底有什么区别呢,听我我细细道来

疯狂打call

call()方法可以指定一个this的值(第一个参数),并且分别传入参数(第一个参数后面的就是需要传入函数的参数,需要一个一个传)

call()方法到底有什么用呢,自然是解决我们刚才提出来的改变this指向,怎么用呢???⬇️

var first = '大黑刀·夜',    second = '二代鬼彻',    third = '初代鬼彻',    fourth = '时雨';var zoro = {  first: '和道一文字',  second: '三代鬼彻',  third: '雪走',  fourth: '秋水'};function sayYourWeapon(num, num2) {  console.log(`这是我${num}得到的刀"${
this[num]}"`) console.log(`这是我${num2}得到的刀"${
this[num2]}"`)}sayYourWeapon('first', 'third'); // 这是我first得到的刀"大黑刀·夜";这是我third得到的刀"初代鬼彻"sayYourWeapon.call(zoro, 'first', 'fourth'); // 这是我first得到的刀"和道一文字";这是我fourth得到的刀"秋水"复制代码

上面这段代码很明显的改变了this的指向,如果我直接调用sayYourWeapon()必然输出的是全局全局变量firstthird的值,而我后面通过sayYourWeapon.call(zoro, 'first', 'fourth')中的call()方法

∵ 改变了函数中的this值,就是传入的zoro,把this值从全局对象改成了zoro对象
∴ 后面输出的也都是对象zoro中的'first', 'fourth'的值

apply所有配置

apply()方法可以指定一个this的值(第一个参数),并且传入参数数组(参数需要在一个数组或者类数组中)

我们应该已经是知道了call()方法怎么用了,那我们熟悉apply()就简单多了,我们可以把上面的例子改一下⬇️

var first = '大黑刀·夜',  second = '二代鬼彻',  third = '初代鬼彻',  fourth = '时雨';var zoro = {  first: '和道一文字',  second: '三代鬼彻',  third: '雪走',  fourth: '秋水'};function sayYourWeapon(num, num2) {  console.log(`这是我${num}得到的刀"${
this[num]}"`) console.log(`这是我${num2}得到的刀"${
this[num2]}"`)}sayYourWeapon('first', 'third'); // 这是我first得到的刀"大黑刀·夜";这是我third得到的刀"初代鬼彻"- sayYourWeapon.call(zoro, 'first', 'fourth'); // 这是我first得到的刀"和道一文字";这是我fourth得到的刀"秋水"+ sayYourWeapon.apply(zoro, ['first', 'fourth']); // 这是我first得到的刀"和道一文字";这是我fourth得到的刀"秋水"复制代码

可以看到,我全篇就只是把call改成了apply,并且把之前'first', 'fourth'这么传进去的参数改成了['first', 'fourth']一个数组。如果我们是在一个函数当中使用,那我们还可以直接使用arguments这个类数组对象⬇️

var first = '大黑刀·夜',    second = '二代鬼彻',    third = '初代鬼彻',    fourth = '时雨';  var zoro = {    first: '和道一文字',    second: '三代鬼彻',    third: '雪走',    fourth: '秋水'  };  function sayYourWeapon(num, num2) {    console.log(`这是我${num}得到的刀"${
this[num]}"`) console.log(`这是我${num2}得到的刀"${
this[num2]}"`) } function mySayYourWeapon(num, num2) { sayYourWeapon.apply(zoro, arguments) // 我们自己声明一个函数,并且在里面调用apply,这是我们只需要传入arguments这个参数,而不需要想call那样一个一个传进去了 } sayYourWeapon('first', 'fourth'); // 这是我first得到的刀"大黑刀·夜";这是我fourth得到的刀"时雨" mySayYourWeapon('first', 'fourth'); // 这是我first得到的刀"和道一文字";这是我fourth得到的刀"秋水"复制代码

羁bind秘密

文章开头我说过这样一句话⬇️

call、apply以及bind,这里这个以及我觉得用的很好

现在我们就来聊聊这个‘以及’的内涵

我为什么说‘以及’呢,因为bindcall、apply这两个方法的使用有一丢丢的不一样。上面我们一个函数调用.call()或者.apply()方法,方法会立即执行,如果函数有返回值会获得返回值,但是bind不一样
bind()方法不会立即执行目标函数,而是返回一个原函数的拷贝,并且拥有指定this值和初始函数(为什么是指定的,当然是我们自己传进去的啦)

什么叫原函数的拷贝呢,那我们先来看一下⬇️

function a() {}console.log(typeof a.bind() === 'function'); // 返回是true,先证明a.bind()是一个函数console.log(a.bind()); // 输出function a() {},跟原函数一样console.log(a.bind() == a); // falseconsole.log(a.bind() === a); // false 不管是 === 还是 == 都是false,证明是拷贝出来一份而不是原先的那个函数复制代码

上面解释了‘原函数的拷贝’这个问题,那接下来我们看看bind()怎么使用

结印准备

bind()方法在传参上跟call是一样的,第一个参数是需要绑定的对象,后面一次传入函数需要的参数,如下⬇️

var name = 'Jack Sparrow';var onePiece = {  name: 'Monkey·D·Luffy'};function sayWhoAmI() {  console.log(this.name)}var mySayWhoAmI = sayWhoAmI.bind(onePiece)sayWhoAmI(); // Jack SparrowmySayWhoAmI(); // Monkey·D·Luffy复制代码

一个简单的实现,本来输出的是全局变量'Jack Sparrow',后来经过bind以后绑定上了对象onePiece,所以输出的就是对象onePiece中的nodeMonkey·D·Luffy。

那我们需要传参的时候怎么办 ⬇️

var first = '大黑刀·夜',  second = '二代鬼彻',  third = '初代鬼彻',  fourth = '时雨';var zoro = {  first: '和道一文字',  second: '三代鬼彻',  third: '雪走',  fourth: '秋水'};function sayYourWeapon(num, num2) {  console.log(`这是我${num}得到的刀"${
this[num]}"`) console.log(`这是我${num2}得到的刀"${
this[num2]}"`)}// 既然我们知道bind是返回一个函数,那我们声明一个变量来接这个函数会看的直观一些var mySayYourWeapon = sayYourWeapon.bind(zoro, 'first', 'fourth'); // 传入初始参数var hisSayYourWeapon = sayYourWeapon.bind(zoro); // 只传入目标对象sayYourWeapon('first', 'third');mySayYourWeapon(); // 因为我们当时bind绑定函数的时候已经传入了目标对象zoro和指定的参数,所以这里就不需要传参数了hisSayYourWeapon( 'first', 'fourth'); // 当然我们开始bind绑定函数的时候不传入,在调用的时候再传入参数也是可以的复制代码

上面的代码我们可以发现mySayYourWeaponhisSayYourWeaponbind的时候一个传入了初始的参数,一个没有传入,但是后续调用的时候可以再传

既然是初始化参数,那我们就可以预设参数一个,然后再传一个——————偏函数(不知道自己理解的对不对,但是肯定是有这么个功能,不懂的可以移步)

印结完了,该出招了

影子模仿术

默认大家到这里已经知道怎么使用bind了,那我们接下来需要挑战的就是,自己手写一个bind方法,这个可以帮助我们更清楚的理解bind方法是怎么运作的,并且面试的时候也可能会被问到哦~

下面我们来看从中复制过来的实现,添加了我自己的理解和注释,希望大家能看懂⬇️

// 判断当前环境的Function对象的原型上有没有bind这个方法,如果没有,那我们就自己添加一个if (!Function.prototype.bind) {  /**   * 添加bind方法   * @param oThis 目标对象   * @returns {function(): *} 返回的拷贝函数   */  Function.prototype.bind = function(oThis) {    if (typeof this !== 'function') {      // closest thing possible to the ECMAScript 5      // 最接近ECMAScript 5的实现(貌似是这个意思)      // internal IsCallable function      // 内部IsCallable函数(?什么鬼)      // 如果当前this对象不是function,就抛出错误,因为只有function才需要实现bind这个方法。。。毕竟是返回函数      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');    }    // 声明变量aArgs保存arguments中除了第一个参数的其他参数的数组,因为第一个参数不是函数需要的参数,而是需要绑定的目标对象    // 这块就用到了call的方法,因为arguments是类数组对象,没有slice这个方法,所以只能从Array那call过来一个使用    var aArgs = Array.prototype.slice.call(arguments, 1);    // 保存原先的this对象,是在调用bind的时候没有传入目标对象,那就使用原先的this对象    var fToBind = this;    // 声明空函数,在下面的原型中可以使用    var fNOP = function() {};    // 需要放回的拷贝函数的本体,从最后的return也知道,最后是返回的fBound这个方法    var fBound  = function() {        // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用        // 下面就涉及到刚才说的是bind时初始化参数,还是bind以后调用的时候再传入参数        return fToBind.apply(          // 判断原始this对象是不是fBound的实例,或者说this的原型链上有没有fBound          this instanceof fBound            // 如果有,就使用原始的this           ? this            // 如果没有,就使用现在的传入的this对象          : oThis,          // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的          // 这一步就是为了保障在bind时候没有传入参数的时候,调用时候传入的参数能使用上          aArgs.concat(Array.prototype.slice.call(arguments)));      };    // 维护原型关系    // 判断原始this对象上有没有prototype    if (this.prototype) {      // Function.prototype doesn't have a prototype property      // 如果原始this对象上有prototype 就把fNOP的prototype改成this.prototype,fNOP就继承自原始this了      fNOP.prototype = this.prototype;    }    // 下行的代码使fBound.prototype是fNOP的实例,因此    // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例    // 既然fNOP是继承自原始this对象的,那这里的这一步就是让拷贝函数也拥有原始this对象的prototype,继承自同一个地方,师出同门    fBound.prototype = new fNOP();    // 最后返回被拷贝出来的函数    return fBound;  };}复制代码

上面的代码中有我添加的注释,方便大家能更好的理解,理解了上面的代码以后,bind方法算是了解的差不多了,其他实现原理上摸清楚了

可能上面的代码注释有点多,看着很费劲,下面贴出没有注释的代码,方便大家复制粘贴调试

if (!Function.prototype.bind) {  Function.prototype.bind = function(oThis) {    if (typeof this !== 'function') {      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');    }    var aArgs = Array.prototype.slice.call(arguments, 1);    var fToBind = this;    var fNOP = function() {};    var fBound  = function() {        return fToBind.apply(this instanceof fBound ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));    };    if (this.prototype) {      fNOP.prototype = this.prototype;    }    fBound.prototype = new fNOP();    return fBound;  };}复制代码

这么看来代码还不算很多就实现了bind方法

人的梦想,是不会完结的,没错吧?

可能 起不到本质上的作用,但是让我们知道有一群可爱的人跟我们一样在为生计奔波劳累着,让我们知道我们的圈子不小,只是没到团结的时候,敢折腾就不赖,人一定要梦想,趁着年轻,万一实现了呢。

带病写博客。。。

年轻嘛,就是干!

ps:博客可以技术分享,也当记录生活了,以后看见的话,没准会说“当时是不是傻”,但是现在感觉perfect


我是前端战五渣,一个前端界的小学生。

转载于:https://juejin.im/post/5c9ca4dd6fb9a070c023092e

你可能感兴趣的文章
jenkins权限管理,实现不同用户组显示对应视图views中不同的jobs
查看>>
我的友情链接
查看>>
CentOS定时同步系统时间
查看>>
批量删除用户--Shell脚本
查看>>
如何辨别android开发包的安全性
查看>>
Eclipse Java @Override 报错
查看>>
知道双字节码, 如何获取汉字 - 回复 "pinezhou" 的问题
查看>>
linux中cacti和nagios整合
查看>>
Parallels Desktop12推出 新增Parallels Toolbox
查看>>
Python高效编程技巧
查看>>
Kafka服务端脚本详解(1)一topics
查看>>
js中var self=this的解释
查看>>
js--字符串reverse
查看>>
面试题
查看>>
Facebook 接入之获取各个配置参数
查看>>
linux的日志服务器关于屏蔽一些关键字的方法
查看>>
事情的两面性
查看>>
只要会营销,shi都能卖出去?
查看>>
sed单行处理命令奇偶行输出
查看>>
走向DBA[MSSQL篇] 从SQL语句的角度 提高数据库的访问性能
查看>>