JS中的this是一个陈词滥调的问题了,因为它并不是一个确定的值,在不同状况下有不同的指向,所以也常常使人困惑。本篇文章会谈谈我本人对this的了解。
this到底是啥
其实this就是一个指针,它批示的就是以后的一个执行环境,能够用来对以后执行环境进行一些操作。因为它批示的是执行环境,所以在定义这个变量时,其实是不晓得它真正的值的,只有运行时能力确定他的值。同样一段代码,用不同的形式执行,他的this指向可能是不一样的。咱们来看看如下代码:
function func() { this.name = "小小飞"; console.log(this); // 看一下this是啥}
这个办法很简略,只是给this增加了一个name属性,咱们把这个办法复制到Chrome调试工具看下后果:
上图中咱们间接调用了func()
,发现this指向的是window,name
属性增加到了window上。上面咱们换一种调用形式,咱们换成new func()
来调用:
咱们看到输入了两个func {name: "小小飞"}
,一个是咱们new返回的对象,另一个是办法外面的console。这两个值是一样的,阐明这时候办法外面this就指向了new返回的对象,而不是后面例子的window了。这是因为当你应用new
去调用一个办法时,这个办法其实就作为构造函数应用了,这时候的this指向的是new进去的对象。
上面咱们别离解说下几种状况
应用new调用时,this指向new进去的对象
这个规定其实是JS面向对象的一部分,JS应用了一种很波折的形式来反对面向对象。当你用new来执行一个函数时,这个函数就变成了一个类,new关键字会返回一个类的实例给你,这个函数会充当构造函数的角色。作为面向对象的构造函数,必须要有可能给实例初始化属性的能力,所以构造函数外面必须要有某种机制来操作生成的实例,这种机制就是this。让this指向生成的实例就能够通过this来操作实例了。对于JS的面向对象更具体的解释能够看这篇文章。
this的这种个性还有一些妙用。一个函数能够间接调用,也能够用new调用,那如果我只想使用者通过new调用有没有方法呢?下图截取自Vue源码:
Vue奇妙利用了this的个性,通过查看this是不是Vue的一个实例来检测使用者是通过new调用的还是间接调用的。
没有明确调用者时,this指向window
这个其实在最开始的例子就讲过了,那里没有明确调用者,this指向的是window。咱们这里讲另外一个例子,函数外面的函数,this指向谁?
function func() { function func2() { console.log('this:', this); // 这里的this指向谁? } func2();}
咱们执行一下看看:
间接执行:
应用new执行:
咱们发现无论是间接执行,还是应用new执行,this的值都指向的window。间接执行时很好了解,因为没有明确调用者,那this天然就是window。须要留神的是应用new时,只有被new的func
才是构造函数,他的this指向new进去的对象,他外面的函数的this还是指向window
。
有明确调用者时,this指向调用者
看这个例子:
var obj = { myName: "小小飞", func: function() { console.log(this.myName); }}obj.func(); // 小小飞
上述例子很好了解,因为调用者是obj,所以func外面的this就指向obj,this.myName
就是obj.myName
。其实这一条和上一条能够合在一起,没有明确调用者时其实隐含的调用者就是window,所以常常有人说this总是指向调用者。
上面咱们将这个例子略微改一下:
var myName = "大飞哥";var obj = { myName: "小小飞", func: function() { console.log(this.myName); }}var anotherFunc = obj.func;anotherFunc(); // 输入是啥?
这里的输入应该是“大飞哥”,因为尽管anotherFunc
的函数体跟obj.func
一样,然而他的执行环境不一样,他其实没有明确的调用者,或者说调用者是window。这里的this.myName
其实是window.myName
,也就是“大飞哥”。
咱们将这个例子再改一下:
let myName = "大飞哥";var obj = { myName: "小小飞", func: function() { console.log(this.myName); }}var anotherFunc = obj.func;anotherFunc(); // 留神这里输入是undefined
这次咱们只是将第一个var
改成了let
,然而咱们的输入却变成了undefined
。这是因为let,const定义变量,即便在最外层也不会变成window的属性,只有var定义的变量才会成为window的属性。
箭头函数并不会绑定this
这句话的意思是箭头函数自身并不具备this,箭头函数在被申明确定this,这时候他会间接将以后作用域的this作为本人的this。还是之前的例子咱们将函数改为箭头函数:
var myName = "大飞哥";var obj = { myName: "小小飞", func: () => { console.log(this.myName); }}var anotherFunc = obj.func;obj.func(); // 大飞哥anotherFunc(); // 大飞哥
上述代码外面的obj.func()
输入也是“大飞哥”,是因为obj
在创立时申明了箭头函数,这时候箭头函数会去寻找以后作用域,因为obj
是一个对象,并不是作用域,所以这里的作用域是window,this也就是window了。
再来看一个例子:
var myName = "大飞哥";var obj = { myName: "小小飞", func: function () { return { getName: () => { console.log(this.myName); } } }}var anotherFunc = obj.func().getName;obj.func().getName(); // 小小飞anotherFunc(); // 小小飞
两个输入都是“小小飞”,obj.func().getName()
输入“小小飞”很好了解,这里箭头函数是在obj.func()
的返回值里申明的,这时他的this其实就是func()
的this,因为他是被obj
调用的,所以this指向obj。
那为什么anotherFunc()
输入也是“小小飞”呢?这是因为anotherFunc()
输入的this,其实在anotherFunc
赋值时就确定了:
var anotherFunc = obj.func().getName;
其实是先执行了obj.func()
- 执行
obj.func()
的时候getName
箭头函数被申明 - 这时候箭头函数的this应该是以后作用域的this,也就是
func()
外面的this func()
因为是被obj
调用,所以this指向obj
- 调用
anotherFunc
时,其实this早就确定了,也就是obj
,最终输入的是obj.myName
。
再来看一个构造函数外面的箭头函数,后面咱们说了构造函数外面的函数,间接调用时,他的this指向window,然而如果这个函数时箭头函数呢:
var myName = "大飞哥";function func() { this.myName = "小小飞"; const getName = () => { console.log(this.myName); } getName();}new func(); // 输入啥?
这里输入的是“小小飞”,原理还是一样的,箭头函数在申明时this确定为以后作用域的this,在这里就是func
的作用域,跟func
的this一样指向new进去的实例。如果不必new,而是间接调用,这里的this就指向window。
DOM事件回调外面,this指向绑定事件的对象
function func(e) { console.log(this === e.currentTarget); // 总是true console.log(this === e.target); // 如果target等于currentTarget,这个就为true}const ele = document.getElementById('test');ele.addEventListener('click', func);
currentTarget
指的是绑定事件的DOM对象,target
指的是触发事件的对象。DOM事件回调外面this总是指向currentTarget
,如果触发事件的对象刚好是绑定事件的对象,即target === currentTarget
,this也会顺便指向target
。如果回调是箭头函数,this是箭头函数申明时作用域的this。
严格模式下this是undefined
function func() { "use strict" console.log(this);}func(); // 输入是undefined
留神这里说的严格模式下this是undefined是指在函数体外部,如果自身就在全局作用域,this还是指向window。
<html> ... <script> "use strict" console.log(this); // window </script> ...</html>
this能改吗
this是能改的,call
和apply
都能够批改this,ES6外面还新增了一个bind
函数。
应用call和apply批改this
const obj = { myName: "大飞哥", func: function(age, gender) { console.log(`我的名字是${this.myName}, 我的年龄是${age},我是一个${gender}`); }}const obj2 = { myName: "小小飞"}obj.func.call(obj2, 18, "帅哥"); // 我的名字是小小飞, 我的年龄是18,我是一个帅哥
留神下面输入的名字是"小小飞",也就是obj2.myName
。失常间接调用obj.func()
输入的名字应该是obj.myName
,也就是"大飞哥"。然而如果你应用call
来调用,call的第一个参数就是手动指定的this
。咱们将它指定为obj2
,那在函数外面的this.myName
其实就是obj2.myName
了。
apply
办法跟call
办法作用差不多,只是前面的函数参数模式不同,应用apply调用应该这样写,函数参数应该放到一个数组或者类数组外面:
obj.func.apply(obj2, [18, "帅哥"]); // 我的名字是小小飞, 我的年龄是18,我是一个帅哥
之所以有call和apply两个办法实现了差不多的性能,是为了让大家使用方便,如果你拿到的参数是一个一个的,那就应用call吧,然而有时候拿到的参数是arguments
,这是函数的一个内置变量,是一个类数组构造,示意以后函数的所有参数,那就能够间接用apply,而不必将它开展了。
应用bind批改this
bind
是ES5引入的一个办法,也能够批改this,然而调用它并不会立刻执行办法自身,而是会返回一个批改了this的新办法:
const obj = { myName: "大飞哥", func: function(age, gender) { console.log(`我的名字是${this.myName}, 我的年龄是${age},我是一个${gender}`); }}const obj2 = { myName: "小小飞"}const func2 = obj.func.bind(obj2); // 返回一个this改为obj2的新办法func2(18, "帅哥"); // 我的名字是小小飞, 我的年龄是18,我是一个帅哥
bind和call,apply最大的区别就是call,apply会立刻执行办法,而bind并不会立刻执行,而是会返回一个新办法供前面应用。
bind函数也能够接管多个参数,第二个及当前的参数会作为新函数的参数传递进去,比方后面的bind也能够这样写:
const func3 = obj.func.bind(obj2, 18); // 留神咱们这里曾经传了一个年龄参数func3("帅哥"); //留神这里只传了性别参数,年龄参数曾经在func3外面了,输入还是:我的名字是小小飞, 我的年龄是18,我是一个帅哥
本人写一个call
晓得了call的作用,咱们本人来写一个call:
Function.prototype.myCall = function(...args) { // 参数查看 if(typeof this !== "function") { throw new Error('Must call with a function'); } const realThis = args[0] || window; const realArgs = args.slice(1); const funcSymbol = Symbol('func'); realThis[funcSymbol] = this; // 这里的this是原办法,保留到传入的第一个参数上 //用传入的参数来调办法,办法外面的this就是传入的参数了 const res = realThis[funcSymbol](...realArgs); delete realThis[funcSymbol]; // 最初删掉长期存储的原办法 return res; // 将执行的返回值返回}
本人写一个apply
apply办法跟call办法很像,区别只是在取调用参数上:
Function.prototype.myApply = function(...args) { if(typeof this !== "function") { throw new Error('Must call with a function'); } const realThis = args[0] || window; // 间接取第二个参数,是一个数组 const realArgs = args[1]; const funcSymbol = Symbol('func'); realThis[funcSymbol] = this; const res = realThis[funcSymbol](...realArgs); delete realThis[funcSymbol]; return res; }
本人写一个bind
本人写一个bind须要用到后面的apply,留神他的返回值是一个办法
Function.prototype.myBind = function(...args) { if(typeof this !== "function") { throw new Error('Must call with a function'); } const _func = this; // 原办法 const realThis = args[0] || window; // 绑定的this const otherArgs = args.slice(1); // 取出前面的参数作为新函数的默认参数 return function(...args2) { // 返回一个办法 return _func.apply(realThis, [...otherArgs,...args2]); // 拼接存储参数和新参数,而后用apply执行 }}
总结
- 函数里面的this,即全局作用域的this指向window。
- 函数外面的this总是指向间接调用者。如果没有间接调用者,隐含的调用者是window。
- 应用new调用一个函数,这个函数即为构造函数。构造函数外面的this是和实例对象沟通的桥梁,他指向实例对象。
- 箭头函数外面的this在它申明时确定,跟他以后作用域的this一样。
- DOM事件回调外面,this指向绑定事件的对象(currentTarget),而不是触发事件的对象(target)。当然这两个能够是一样的。如果回调是箭头函数,请参考上一条,this是它申明时作用域的this。
- 严格模式下,函数外面的this指向undefined,函数里面(全局作用域)的this还是指向window。
- call和apply能够扭转this,这两个办法会立刻执行原办法,他们的区别是参数模式不一样。
- bind也能够批改this,然而他不会立刻执行,而是返回一个批改了this的函数。
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。
作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges