this是执行主体它与执行上下文有着本质的区别,this的指向可以分为三种大的情况,如下:
一、全局中的this指向
全局上下文中,this指向window。
console.log(this === window); //true
二、块级上下文中this的指向
块级上下文中,它没有自己的this,它的this是继承上下文中的this。
{
let a = 12;
console.log(this); //window
}
let obj = {
fn() {
{
let a = 12;
console.log(this); //fn中this是obj,所以这里的this继承fn的this,this指向为obj
}
},
};
obj.fn();
三、函数中this的指向
函数中this指向比较的复杂,分为以下几种情况:
1.事件绑定
给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的this是当前元素本身。但在ie6-8中基于attachEvent方法实现的事件绑定,事件触发,方法中的this指向window而不是元素本身。
let body = document.body;
body.onclick = function () {
// 事件触发,方法执行,方法中的this是body
console.log(this); //=> body
};
body.addEventListener('click', function () {
console.log(this); //=>body
});
IE6~8中的事件绑定
box.attachEvent('onclick', function () {
console.log(this); //=>window
});
2.普通方法执行
普通方法执行只需要看函数执行的时候方法名前面是否有“点”,有“点”,点前面是谁this就是谁,没有点在非严格模式下this指向window,严格模式下指向undefined。
//普通方法执行(包括自执行函数,普通函数执行,对象成员访问)
//自执行函数,也是看函数执行时前面是否有点,函数中的this和函数在哪里定义,在哪里执行无关
(function () {
console.log(this); //=>window
})();
let obj = {
fn: (function () {
console.log(this); //=>window
return function () {}
})() //把自执行函数执行的返回值赋值给obj.fn
};
//函数中this与它在哪里定义的,在哪里执行的无关
function func() {
// this => window
console.log(this);
}
document.body.onclick = function () {
// this => body
func();
};
3.构造函数
构造函数体中的this是当前类的实例.
function Func() {
this.name = "F";
console.log(this); //=>构造函数体中的this在“构造函数执行”的模式下,是当前类的一个实例,并且this.XXX=XXX是给当前实例设置的私有属性
}
Func.prototype.getNum = function getNum() {
// 原型上的方法中的this不一定都是实例,主要看执行的时候,“点”前面的内容
console.log(this);
};
let f = new Func;
f.getNum();
f.__proto__.getNum();
Func.prototype.getNum();
4.es6中的箭头函数
箭头函数(Arrow Function)没有自己的this,它的this是继承所在上下文中的this。
let obj = {
func: function () {
console.log(this);
},
sum: () => {
console.log(this);
}
};
obj.func(); //=>this:obj
obj.sum(); //=>this是所在上下文中的this:window
obj.sum.call(obj); //=>箭头函数是没有this,所以哪怕强制改也没用 this:window
所以不要随意的使用箭头函数,但箭头函数的使用部分时候还是很方便,节约了不少代码。在下面代码中,如果要实现i++的话,那就可以用到箭头函数
let obj = {
i: 0,
func() {
console.log(this); //this=>obj
setTimeout(function () {
//回调函数中的this一般指向的是window,特殊情况除外
this.i++;//this=>window
console.log(this);
},(1000))
}
}
obj.func();
用箭头函数,代码如下:
let obj = {
i: 0,
func() {
setTimeout(() => {
// 箭头函数中没有自己的this,用的this是上下文中的this,也就是obj
this.i++;
console.log(this);
}, 1000);
}
};
obj.func();
也可以用以下方案:
let obj = {
i: 0,
func() {
let _this = this;//利用_this进行this的接收,传递到回调函数中
setTimeout(function () {
_this.i++;
console.log(_this);
}, 1000);
}
};
obj.func();
let obj = {
i: 0,
func() {
setTimeout(function () {
// 基于bind把函数中的this处理成obj
this.i++;
console.log(this);
}.bind(this), 1000);
}
};
obj.func();
练习题:
var num = 10;
var obj = {
num: 20
};
obj.fn = (function (num) {
this.num = num * 3;
num++;
return function (n) {
this.num += n;
num++;
console.log(num);
}
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);
5.call/apply/bind方式
Function.prototype内部有call/bind/apply三种方法手动的改变函数中的this指向。
语法如下:
call: function.call(thisArg, arg1, arg2, ...)
function作为Function的一个实例,可以基于__proto__找到Function.prototype的call方法,并且把找到的call方法执行;
在call方法执行的时候,会把function执行,并且把函数中的this指向为thisArg,并且把arg1,arg2,...等参数值分别传递给函数。
apply:func.apply(thisArg, [argsArray]);
和call作用一样,只不过传递给函数的参数需要一数组的形式传递给apply。
bind:function.bind(thisArg[, arg1[, arg2[, ...]]])
语法上和call类似,但是作用和call/apply都不太一样;call/apply都是把当前函数立即执行,并且改变函数中的this指向的,而bind是一个预处理的思想,基于bind只是预先把函数中的this指向thisArg,把arg1这些参数值预先存储起来,但是此时函数并没有被执行。
在下面的代码中我们可以看到,call和apply的唯一区别在于传递参数的形式不一样,apply以精数组的式传递参数。
call方法的第一个参数,如果不传或者是传递的为null/undefined的话,在非严格模式下,this指向window,严格模式下指向传递的值。
let obj = {
name: 'obj'
};
function func(x, y) {
console.log(this, x, y);
}
func.call(obj, 11, 12);//obj 11 12
func.apply(obj, [11, 12]); //obj 11 12
func.call();//window undefined undefined
func.call(null);//window undefined undefined
func.call(undefined);//window undefined undefined
func.call(11);//Number undefined unefined
对于bind方法我们要注意的是它是一种预处理的操作,bind是一个预处理的思想,基于bind只是预先把函数中的this指向thisArg,把arg1这些参数值预先存储起来,但是此时函数并没有被执行。这时我们可以完成以下类似的需要:
把func函数绑定给body的click事件上,要求触发body的点击行为后,函数才执行,代码如下:
let body = document.body;
let obj = {
name: 'obj'
};
function func(x, y) {
console.log(this, x, y);
}
在利用bind的时候,我们可以如下操作:
body.onclick = func.bind(obj, 10, 20);
但bind不兼容ie6-8,把我们也可以利用匿名函数
body.onclick = function anonymous() {
func.call(obj, 10, 20);
};
bind的原理是执行bind方法,返回一个匿名函数,把返回的匿名函数赋值给事件绑定 ,在事件触发的时候,先执行匿名函数,在匿名函数中我们可以改变this指向或者传参完成需求.结合上面的例子可以得到如下代码:
/*
* 执行bind,返回一个匿名函数绑定事件或者是其它内容
*当事件触发的时候,首先执行的是匿名函数(此时匿名函数中的this和bind中的this无关
*解决方案:This = this
*/
Function.prototype.bind = function bind(thisArg = window, ...args) {
//this->func bind执行它里面的this是要处理的函数func
let This = this;
// 给当前元素的某个事件行为绑定方法,当事件触发,行为执行,浏览器会默认给方法传递事件对象(ev)
//在amonymous中可能会接收到一些参数信息
return function anonymous(...innerArgs) {
//在func中我们要改变this指向,传参,把func执行
//this -> body 匿名函数是在触发body的点击事件时才执行,所以this指向body
//传参的时候ev也要进行传递,所以要用到concat()方法
This.apply(thisArg, args.concat(innerArgs));
};
};
body.onclick = func.bind(obj, 10, 20);
从上面我们可以知道,bind方法运用了闭包的知识。bind形成了一个不被销毁的上下文,预先把需要执行的函数,改变的this及后续需要给函数传递的参数信息等都保存到不释放的上下文中,后续使用的时候直接调用,这就是经典的预先存储的思想,柯里化函数。
实际运用一:把类数组转成数组的方法
function func() {
//1.Array.form()方法
/*
let args = Array.from(arguments);
console.log(args);
*/
//2.es6中的展开运算符
/*
let args = [...arguments];
console.log(args);
*/
//3.手动循环
/*
let args = [];
for (let i = 0; i < arguments.length; i++){
args.push(arguments[i]);
}
*/
/* 4.借用数组原型上的方法操作类数组
*ARGUMENTS具备和数组类似的结构,所以操作数组的一些代码(如:循环)也同样适用于arguments
*我们可以让Array原型上的内置方法执行,并且让方法中的this变成我们要操作的类数组=?“借用数组原型上的方法操作类数组”
*让类数组也和数组一样可以调用这些方法实现具体的需求
*/
/*
let args = Array.prototype.slice.call(arguments);
//简化
let args = [].slice.call(arguments);
console.log(args);
*/
}
func(10, 20, 30);
从上面我们可以看到,在js中可以从其他对象借用方法来构建某些功能,而不必继承它们的所有属性和方法。像上面的代码中,arguments并不是一个真正的数组,不能用数组原型上的方法,但是可以让数组原型上的方法执行,让方法中的this(一般是需要处理的实例)变为实例,这样就相当于实例在借用这个方法实现具体的功能,这种借用规则,利用的就是call改变this指向实现的。
实际运用二:求数组中的最大值
let arr = [12, 34, 29, 49, 23, 4];
// 1.排序
/*
let max = arr.sort((a, b) => b - a)[0];
console.log(max);
*/
// 2.循环
/*
let max = arr[0];
arr.forEach((item) => {
if (item > max) {
max = item;
}
});
*/
//3.Math.max(n1,n2,......);
/*
//用展开运算符
// let max = Math.max(...arr);
//用apply
let max = Math.max.apply(Math, arr);
console.log(max);
*/
call()方法的源码分析:call()方法中综合的应用了各种知识,主要涉及到成员访问
/*
*原理:
* 1.给thisArg设置一个属性,属性值一定是我们要执行的函数即this
* 2.按下来基于thisArg.xxx()成员访问执行方法,就可以把函数执行且改变里面的this
* 3.都处理完后,把给thisArg设置的属性删除掉
*/
Function.prototype.call = function call(thisArg, ...args) {
//非严格模式下,不传,或者是传递的为null&undefined的话,this指向window
thisArg == undefined ? thisArg = window : null;
//thisArg不能是基本数据类型值,如果传递是值类型我们需要将其变为对应的对象类型
if (!/^(object|function)$/.test(typeof thisArg)) {
thisArg = /^(symbol|bigint)$/.test(typeof thisArg) ? Object(thisArg) : new thisArg.constructor(thisArg);
}
/* 可以把上面的三元运算符转换成下面的if语句
let type = typeof thisArg;
if (!/^(object|function)$/.test(type)) {
if (/^(symbol|bigint)$/.test(type)) {
thisArg = Object(thisArg);
} else {
thisArg = new thisArg.constructor(thisArg);
}
}
*/
//在给thisArg设置属性的时候,需要注意属性名尽可能保持唯一性,避免它修改默认对象中的结构,所以用到了Symbol()
let attr = Symbol('Attr'),
result;
//1.第一步:给thisArg设置this的属性
thisArg[attr] = this;
//2.执行方法,改变this指向并且传参
result = thisArg[attr](...args);
//3.删除添加的属性
delete thisArg[attr];
return result;
}
下面我们从一道题目来加深对call原理的理解。在下面代码中,我们重点要关注的是B.call.call.call(A,20,10)。B.call.call找到的也是Function.prototype上的call。
var name = "davina";
function A(x, y) {
var res = x + y;
console.log(res, this.name);
}
function B(x, y) {
var res = x - y;
console.log(res, this.name);
}
B.call(A, 40, 30);
B.call.call.call(A, 20, 10);
Function.prototype.call(A, 60, 50);
Function.prototype.call.call.call(A, 80, 70);
总结:this的指向可以归纳为以下图片