跟我左手右手一起慢动作,右手左手慢动作重复。额~貌似哪里有点不对劲哎?让我想想,右手?左手?慢动作??重复???重播???不对不对,是左手call,右手apply,一起来bind this。

额,这都能强扯一波,好吧,让我吐血一波~~~说起了js中的this,确实是个有趣的话题,也是很多小伙伴一开始傻傻分不清的老命题了。算是老梗重提,再来聊聊this吧。

关于this,首先要提的一点就是,它指向一个对象,具体指向谁是由函数运行时所处的上下文决定的。这是最重要的一个概念,也是理解js中this的关键。所谓的上下文,可以理解为函数运行时的环境,例如一个函数在全局中运行,那么它的上下文就是这个全局对象,客户端中这个global对象就是window;函数作为对象的方法运行,那么它的上下文就是该对象。

关于this的指向问题,我们可以大致分为如下几种情景来讨论:

  • 函数作为普通函数调用
  • ES5严格模式下的函数调用
  • 函数作为对象的一个方法调用
  • 构造器中的this(也就是常说的类中的this,但是要搞清楚js是没有类的,是基于原型委托实现的继承,类只是大家习惯性的叫法)

(1)函数作为普通函数调用:大家学习js,对函数应该是再熟悉不过了。函数可是js中的一等公民,人中吕布、马中赤兔啊。

var name1 = 'hello this';
window.name2 = 'hello global';
function func () {console.log(this.name1); // 输出:"hello this"console.log(this.name2); // 输出:"hello global"
}
func();复制代码

这里的代码大家自然一眼就知道结果了,结果写在了上面的注释里。通过运行结果我们知道,普通函数在全局调用中,this指向全局对象。这里我们定义了一个全局变量name1,和一个window的属性name2,所以this.name1和this.name2如我们的预期指向了这两个值。值得一提的是:定义的全局变量,是被作为全局对象window的属性存在的哦。此时我们打印看下window对象,看图:

(2)ES5严格模式下的函数调用:this不再指向全局对象,而是undefined。

function strictFunc () {'use strict'console.log(this)console.log(this.name)
}
strictFunc()复制代码

我们先看下运行结果:

可以看到,this打印出来的值是undefined,而this.name会直接报错。由此说明,严格模式下,this已经不再指向全局对象,而是undefined值。引用undefinednull值的属性会报Uncaught TypeError错,这点我们在日常开发中需要注意一下,以免因为一个错误导致后面的程序直接挂掉(这是js单线程的原因,一旦程序出错,后面便不会再执行)。特别是我们在拿到一些不是我们决定的数据(例如后台返回的)进行处理的时候,使用对象的属性时最好判断一下,这样在极端情况下,也可以保证我们的程序继续跑下去,而不至于直接挂掉:

obj && obj.name
// 而不是直接取值:
obj.name或者用try/catch捕获错误:
try {const { data } = await api.getArticleList()
} catch {} finally {}
复制代码

(3)函数作为对象的方法使用:this指向该对象

var obj = {name: 'xiaoming',getInfo () {return '姓名: ' + this.name;}
}
console.log(obj.getInfo()); // 姓名: xiaoming 复制代码

当对象当属性的值是一个函数时,我们会称这个函数是这个对象的一个方法。该方法中的this在运行时指向的是该对象。上面的例子的输出结果也看的清清楚楚,然鹅,没错,就是鹅,现实有时候是会啪啪打脸的,打的响亮亮的、轻脆脆的、绿油油的~哎,我为什么要说绿油油,毛病。下面我简单改写一个上面的代码:

// 还是这个obj,还是熟悉的味道
var obj = {name: 'xiaoming',getInfo () {return '姓名: ' + this.name;}
}
// 定义一个引入obj.getInfo的变量
var referenceGetInfo = obj.getInfo;
console.log(referenceGetInfo()); // 输出:姓名:复制代码

最终我们没有拿到预期的name值,打脸了吧,说好了的指向该对象的呢!果然我们男人都是骗子,都是大猪蹄子!

这是为什么呢?我们知道js分为两种数据类型:基本数据类型,如string、number、undefined、null等,引用类型,如object。而像数组、函数等,本质都是对象,所以都是引用类型。函数名只不过是指向该函数在内存中位置的一个引用。所以,这里var referenceGetInfo = obj.getInfo在赋值之后,referenceGetInfo也只是该函数的一个引用。在看referenceGetInfo 的调用位置,是在全局中,所以是作为普通函数调用的。由此this指向window,所以没有值。可以在getInfo函数中,增加如下验证,结果必然是true

console.log(this === window)复制代码

(4)构造器函数中的this:指向该构造器返回的新对象

说起构造器函数,可能感觉会有些生硬,其实就是我们常说的定义类时的那个函数。例如,下面这个最常见的一个类(构造器函数):

// 定义Person类
var Person = function (name, sex) {this.name = name;this.sex = sex;
}
// 定义Person类的原型对象
Person.prototype = {constructor: Person,getName: function () {return '我叫:' + this.name;},getSex: function () {return '性别:' + this.sex;}
}
// 实例化一个p1
var p1 = new Person('愣锤', '男');
// 调用p1的方法
console.log(p1.getName()); // 我叫:愣锤
console.log(p1.getSex()); // 性别:男复制代码

构造器函数本是也是一个函数,如果直接调用该函数,那它和普通函数没什么区别。但是通过new调用之后,那它就成为了构造器函数。构造器函数在实例化时会返回一个新创建的对象,并将this指向该对象。所以this.name的值是"愣锤"。另外这里再提一点,如果你担心用户使用类时忘记加new,可以通过如下方式,强制使用new调用:

var Person = function (name, sex) {// 在构造器中增加如下这一行,其余不变if (!(this instanceof Person)) return new Person(name, sex);this.name = name;this.sex = sex;
}复制代码

该行代码判断了当前的this是否是Person类的实例,如果不是则强制返回一个通过new初始化的类。以为如果用户忘记使用new初始化类,那么此时的构造器函数是作为普通函数调用的,this在非严格模式下指向window,肯定不会是Person类的实例,所以我们直接强制返回new初始化。这也是我们在开发类库时可以使用的一个小技巧。

弄明白了js中的this的指向,下面我们再聊聊如何改变this的指向。在js中,改变this指向方法,常见的有如下几种:

  • Function.prototype.call()
  • Function.prototype.apply()
  • Function.prororype.bind()
  • 除此之外,还有eval()、with()等

(1)call()方法和apply()方法都是ES3中就存在的方法,可以改变函数的this指向,两者的功能完全一样,所以这里放在一起说。唯一的区别是两者调用时传入的参数不同,后面会仔细介绍。

// 还是熟悉的味道,还是那个obj
var obj = {name: 'xiaoming',getInfo (sex) {return '姓名: ' + this.name + '性别:' + this.sex || '未知';}
}
// 定义另一个obj对象
var otherObj = {name: '狗子你变了,你再也不是我认识的那个二狗了!'
}console.log(obj.getInfo.call(otherObj, '女'));
// 姓名: 狗子你变了,你再也不是我认识的那个二狗了!性别:女复制代码

我们通过callobj.getInfo方法放在ohterObj这个对象执行,输出了ohterObj.name的值,由此验证了call可以函数this的指向。call()方法接收多个参数:

  • 第一个参数为可选参数,即this指向的新的上下文对象。如果不传该参数,则指向全局对象。若不传入第一个参数且该方法(getInfo)使用严格模式,this值且undefined,和普通函数的严格模式一样,从undefined上取值会报错。
  • 后面的所有参数都是作为参数传递给方法调用

apply()方法和call的功能一样,只不过传入的参数不一样:

  • 第一个参数为可选参数,和上面?call的一样
  • 第二个参数是一个参数数组/类数组,数组包含的所有参数都会作为参数传递给该方法调用

用法很简单,和call一样就不多介绍了。但是这里提到了类数组概念,说一下什么是类数组,可以理解为本身不是数组,但是却可以像数组一样拥有length属性(例如函数的arguments对象)。我们没有确切的办法判断一个对象是不是类数组,所以这里我们只能使用js中的鸭子类型来判断。何为鸭子类型:如果它走起路来像鸭子,叫声也像鸭子,我们便认为它就是鸭子。

鸭子类型是js中很重要的一个概念,因为我们此时并不真正关心它是不是鸭子,我们只是想听到鸭子叫/或者看到鸭子走,即我们要的只是它拥有鸭子的行为,至于它是不是鸭子,无所谓呀!!!

所以只要一个对象能拥有数组的行为,我们就可以把它作为数组使用。下面引入underscore中的类数组判断方法说明:

var isArrayLike = function(collection) {
var length = getLength(collection);return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};复制代码

underscore.js中对类数组的判断其实也是运用了鸭子类型的思想,即判断如果该对象拥有length属性且是number类型,并且length的值大于等于0小于等于一个数组的最大元素个数,那我们就认定他是数组。

好了,有的稍微扯远了。

下面继续apply的实际运用场景,例如柯里化函数:

// 定义一个柯里化函数
var currying = function () {var arrPro = Array.prototype,fn = arrPro.shift.call(arguments),args = arrPro.slice.call(arguments);return function () {var _args = arrPro.slice.call(arguments);return fn.apply(fn, args.concat(_args));}
}
// 定义一个返回a+b的函数var add = function (a, b) {return a + b;
}
// 将这个求和函数进行柯里化,使其第一项的值恒为5
var curryAdd = currying(add, 5);
var res = curryAdd(4);
console.log(res); // 9复制代码

我们在开发中apply方法和call方法是用的比较多的,例如这里柯里化函数。特别是高阶函数中,函数作为值返回的时候,会经常使用apply这些方法来绑定函数运行时的上下文对象。

我们再看一个更常见的函数节流吧:

// 去抖函数
function debounce (fn, delay) {var timer;return function () {var args = arguments;var _this = this;timer && clearTimeout(timer);timer = setTimeout(function () {fn.apply(_this, args);}, delay);}
}
// 调用,在浏览器窗口滚动的情况下,debounce里的函数并不会被频繁触发,而是滚动结束500ms后触发
window.addEventListener('scroll', debounce(function () {console.log('window scroll');
}, 500), false);复制代码

我们在这个去抖函数里,在返回的函数里,使用里定时器,而定时器的第一个参数是一个函数,所以形成里一个局部的函数作用域。为了能保证我们的fn函数中的this的正确指向,我们通过apply改变它的指向。

所谓去抖函数,在一个函数被频繁调用的时候,如果此次调用距离上一次的时间小于我们定下的delay 值,那么取消本次调用。主要用来防止频繁触发的问题,从而提供程序运行性能。注意,上面只是一个函数去抖,真正在提升滚动性能的时候,我们更多的是会将去抖和节流结合起了使用。此处更多地在于演示apply的运用场景,不再多做节流去抖方面的说明。

call方法在v8的实现中,其实是作为apply方法的语法糖,由此,我们可以试着使用apply来模拟一个call方法(并非v8源码实现):

Function.prototype.call = function () {var ctx = Array.prototype.shift.apply(arguments);return this.apply(ctx, arguments);
}复制代码

我们知道call方法,第一个参数是上下文对象,所以我们的第一件事就是取出参数中的第一个参数ctx,然后把剩余的参数使用apply的方式调用。so,就是这样。

(2)说完了call和apply,下面我们再说一下ES5引入的新方法:Function.prototype.bind

该方法返回一个新的函数,并将该函数的this绑定到指定的上下文环境。接收多个参数:

  • 第一个参数为this绑定到的新上下文环境
  • 后面的参数会作为参数传递给该函数

用法很简单,相信大家都会用:

// 还是那个熟悉的狗子,哦不对,还是那个熟悉的对象
var obj = {name: 'xiaoming',getInfo (sex, hobby) {return '姓名: ' + this.name + ', 性别:' + (sex || '未知') + hobby;}
}
// 另外一个狗子,呸呸呸!另外一个对象
var obj2 = {name: '我已经不是你认识的狗子了'
}
// 输出:姓名: 我已经不是你认识的狗子了, 性别:男, 兴趣:打球
var newGetInfo = obj.getInfo.bind(obj2, '男');console.log(newGetInfo('打球'));复制代码

可以看到,bind()后返回了一个新函数,并把第一个参数后面的参数传递给了obj.getInfo方法,在运行newGetInfo('打球')时,又继续把参数传递给了obj.getInfo方法。是不是发现它天然支持了函数柯里化,是不是感觉跟我们上面的柯里化函数功能一样?

但是bind方法,是es5引入的,在es3是不支持的。这时候可能会说了,es5已经是主流了,大家也都已经大量使用es6及更高的语法,反正又babel等工具帮我们转换成es5的。没错,但是我们还是要了解其实现的,比如写一个bind方法的profill。做到知其然,知其所以然。

// 如果本身支持bind方法,则使用原生的bind方法,否则我们就实现一个使用
Function.prototype.bind = Function.prototype.bind || function () {var fn = this;  var ctx = arguments[0];var args = Array.prototype.slice.call(arguments, 1);return function () {var _args = Array.prototype.slice.call(arguments);return fn.apply(ctx, args.concat(_args));}
}复制代码

讲到这,相信已经可以将this/call/apply方法搞清楚了。由此还引申出更多的函数节流/去抖/柯里化/反柯里化,还是可以继续深入深究一下的。

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

【愣锤笔记】一篇小短文让你彻底搞懂this、call、apply和bind相关推荐

  1. 【愣锤笔记】基于vue的进阶散点干货

    vue的开发如日中天,越来越多的开发者投入了vue开发大军中.希望本文中的一些vue散点,能在实际项目中灵活运用,切实地为我们解决一些难点问题. 插槽 // 组件调用时使用插槽 <todo-li ...

  2. java中对递归的限制是什么_什么是递归,通过这篇文章,让你彻底搞懂递归

    美丽开始于你决定做自己的那一刻. 啥叫递归 tips:文章有点长,可以慢慢看,如果来不及看,也可以先收藏以后有时间在看. 聊递归之前先看一下什么叫递归. 递归,就是在运行的过程中调用自己. 构成递归需 ...

  3. 一篇小短文,带你了解屏幕刷新背后的故事

    /   今日科技快讯   / 近日,国际半导体产业协会致信美国商务部,呼吁重审特朗普政府去年针对中国出台实施的一系列限制措施,并敦促即将上任的美国商务部长即使要限制技术出口,也需与盟友合作. /    ...

  4. ES6学习笔记(四):教你轻松搞懂ES6的新增语法

    文章目录 let const let.const.var的区别 解构赋值 数组解构 对象解构 箭头函数 剩余参数 总结 let ES6新增的用于声明变量的关键字 let声明的变量只在所处于的块级有效 ...

  5. 开发人员必学!这篇入门你必须了解!搞懂这些直接来阿里入职

    前言 Netty 是一款基于 Java 的网络编程框架,能为应用程序管理复杂的网络编程.多线程处理以及并发.Netty 隐藏了样板和底层代码,让业务逻辑保持分离,更加易于复用.使用 Netty 可以得 ...

  6. 什么是递归,通过这篇文章,让你彻底搞懂递归

    想了解更多数据结构以及算法题,可以关注微信公众号"数据结构和算法",每天一题为你精彩解答.也可以扫描下面的二维码关注 啥叫递归 聊递归之前先看一下什么叫递归. 递归,就是在运行的过 ...

  7. 【微信小程序】一文搞懂条件渲染、列表渲染以及wxss模板样式

  8. 一篇文章带你搞懂网络层(网际层)-- 地址篇

    网络层(Network Layer)是OSI模型中的第三层(TCP/IP模型中的网际层),提供路由和寻址的功能,使两终端系统能够互连且决定最佳路径,并具有一定的拥塞控制和流量控制的能力.相当于发送邮件 ...

  9. 小猪猪C++笔记基础篇(四)数组、指针、vector、迭代器

    小猪猪C++笔记基础篇(四) 关键词:数组,Vector. 一.数组与指针 数组相信大家学过C语言或者其他的语言都不陌生,简单的就是同一个变量类型的一组数据.例如:int a[10],意思就是从a开始 ...

最新文章

  1. Ubuntu 14.04 64bit上安装LNMP环境
  2. linux 错误 kernel panic not syncing vfs unable to mount root fs on unknown-block 0 0
  3. 算法与数据结构之二分查找
  4. php读文阻塞,php socket编程 读完成后写阻塞
  5. 【超级鼠标键盘锁】之远线程注入winlogon.exe进程屏蔽Ctrl+Alt+Del、Win+L
  6. go ssh 执行多个命令_Gox语言中通过SSH远程执行命令及上传下载文件-GX10
  7. Go 语言编译过程概述
  8. 虽然现在用APACHE COMMONS DBCP可以非常方便的建立数据库连接池,
  9. 读书随笔:The Book of Why——CHAPTER 2:From Buccaneers to Guinea Pigs: The Genesis of Causal Inference
  10. 学python用什么软件-初学 Python 需要安装哪些软件?
  11. 检查手机是否安装外置SD卡
  12. 解决看网课鼠标不能移开,视频不能加速
  13. kindle看pdf乱码_Kindle 中文书名 目录 乱码 解决办法
  14. 【JavaWeb】实现网页验证码
  15. Labview实现简单知乎日报客户端
  16. NPOI导出word,NPOI导出word表格,NPOI复制table表格 XWPFDocument中XWPFTable
  17. 欧拉函数φ(x)相关性质及计算
  18. [Error Code: 904, SQL State: 42000] ORA-00904 : 标识符无效
  19. GDP越高就越幸福吗?用Python分析《世界幸福指数报告》后我们发现…
  20. oracle数据库有触发器,Oracle数据库触发器(Triggers)

热门文章

  1. 量子计算机交叉学,第二届UTS量子计算机科学冬令营顺利闭幕
  2. NGS测序基础梳理01-文库构建(Library Preparation)
  3. java怎么完成输出语句
  4. Webots学习笔记(一)---初识软件+建立简单模型
  5. AC自动机(题目+模板)
  6. PPTP拨号过程分析
  7. 【A200】 TX1核心 JetPack4.6.2版本如何修改DTB文件测试全部SPI
  8. QIIME 2教程. 21进化树q2-phylogeny(2020.11)
  9. 字符串转换为数组的方法
  10. 两向量叉乘的计算公式_向量叉乘公式是什么啊