JS中的this指向总结

为什么会出现this

首先在学习this到底指向谁之前,先要知道为什么要有this?实际上从某些角度来说,即便没有this也无伤大雅,我们先看个例子。

  1. const obj = {
  2. name: "coderwei",
  3. running() {
  4. console.log(obj.name + "在跑步");
  5. },
  6. sleep() {
  7. console.log(obj.name + "在睡觉");
  8. },
  9. study() {
  10. console.log(obj.name + "在学习");
  11. },
  12. };
  13. obj.running(); //coderwei在跑步
  14. obj.sleep(); //coderwei在睡觉
  15. obj.study(); //coderwei在学习

在这个例子中,我们会发现,即便不用this,也不影响我们代码的正常执行,但是如果有一天obj这个对象的名字发生了变化,那么内部的所有obj都要换成对应的名字,否则就找不到这个对象了,这样就会影响我们代码的正常运行。所以我们就需要一个对象,能够指向他自身,那么这个时候this就理所当然的出现了。

  1. const obj = {
  2. name: "coderwei",
  3. running() {
  4. console.log(this.name + "在跑步");
  5. },
  6. sleep() {
  7. console.log(this.name + "在睡觉");
  8. },
  9. study() {
  10. console.log(this.name + "在学习");
  11. },
  12. };
  13. obj.running(); //coderwei在跑步
  14. obj.sleep(); //coderwei在睡觉
  15. obj.study(); //coderwei在学习

我们会神奇的发现代码照常运行,并没有任何问题,更神奇的是这个this还是动态绑定的,这也是this那么晦涩难懂的原因,他在不同的情况下会有不同的绑定规则。

this的绑定规则

以下绑定规则都是在非严格模式下

如果没有指明在什么环境下,默认都是在浏览器环境下

全局作用域下的this到底指向谁?

首先我们需要知道,this的指向和所处的环境有关,在浏览器下面和在node环境下指向的是不同的东西,具体有什么区别在实际案例中会指出,首先我们先看下面例子:

  1. console.log(this);

我们只是单纯的输出一下this,会发现在浏览器下面运行这段js脚本输出的是window,而在node环境下输出的是一个空对象({}),首先在浏览器下,这个this指向浏览器的window对象,这没什么好说的,在node环境下为什么会指向一个空对象?那是因为node是commonjs的最成功的实践者,他内部使用commonjs的模式开发,他将每个文件都分成一个模块,而这个this就是使用了call绑定到一个变量thisValue,看下面这行图片

07_this指向 - 图1

那么问题来了,this.exports有是一个什么东西呢?继续看下面这一张图片

07_this指向 - 图2

显而易见,这个东西就是一个空对象。我的理解是这个空对象实际上就是module.exports,如果我的理解有误,可以随时联系提醒我。

默认绑定

  1. //定义一个函数
  2. function foo() {
  3. console.log(this);
  4. }
  5. foo(); //window

我们会发现这里的this在浏览器环境下指向window,而在node环境下这里的this指向global全局对象。其实在判断函数的this指向的时候,我们第一点需要判断的是他是不是一个独立的函数调用,如果是,那么都是指向window的。

看这个例子:

  1. let obj = {
  2. name: "coderwei",
  3. foo: function () {
  4. console.log(this);
  5. },
  6. };
  7. let bar = obj.foo;
  8. bar(); // window

一样的,不用去管bar对象是谁给的,怎么给的,就看他是怎么调用的,前面赋值在写的花里胡哨,只需要永远记得,this是在执行的时候动态绑定的,只要你的调用的是一个单独的函数调用,那么这个this就是指向window。

隐式绑定

隐式绑定在实际开发中也很常见,是通过某个对象调用的,这个时候会进行一个隐式绑定。

函数和方法

在js中,方法和函数有什么区别?我们主要看这个函数是不是独立的,如果他是独立的,那么我们将他称之为函数,如若不然,我们将他称之为方法。举个例子,在对象中的函数我们通常称之为方法,因为他始终和这个对象联系在一起,调用也是通过这个对象调用的

看下面例子:

  1. function foo() {
  2. console.log(this.name);
  3. }
  4. foo(); //'',因为window对象上本来就有个name属性,所以打印出来是一个空字符串
  5. let obj = {
  6. name: "coderwei",
  7. foo,
  8. };
  9. obj.foo(); //coderwei

在执行foo函数的时候,通过obj对象调用的,所以this会动态绑定到obj对象上,这就是一个典型的隐式绑定的例子。所以还是那句话,一定要关注函数调用的位置,他怎么被调用的,this指向的关键在这个位置。

显式绑定

首先我们先看看在js中函数有几种调用方式

  1. function foo() {
  2. console.log("函数执行");
  3. }
  4. foo(); //函数执行
  5. foo.call(); //函数执行
  6. foo.apply(); //函数执行

我们会发现上面三种调用方式,都会执行函数,那么下面两种方式存在的意义是什么?js发展至今,只要某个语法存在,那么必定有他存在的意义,不然早就被抛弃的。区别在于this指向的问题,下面两种调用方式我们可以手动的绑定一个对象作为this的指向,还有一种bind方法,也是绑定this的,但是他并不会执行这个函数,后续我会单独出一篇文章探讨call、apply和bind方法并且使用js自己实现一下这三个方法。

  1. let obj = {
  2. name:'coderwei',
  3. }
  4. function foo(){
  5. console.log(this.name)
  6. }
  7. foo() // ''
  8. foo.call(obj) //coderwei
  9. foo.apply(obj) //coderwei
  10. let bar = foo.bind(obj) //bind方法只绑定不执行,绑定好后返回一个新的函数,以后这个函数的this
  11. //就是bind的第一个参数
  12. bar() //coderwei

我们会发现使用call或者是apply的方式调用,输出的结果和直接调用是不用的,他们的this指向的是call函数的第一个参数。当然会有第二个参数,第二个参数也是这两个方法不同的地方。这里我们就不做探讨,一起放到后面专门讲这几个方法的文章中

小提示

如果call、apply、bind绑定了一个null或者是undefined,那么默认会绑定到window,应该是实现这几个函数的时候内部做了边界判断

new绑定

new操作符涉及到面向对象的概念,也是js实现面向对象的基础。首先对于new操作符在内部做了什么,我们就直接上结论,我们主要关注使用new操作符之后,我的this指向谁了?

  1. /*
  2. 这里我们需要注意一个细节,通常情况下构造函数我们会以大写字母开头,用于和普通的函数做一个区分,
  3. 因为他们都是用function定义的。果然,不遵守这个规范也无可厚非,从代码运行的层面来说,他并不会影
  4. 响代码的正常运行,主要是加以区分方便日后的开发者和自己进行维护
  5. */
  6. function Person() {}
  7. const p1 = new Person();

这里我们需要注意一个细节,通常情况下构造函数我们会以大写字母开头,用于和普通的函数做一个区分,因为他们都是用function定义的。果然,不遵守这个规范也无可厚非,从代码运行的层面来说,他并不会影响代码的正常运行,主要是加以区分方便日后的开发者和自己进行维护

this指向谁了

  1. 在内存中创建一个对象p1
  2. 将People的prototype赋值给p1.proto
  3. 将创建出来的对象p1和构造函数内部的this进行一个绑定
  4. 处理内部代码
  5. return 这个对象出去(函数内部没有返回其他对象

new操作符我们主要关注this绑定的问题,在内部会将p1对象的this绑定到函数调用的this上面。

构造函数的this

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. const p1 = new Person("coderwei");
  5. console.log(p1.__proto__ === Person.prototype); //true 验证上面第二点
  6. console.log(p1.name); //coderwei

内置函数的this

  1. forEach ```javascript //1. 只传递一个参数 let items = [1, 2, 3, 4, 5]; items.forEach(function (item) { console.log(item, this); // 我们会发现这里的this指向的是window });

//2. 传递两个参数 let items = [1, 2, 3, 4, 5]; items.forEach(function (item) { console.log(item, this); // 我们会发现这里的this指向的是coderwei },’coderwei’);

  1. <br />当然类似的方法也有map、filter等等,需要注意的是这里不能写成箭头函数,因为箭头函数不绑定this的。
  2. 2.
  3. setTimeout
  4. ```javascript
  5. setTimeout(function () {
  6. console.log(this); // window
  7. }, 200);


这个位置的this指向的也是window,但是这里有个特殊的点,看下面代码

  1. "use strict";
  2. function foo() {
  3. console.log(this);
  4. }
  5. foo(); //undefined


在严格模式下函数内部的this指向的是undefined。但是放到setTimeout上,事情就变得不一样了。

  1. "use strict";
  2. setTimeout(function () {
  3. console.log(this); // window
  4. }, 200);


诡异吧,这个this指向的还是window,这个点笔者没有办法证明是如何实现的,如果有其他朋友有思路的可以指出。我的猜测是内部处理setTimeout的时候,对这个函数并不是直接执行,而是执行了一个bind方法,这个在翻阅资料的时候看到过一篇文章,在V8的测试文件中关于setTimeout的片段的注释中提到了一嘴bind。

this绑定的优先级

显式绑定vs隐式绑定

首先隐式绑定没有什么好说的,隐式绑定讲道理嘛,他就应该是最低的,他是内部自动绑定的,所以说只要你想改,无论用什么方式,都应该成功的改变this的指向

  1. function foo() {
  2. console.log(this);
  3. }
  4. foo.call("coderwei"); //coderwei
  5. foo.apply("coderwei");//coderwei
  6. const bar = foo.bind("coderwei");
  7. bar();//coderwei

隐式绑定VS new绑定

  1. let obj = {
  2. name: "coderwei",
  3. foo: function () {
  4. console.log(this);
  5. },
  6. };
  7. obj.foo(); // obj对象
  8. new obj.foo(); // foo函数

显式绑定vs new绑定

  1. function foo() {
  2. console.log(this);
  3. }
  4. let bar = foo.bind("coderwei");
  5. new bar();// foo函数

这里需要注意一点,call和apply不能被new实例化,所以他们之间无法比较优先级

总结

  1. new绑定优先级大于显式绑定
  2. 显式绑定优先级大于隐式绑定
  3. 隐式绑定优先级大于默认绑定

箭头函数

箭头函数是ES6新增的一种新的编写函数的方式,他会比以往编写函数的方式更加简单。

  • 箭头函数是不会绑定this的,他也没有arguments(在函数内部能够拿到的实参列表)
  • 箭头函数不能当成构造函数来使用,因为他没有自己的this

因为箭头函数没有this,所以它不适用上面的几钟规则,如果箭头函数内部使用到了this,他就需要去到外层作用域去找this

  1. let obj = {
  2. name: "coderwei",
  3. foo: function () {
  4. console.log(this.name);
  5. },
  6. };
  7. obj.foo(); //coderwei
  8. //改造成箭头函数
  9. let obj = {
  10. name: "coderwei",
  11. foo: () => {
  12. console.log(this.name);
  13. },
  14. };
  15. obj.foo();//''还是那一点,window下面本来就有个name属性,所以是空字符串。本质上这里指向window

分析几道面试题

第一道

  1. var name = "window";
  2. var person = {
  3. name: "person",
  4. sayName: function () {
  5. console.log(this.name);
  6. },
  7. };
  8. function sayName() {
  9. var sss = person.sayName;
  10. sss(); //他就是一个独立的函数调用,前面在花里胡哨都不需要关心
  11. person.sayName(); //隐式绑定不多说
  12. (person.sayName)();
  13. (b = person.sayName)();
  14. }
  15. sayName();
  16. // window
  17. //coderwei
  18. //coderwei
  19. //window

这道题总的来说没多大难度,主要关注(person.sayName)()本质上还是通过person调用的,所以他的this还是指向person,(b = person.sayName)()这里他是一个自执行函数,也就是前面讲的独立的函数调用,所以说这里指向了window

第二道

  1. var name = "window";
  2. var person = {
  3. name: "coderwei",
  4. foo1: function () {
  5. console.log(this.name);
  6. },
  7. foo2: () => console.log(this.name),
  8. foo3: function () {
  9. return function () {
  10. console.log(this.name);
  11. };
  12. },
  13. foo4: function () {
  14. return () => {
  15. console.log(this.name);
  16. };
  17. },
  18. };
  19. var person2 = { name: "person2" };
  20. person.foo1();
  21. person.foo1.call(person2);
  22. person.foo2();
  23. person.foo2.call(person2);
  24. person.foo3()();
  25. person.foo3.call(person2)();
  26. person.foo3().call(person2);
  27. person.foo4()();
  28. person.foo4.call(person2)();
  29. person.foo4().call(person2);
  30. // coderwei
  31. // person2
  32. // window
  33. // window
  34. // window
  35. // window
  36. // person2
  37. // coderwei
  38. // person2
  39. // coderwei

分析一下每一行的函数调用,

  • 在第一次调用的时候,我们直接通过一个对象去调用一个函数,这里会有一个默认绑定,所以结果是coderwei
  • 第二次函数调用通过call的方法来调用,所以他会覆盖隐式绑定,以显示绑定为主,所以结果是person2
  • 第三次函数调用是调用了一个箭头函数,箭头函数不绑定this,所以他回去外层作用域去找,外层作用域是全局,注意:对象没有作用域,不要把对象那个花括号当成一个作用域。
  • 第四次函数调用通过call的方式来调用,但是咱们的箭头函数不绑定this,你给我,我还不要呢,所以他的this依旧去上层作用域找,所以还是window
  • 第五次函数调用是返回了一个新的函数,实际上是拿到新的函数直接做一个调用,所以他就是一个独立的函数调用,结果显而易见,依旧是window
  • 第六次函数调用是给foo3用call绑定了一个this,然后在执行内部返回的函数。但是我们只是给外部的函数绑定了一个this,关内部函数什么事情,本质上还是一个独立的函数调用,所以返回的还是一个window
  • 第七次函数调用是先执行foo3函数,拿到返回的函数之后给这个函数绑定了this,所以结果是person2
  • 第八次函数调用式是先调用foo4函数,然后拿到返回的函数后继续调用。但是需要注意的是返回的函数是一个箭头函数,所以他没有自己的this,他要去外层作用域找,他是被一个函数包裹的,函数是有作用域的,也就是说他拿到的是外层的函数的this,也就是我们前面进行隐式绑定的person2
  • 第九次函数调用跟第八次一样,都是要去外层寻找this,不过这一次我们给外层的函数指定了this,是person2,所以他拿到的就是person2
  • 第十次函数调用是拿到返回的箭头函数之后给这个箭头函数绑定this,箭头函数没有自己的this,也不能绑定this,所以说他依旧是去外层找,外层函数的this隐式绑定的person,也就是coderwei

所以说箭头函数有点像一个二百五,我不管,我就是没有,你给我我也不要,我就要去上层作用域要,他是什么我就拿什么作为我的this。