初学JS,你有没有被代码里的this搞得晕头转向?
有一说一,反正我晕了。
于是经过一番系统学习,终于算是理清了this这个鬼东西,希望看完这篇文章的你也能战胜this呀!
全局上下文中的this
在浏览器环境中,全局上下文中的this,就是window。
let a = 12;
console.log(this); // 控制台输出为 window
块级上下文中的this
块级上下文中没有自己的this,它的this是继承所在上级上下文的this。
let obj = {
fn(){
// fn函数中的this是obj
{
let a = 12;
console.log(this); // 输出obj。继承上级上下文,即函数fn的私有上下文中的this
}
}
}
obj.fn();
函数私有上下文中的this
私有上下文中的this千变万化,可以总结为以下五种情况:
事件绑定
给元素的某个事件行为绑定方法,事件触发时方法执行,此时方法中的this一般为元素本身。
DOM0事件绑定:
let body = document.body;
body.onclick = function(){
console.log(this); // body
}
DOM2事件绑定( 除IE6~8 ):
body.addEventListener('click',function(){
console.log(this); // body
})
那二般情况呢?
在DOM2事件绑定的IE6~8浏览器中,this指向window。
body.attachEvent('onclick',function(){
console.log(this); // window
})
普通方法执行
这里的普通方法包含:普通函数、自执行函数、对象成员访问调取方法执行…等。
只要看函数执行时,方法名前是否有“点”就是了。
如果有“点”——xxx.方法()中this就是xxx;如果没有“点”——this即为window(严格模式下为undefined)。
// 自执行函数
(function(){
console.log(this); // 非严格模式下输出window,严格模式下输出undefined
})();
let obj = {
fn: (function(){
console.log(this); // 输出window
return function(){};
})();
}
上面两个例子都是自执行函数执行,输出均为window。说明this的指向,与函数在哪定义和执行的无关。自执行函数中的this一般都为window(因为函数执行时前面没有“点”)
function func(){
console.log(this);
}
let obj = {
func:func
}
// 普通函数执行
func(); // this:window
// 成员访问方法执行
obj.func(); // this:obj
[].slice()
方法中this->当前空数组。
Array.prototype.slice()
方法中的this->Array.prototype。
涵盖以上两种情况的一道题:
// 点击body输出什么?
function func(){
console.log(this);
}
document.body.onclick = function(){
func();
}
答案:window
为什么不是绑定了方法的元素body呢?整个过程是这样的:
body被点击时,首先执行的是为body的click事件绑定的匿名函数(这个函数中的this是body,但这个this题目中并没有要求输出),在这个匿名函数内部执行了func函数——函数执行时前面没有“点”呀,所以输出window。
注意:“给body的click事件绑定的函数”与“后来执行的func函数”可不是一个函数哦。
构造函数执行
我们在之前的文章中也有提到过,自定义类创建时与普通函数有哪些区别,其中一条就是“让函数中的this指向新创建的实例”。
没错,构造函数体中的this指向的就是当前类的实例。因此,函数中出现的this.xxx = xxx
这样的代码就是给当前实例设置私有属性;换句话说,类中凡是不带this
的代码统统都与它创建的实例无关,充其量也就是这个函数私有上下文中的私有变量而已。
function Func(){
console.log(this);
}
Func.prototype.getNum = function getNum(){
console.log(this);
}
let f = new Func();
构造函数执行,构造函数体中的this指向当前类的实例——f
,因此输出的this为f
;那原型上getNum方法中的this呢?那就说不好了,上面的代码中getNum方法只是创建还没执行,至于最终输出的this是谁就取决于执行时啥情况了。
因此,原型上方法中的this不一定是实例,要看执行时前面有没有点( 即当作普通函数看待 )。
ES6中的箭头函数
箭头函数没有自己的this,它的this继承自上级上下文中的this——这一点和块级上下文类似。(悄悄说,箭头函数不光没有自己的this,还没有arguments实参集合呢)
let obj = {
func:function(){
console.log(this);
}
sum:()=>{
console.log(this);
}
}
obj.func(); // this:obj
obj.sum(); // this:window
obj.sum.call(obj); // this:window
① 执行obj.func()
,对象成员访问执行函数,“点”前是obj,因此this指向obj。
② 执行箭头函数sum,它没有自己的this,于是找到上级上下文(全局)中的this->window。
③ 试图强行把sum中的this通过call函数修改为obj:我都没有this!你强行修改有用嘛!没用!
遇到箭头函数中使用this的情况,不管代码是咋写的,直接去找它上级上下文中的this。
尽管不建议乱用箭头函数( 因为this的特殊原因 ),但不得不承认在某些需求下,使用箭头函数非常的方便!真香!
let obj = {
i:0,
func(){ // 注意哦,这不是箭头函数,而是ES6中普通函数的简写
setTimeout(function(){
this.i++;
console.log(obj); // {i:0,func:f}
},1000);
}
}
obj.func();
// 我就想问,1秒钟之后,obj中i的值是多少嘞?答:它还是0
在func函数内部,this指的是obj没错。而定时器中回调函数中的this可是window啊,所以自加一的操作是给window.i的,跟obj中的 i 一毛钱关系都么得。
那我想让这个++的操作作用在obj中的i,应该咋写?
一个经典的操作是这样的:
let obj = {
i:0,
func(){
let _this = this; // 这里的this是obj,我先用一个变量把它保存起来
setTimeout(function(){
_this.i++; // 这样就是让之前保存的obj中的i ++啦
console.log(obj); // {i:1,func:f}
},1000);
}
}
obj.func();
另一种方法:基于bind把函数中的this预先处理为obj
let obj = {
i:0,
func(){
setTimeout(function(){
this.i++;
console.log(obj); // {i:1,func:f}
}.bind(this),1000); // 这里强行让内部这个回调函数中的this指向外层的this(即obj)
}
}
obj.func();
duang~真香警告!
let obj = {
i:0,
func(){
setTimeout(()=>{
this.i++; // 箭头函数中没有自己的this,这里用的this是继承自外层的,也就是func函数的this->obj!
console.log(obj); // {i:0,func:f}
},1000);
}
}
obj.func();
call/apply/bind方法
这三兄弟可🐂🍺了!强制手动改变函数中的this指向,前三种情况使用这种方法后,都以手动改变的为主。简单粗暴!
emmm为啥只有前三种情况,难道还有漏网之鱼???
(箭头函数内心os:劳资都没有this,随你怎么改,都对我不起作用蛤蛤蛤蛤蛤蛤!)
这三个函数都在函数类的原型对象Function.prototype上,因此所有的函数都能基于原型链__proto__找到并调用这三个方法。
-
call方法用法:[function].call([context],params1,params2,…)
含义:执行call方法时,会将后面的参数传递给[function]执行,并把函数中的this修改为[context]
@ [function]: 希望改变自身this的那个函数
@ [context]: 希望把this变为它!
@ params1, params2, … : 传递给函数的参数第一个参数不传 / null / undefined时:
非严格模式——this:window
严格模式——传谁this就是谁,不传就是undefined -
apply方法用法:[function].call([context],[params1,params2,…])
与call方法相同,唯一区别在于传递参数的方式。apply方法不是将参数一项项传给函数,而是将所有参数保存在一个数组中。( 其实内部也会一项项传给function,但这就不需要我们操心啦 )let obj = { name:'OBJ' } function func(){ console.log(this,x,y); } func(); // 这就是一个普通函数执行,输出window undefined undefined obj.func(); // 这里有坑,提示:obj和func有半毛钱关系么? func.call(obj,10,20); // 这下就扯上关系了,强行把func中的this改为obj,输出obj 10 20 func.apply(obj,[10,20]); // 同上,输出obj 10 20
-
bind方法用法:[function].call([context],params1,params2,…)
bind的语法与call相同,作用与以上两个方法均不同。它是预先修改this,预先存储参数,而函数不被立即执行;也就是说,call / apply都是立即执行函数的。
// 需求:把func函数绑定给body的click事件,触发body的click事件时,func函数执行
// 要让func函数中的this,并给func函数传递参数10,20
// 方案一:
document.body.onclick = func.call(obj,10,20);
// 方案二:
document.body.onclick = function anonymous(){
func.call(obj,10,20);
}
// 方案三:
document.body.onclick = func.bind(obj,10,20);
依次剖析一下这三种方案:
- 方案一:不可取
func.call(obj,10,20)
会立即执行func函数(并修改this&传参),它哪管你什么时候点击body。这种写法实际上是将func.call(obj,10,20)
执行的结果作为值绑定给body点击事件。问题来了,call函数执行的返回值还是我们想要的么?No! - 方案二:可!
将一个匿名函数绑定给body的点击事件,也就是将这个匿名函数的地址绑定给了事件,并没有立即执行。当点击事件触发时,将匿名函数执行,然后执行func.call。事实上,在bind方法之前,大家就是这样处理的。已经和bind的原理非常类似了。 - 方案三:也可!
bind函数不会立即执行,而是预先修改this,并预先存储需要传递的参数。后续需要时(事件触发)再执行函数。
当然啦,如果你知道bind的内部机制是怎样的,理解起来就更加简单啦!
预知内部机制,且听下回分解~
感谢阅读!