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

41次阅读

共计 6819 个字符,预计需要花费 18 分钟才能阅读完成。

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 赋值时就确定了:

  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

正文完
 0