this到底指向啥?看完这篇就知道了!

JS中的this是一个老生常谈的问题了,由于它并非一个肯定的值,在不一样状况下有不一样的指向,因此也常常令人困惑。本篇文章会谈谈我本身对this的理解。javascript

this究竟是啥

其实this就是一个指针,它指示的就是当前的一个执行环境,能够用来对当前执行环境进行一些操做。由于它指示的是执行环境,因此在定义这个变量时,实际上是不知道它真正的值的,只有运行时才能肯定他的值。一样一段代码,用不一样的方式执行,他的this指向多是不同的。咱们来看看以下代码:html

function func() {
  this.name = "小小飞";
  
  console.log(this);    // 看一下this是啥
}

这个方法很简单,只是给this添加了一个name属性,咱们把这个方法复制到Chrome调试工具看下结果:java

image-20200226153116734

上图中咱们直接调用了func(),发现this指向的是window,name属性添加到了window上。下面咱们换一种调用方式,咱们换成new func()来调用:git

image-20200226153812137

咱们看到输出了两个func {name: "小小飞"},一个是咱们new返回的对象,另外一个是方法里面的console。这两个值是同样的,说明这时候方法里面this就指向了new返回的对象,而不是前面例子的window了。这是由于当你使用new去调用一个方法时,这个方法其实就做为构造函数使用了,这时候的this指向的是new出来的对象。github

下面咱们分别讲解下几种状况segmentfault

使用new调用时,this指向new出来的对象

这个规则实际上是JS面向对象的一部分,JS使用了一种很曲折的方式来支持面向对象。当你用new来执行一个函数时,这个函数就变成了一个类,new关键字会返回一个类的实例给你,这个函数会充当构造函数的角色。做为面向对象的构造函数,必需要有可以给实例初始化属性的能力,因此构造函数里面必需要有某种机制来操做生成的实例,这种机制就是this。让this指向生成的实例就能够经过this来操做实例了。关于JS的面向对象更详细的解释能够看这篇文章。数组

this的这种特性还有一些妙用。一个函数能够直接调用,也能够用new调用,那假如我只想使用者经过new调用有没有办法呢?下图截取自Vue源码:app

image-20200226160322071

Vue巧妙利用了this的特性,经过检查this是否是Vue的一个实例来检测使用者是经过new调用的仍是直接调用的。函数

没有明确调用者时,this指向window

这个其实在最开始的例子就讲过了,那里没有明确调用者,this指向的是window。咱们这里讲另一个例子,函数里面的函数,this指向谁?工具

function func() {
  function func2() {
    console.log('this:', this);   // 这里的this指向谁?
  }
  
  func2();
}

咱们执行一下看看:

直接执行:

image-20200226162443098

使用new执行:

image-20200226162626673

咱们发现不管是直接执行,仍是使用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赋值时就肯定了:

  1. var anotherFunc = obj.func().getName;实际上是先执行了obj.func()
  2. 执行obj.func()的时候getName箭头函数被申明
  3. 这时候箭头函数的this应该是当前做用域的this,也就是func()里面的this
  4. func()由于是被obj调用,因此this指向obj
  5. 调用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是能改的,callapply均可以修改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执行
  }
}

总结

  1. 函数外面的this,即全局做用域的this指向window。
  2. 函数里面的this老是指向直接调用者。若是没有直接调用者,隐含的调用者是window。
  3. 使用new调用一个函数,这个函数即为构造函数。构造函数里面的this是和实例对象沟通的桥梁,他指向实例对象。
  4. 箭头函数里面的this在它申明时肯定,跟他当前做用域的this同样。
  5. DOM事件回调里面,this指向绑定事件的对象(currentTarget),而不是触发事件的对象(target)。固然这两个能够是同样的。若是回调是箭头函数,请参考上一条,this是它申明时做用域的this。
  6. 严格模式下,函数里面的this指向undefined,函数外面(全局做用域)的this仍是指向window。
  7. call和apply能够改变this,这两个方法会当即执行原方法,他们的区别是参数形式不同。
  8. bind也能够修改this,可是他不会当即执行,而是返回一个修改了this的函数。

文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。

做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

相关文章
相关标签/搜索