【愣锤笔记】一篇小短文让你完全搞懂this、call、apply和bind

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

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

关于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。bash

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

咱们先看下运行结果:babel


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

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方法搞清楚了。由此还引伸出更多的函数节流/去抖/柯里化/反柯里化,仍是能够继续深刻深究一下的。

相关文章
相关标签/搜索