跟我左手右手一块儿慢动做,右手左手慢动做重复。额~貌似哪里有点不对劲哎?让我想一想,右手?左手?慢动做??重复???重播???不对不对,是左手call,右手apply,一块儿来bind this。es6
额,这都能强扯一波,好吧,让我吐血一波~~~提及了js中的this,确实是个有趣的话题,也是不少小伙伴一开始傻傻分不清的老命题了。算是老梗重提,再来聊聊this吧。api
关于this,首先要提的一点就是,它指向一个对象,具体指向谁是由函数运行时所处的上下文决定的。这是最重要的一个概念,也是理解js中this的关键。所谓的上下文,能够理解为函数运行时的环境,例如一个函数在全局中运行,那么它的上下文就是这个全局对象,客户端中这个global对象就是window
;函数做为对象的方法运行,那么它的上下文就是该对象。数组
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
值。引用undefined
和null
值的属性会报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
初始化。这也是咱们在开发类库时可使用的一个小技巧。
(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, '女'));
// 姓名: 狗子你变了,你不再是我认识的那个二狗了!性别:女复制代码
咱们经过call
把obj.getInfo
方法放在ohterObj
这个对象执行,输出了ohterObj.name
的值,由此验证了call能够函数this的指向。call()方法接收多个参数:
apply()方法和call的功能同样,只不过传入的参数不同:
用法很简单,和call同样就很少介绍了。可是这里提到了类数组概念,说一下什么是类数组,能够理解为自己不是数组,可是却能够像数组同样拥有length属性(例如函数的arguments对象)。咱们没有确切的办法判断一个对象是否是类数组,因此这里咱们只能使用js中的鸭子类型来判断。何为鸭子类型:若是它走起路来像鸭子,叫声也像鸭子,咱们便认为它就是鸭子。
鸭子类型是js中很重要的一个概念,由于咱们此时并不真正关心它是否是鸭子,咱们只是想听到鸭子叫/或者看到鸭子走,即咱们要的只是它拥有鸭子的行为,至于它是否是鸭子,无所谓呀!!!
因此只要一个对象能拥有数组的行为,咱们就能够把它做为数组使用。下面引入underscore中的类数组判断方法说明:
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绑定到指定的上下文环境。接收多个参数:
用法很简单,相信你们都会用:
// 仍是那个熟悉的狗子,哦不对,仍是那个熟悉的对象
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方法搞清楚了。由此还引伸出更多的函数节流/去抖/柯里化/反柯里化,仍是能够继续深刻深究一下的。