JavaScript系列: 全面掌握JavaScript中的this

201 阅读11分钟

JavaScript中的this,既是难点也是重点。痛苦的来说,不学又不行,学着又头痛。

虽然现在React、Vue3都是hook的思维模式,函数式编程,减少了对this的使用(少了很多的痛苦)。但是对于开发者而言,在平时编写代码的过程中,有着面向对象编程思维,this还是经常打交道的。

本篇就来彻底的聊一聊this的一系列规则,希望你有所收获。

为什么需要this?

this是必须的吗?答案是否定的,可以通过其它的方式,来实现this相同的功能。

示例:

 var obj = {
   name: "copyer",
   getName: function () {
     console.log(obj.name); // 代替:console.log(this.name)
   },
   setName: function (str) {
     obj.name = str; // 代替:this.name = str
   },
 };

通过上面的代码可以看出,getNamesetName方法中拿取对象obj中的name属性,可以通过 this 拿取,也可以通过obj.name 直接拿取。所以this并不是代码中的必需品。

那为什么又需要 this 呢?

想一下,如果把对象名 obj 改为 newObj,那么在上面的代码中,就需要三行代码都需要改动(如果代码量大的话,就可能存在非常多的地方需要更改)。但是如果使用了 this,就算改变了属性名,但是对其中内部代码是影响范围不是很广的,修改的代码就相对地少(甚至没有)。

小结:

  • 从某些角度来说,没有this,通过其他的方式也能解决,但是会使代码变得比较的难维护。
  • 如果使用了this,那就增加了学习成本。
  • 需不需要使用this,看你咯,哈哈哈。

全局作用域中的this

this 存在于两个地方:全局作用域函数作用域。大多数情况下,this出现于函数作用域中。

那么全局作用域下的this,只需要记住即可:

  • 浏览器: this指向window对象
  • node环境: this指向 {}, node环境不存在window对象

在下面的内容,都是针对函数作用域中的this。

函数中this的指向?

直接说结论:this的指向跟函数定义的位置是没有关系的,跟函数的调用方式是有关系的(当前箭头函数除外,下面会讲到)。

函数在被调用的时候,就会在函数调用栈创建一个函数执行上下文,在函数编译阶段,就开始确定了this的指向。

08_1.png

在函数执行的时候,函数会经历两个阶段:编译阶段和执行阶段。

在编译阶段的时候,才确定了this的指向,在执行阶段使用的this就是根据编译阶段而来的。

代码演示

 function foo() {
   console.log(this)
 }
 ​
 // 调用方式一:
 foo() // this: window
 ​
 // 调用方式二:
 var obj = {foo: foo}
 obj.foo() // this: obj
 ​
 // 调用方式三:
 foo.apply('abc') // this: String('abc')

定义了一个函数foo,通过三种不同的方式调用,那么this的指向就不同。从而验证了上面的结论。

函数中this的绑定规则

既然知道了函数是在执行的时候,才确定this的指向。那么就来聊一聊this的四种绑定规则吧。

规则一:默认绑定

就是函数独立调用的时候:就没有绑定到某个对象上进行调用。

那么this的指向就是window

 // 案例一:
 function foo() {
     console.log(this) // window
 }
 foo()
 ​
 // 案例二:
 var obj = {
     name: 'copyer',
     bar: function() {
         console.log(this) // window
     }
 }
 var fn = obj.bar
 fn()

规则二:隐式绑定

通过对象对函数进行调用,那么这时候的this就指向调用的对象

 // 案例一:
 var obj = {
     name: 'copyer',
     foo: function() {
         console.log(this) // obj
     }
 }
 obj.foo()
 ​
 // 案例二:
 var obj1 = {
     name: 'obj1',
     foo: function() {
         console.log(this) // obj1
     }
 }
 var obj2 = {
     name: 'obj2',
     obj1: obj1
 }
 obj2.obj1.foo()

规则三:显示绑定

通过 applycallbind 三个函数,手动指定 this 的指向。

 // 案例一:
 function foo() {
   console.log(this);
 }
 foo.apply(window); // window
 ​
 foo.call("123"); // String('123')
 ​
 foo.bind(123)(); // Number(123)

小知识点

  • 相同点

    1. 所有函数都可以调用三个函数,因为这三个函数在原型链上(Prototype)。
    2. 三个函数的第一个参数,内部都看成一个对象,this就专指该对象。
  • 不同点:

    1. call 和 apply 的第一个参数相同,apply 函数的第二个参数是数组(数组里面的内容分别是传递过来的参数),call 函数从第二个参数开始,传递过来的参数就是依次有序的写下去。
    2. bind 函数就是返回一个新的函数,用于被执行。

规则四:new 绑定

JavaScript中的函数可以当做成一个类的构造函数来使用,也就是使用 new 关键词。

new 内部实现原理

  • 创建一个空对象{}
  • 获取构造函数
  • 链接到原型
  • 绑定this,执行构造函数
  • 返回新对象
 function mockNew() {
     let obj = {}
     // 获取构造函数
     let con = arguments.__proto__.constructor
     // 连接原型
     obj.__proto__ = con.prototype
     let res = con.apply(obj, arguments)
     // 返回一个新对象
     return typeof res === 'object' ? res : obj
 }
 // https://blog.csdn.net/qq_22841387/article/details/123345400

从 mockNew 内部的实现,this的指向也是通过显示绑定(apply)实现的,this的指向就是创建的新对象

 // 案列一:
 function Person(name) {
     this.name = name // 里面的this,就是指向实例化的person对象
 }
 var person = new Person('copyer')

小结

this的四种绑定规则,就是这样,也不是很难理解吧。this的四种规则足以对付绝大数函数的this指向了。理解好了四种绑定规则,再去看下面的内容哟。

规则之间的优先级

对于一个函数而言,在调用的时候,如果有多个绑定规则聚集一身,那么this该听谁的呢?谁的拳头大就指向谁(优先级)。

默认绑定的优先级最低

毫无疑问,默认值就是处于最底层。

显示绑定 > 隐式绑定

 // 验证
 function foo() {
   console.log(this);
 }
 var obj = {
   name: 'copyer',
   foo: foo
 }
 ​
 obj.foo.apply('abc') // this: String('abc')

new绑定 > 隐式绑定

 // 验证
 function foo() {
   console.log(this);
 }
 var obj = {
   name: 'copyer',
   foo: foo
 }
 ​
 var a = new obj.foo() //this: foo {} 实例对象

new绑定 > 显示绑定

new关键词apply函数或者 call函数 不能同时进行使用。原因:new关键词也是调用函数,apply函数和call函数也是调用函数,之间相互矛盾了。

所以只能比较 newbind函数之间的比较。

 // 验证
 function foo() {
   console.log(this);
 }
 var obj = {
   name: 'copyer',
   foo: foo.bind('abc') // bind返回一个新函数,this指向了String('abc')
 }
 ​
 var a = new obj.foo() //this: foo {} 实例对象

小结

优先级: new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

跳出规则之外的现象

上面的四种规则,可以解决绝大多数函数的this指向,但是还有少部分函数的this是没有遵循规则的。

NO1、严格模式

在严格模式下,默认绑定的this是undefined。

 "use strict";
 function foo() {
   console.log(this); // this: undefined
 }
 foo()

N02、忽略显示绑定

当 apply、call、bind的第一个参数是unll或者undefined时,this指向window

 function foo() {
   console.log(this); // this: window
 }
 foo.apply(undefined)

但是在严格模式下,又恢复了该规则

 "use strict"
 function foo() {
   console.log(this); 
 }
 foo.apply(undefined) // this: undefined
 foo.apply(null)      // this: null

NO3、间接函数引用

这种代码非常的少见,但是不排除面试题中可能出现,所以还是了解下。那么直接进入正题。

 var obj1 = {
   name: "obj1",
   getName: function () {
     console.log(this);
   },
 };
 var obj2 = {
   name: "obj2",
 };
 (obj2.bar = obj1.getName)();  //this: window  独立函数调用

是不是很疑惑:

 (obj2.bar = obj1.getName)();  // this: window
 // 等价于
 obj2.bar = obj1.getName;
 obj2.bar()  // this: obj2

其中的原因,我也不知道,哈哈哈。

小知识点

当一行中 以 [ ( 开头的时候,需要在上面一行代码加上一个分号;。不然JS引擎词法分析的时候,会看成一行代码;

示例:

 var obj2 = {
   name: "obj2",
 } // 如果没有分号
 (obj2.bar = obj1.getName)()
 ​
 // 会被解析成
 var obj2 = {
   name: "obj2",
 }(obj2.bar = obj1.getName)()   // 语法报错

NO4、箭头函数(重点理解)

ES6中新增了一个函数类型:箭头函数

 // 形式如下
 () => {}   // 代替了 function() {}

特点

  1. 不能绑定thisarguments属性。
  2. 箭头函数不能作为构造函数使用,即不能和new关键词一起使用

简写优化

  1. 当只有一个参数时,可以省略小括号
  2. 当函数体只有一句代码时,可以省略大括号(如果返回一个对象,可以简写为({})
 [1,2,3].map(v => ({id: v}))
 // 等价于
 [1,2,3].map(v => {
     return {id: v}
 })

说多了,扯远了,this才是主题。

箭头函数是没有自己的this,如果使用this,那么就会去寻找上一层作用域中的this

this的查找规则: 延着作用域链寻找this,找到停止

对象是不会产生作用域的,也就是说没有this。

重点来啦!!!

函数的作用域(以及父级作用域)在定义函数的时候,就已经确定了,所以箭头函数的this在定义的也已经被确定了; 简单的来说箭头函数的this不是在执行的时候确定,而是在定义的时候就已经确定了(跟上面的规则,恰恰相反)

 // 案列一:
 let obj = {
   name: 'copyer',
   getName: function () {
     let logName = () => {
       console.log(this) // this: obj
     }
     logName()
   }
 }
 obj.getName()

obj.getName调用函数,使用隐式绑定。所以 getName函数作用域中的this就是指向obj

getName函数体中的logName函数是一个箭头函数,那么箭头函数中的this就会寻找上一层作用域(即父级作用域),就是getName的函数作用域,其this为obj,所以箭头函数中的this也是指向obj。

 // 案例二:
 let obj = {
   name: 'copyer',
   getName: () => {
     let logName = () => {
       console.log(this) // this: window
     }
     logName()
   }
 }
 obj.getName()

通过obj.getName调用函数,使用隐式绑定。但是呢?getName是一个箭头函数(四种绑定规则失效),是不存在this的,那么就会寻找上一层作用域,即全局作用域,this指向window,所以getName函数作用域中的this也是指向window

getName函数体中的logName,也是一个箭头函数,也会寻找上一层作用域,即getName函数作用域,this指向window,那么logName的函数作用域this也是指向window。

小结

箭头函数的this理解也还好吧,只要你熟悉作用域作用域链,箭头函数的this的原理跟其是一样的。

内置函数的this指向

在JavaScript中提供了一些内置函数,其参数接受一个回调函数。因为不知道内置函数中内部是怎么调用回调函数,所以说,也就无法确定this到底是指向什么。这就只有在平时的过程中,慢慢积累,使用多了,也就知道this的指向了。

setTimeout

setInterval也是一样的。

 // 案例一:setTimeout中的回调函数是个普通函数(function)
 var obj = {
   name: "copyer",
   getName: function () {
     setTimeout(function () {
       console.log(this); // this: window
     }, 1000);
   },
 };
 obj.getName()
 ​
 // 案例二:setTimeout中的回调函数是个箭头函数(寻找上一层作用域)
 var obj = {
   name: "copyer",
   getName: function () {
     setTimeout(() => {
       console.log(this); // this: obj
     }, 1000);
   },
 };
 obj.getName()
 ​
 // 案例三: 严格模式下,上面的两种情况也是一样的

猜测: setTimeout和setInterval中的回调函数属于直接调用(默认绑定),this指向window;如果回调函数是箭头函数,就会寻找上一层作用域。

dom事件

对一些dom节点进行操作(比如:onclick)

 var el = document.getElementsByClassName('box')[0]
 el.onclick = function () {
   console.log(this) // this: dom节点
 }

给dom的onclick 赋值一个函数,当鼠标点击的时候,就会调用该函数。

猜测:JS引擎内部调用onclick时,内部调用函数采用的是显示绑定this;即onclick.apply(dom)

forEach/map/filter

三个函数,都是用于循环。

接受的参数:

  • 参数一:回调函数(必选),内部自动执行
  • 参数二:this的指向(可选)
 // 不传递第二个参数情况下,this指向window
 [1, 2, 4].forEach(function () {
   console.log(this); // this: window
 });
 ​
 // 传递第二个参数
 var obj = {name: 'copyer'}
 [1, 2, 4].forEach(function () {
   console.log(this); // this: obj
 },obj);

猜测:forEach内部调用回调函数,是显示绑定。forEach的第二个参数默认是为window;如果没有传递第二个参数,this就指向window;如果传递了第二个参数:this就指向第二个参数的内容。

map 和 filter 也是一样的。

总结

终于总结完了this的指向问题。内容有点多,情况也比较多,需要经常性的回顾。

但是说,this有多难的话,也应该不至于,记住一下几点原则:

  1. 普通函数(function)是在执行的时候,才确定this的指向的。
  2. 箭头函数是在定义的时候,this就已经确定了(因为定义的时候,作用域已经确定了)。
  3. 记住四种绑定规则:new绑定、显示绑定会、隐式绑定、默认绑定;以及它们的优先级顺序。

记住上面的三点,大部分的this指向问题是没有问题的(NO Problem)。当然,还有一些特殊的情况,需要自己在平时的开发过程中,自己积累。

如果,上面的内容有问题的话,请多多指教。

附带几道面试题,加深this的使用。

面试题

 // 面试题一:
 var name = "window";
 var person = {
   name: "person",
   getName: function () {
     console.log(this.name);
   },
 };
 function getName() {
   var fn = person.getName();
   fn(); // window
   person.getName(); // person
   (b = person.getName)(); // window
 }
 getName();
 // 面试题二:
 var name = 'window'
 var person1 = {
   name: 'person1',
   foo1: function () {
     console.log(this.name)
   },
   foo2: () => console.log(this.name),
   foo3: function () {
     return function () {
       console.log(this.name)
     }
   },
   foo4: function () {
     return () => {
       console.log(this.name)
     }
   }
 }
 ​
 var person2 = { name: 'person2' }
 ​
 person1.foo1() // person1
 person1.foo1.call(person2) // person2
 ​
 person1.foo2() // window
 person1.foo2().call(person2) // window
 ​
 person1.foo3()() // window
 person1.foo3.call(person2)() // window (犯过错)
 person1.foo3().call(person2) // person2
 ​
 person1.foo4()() // person1
 person1.foo4.call(person2)() // person2(犯过错)
 person1.foo4().call(person2) // person1
 // 面试题三:
 var name = "window";
 function Person(name) {
   this.name = name;
   this.foo1 = function () {
     console.log(this.name);
   };
   this.foo2 = () => console.log(this.name);
   this.foo3 = function () {
     return function () {
       console.log(this.name);
     };
   };
   this.foo4 = function () {
     return () => {
       console.log(this.name);
     };
   };
 }
 ​
 var person1 = new Person("person1");
 var person2 = new Person("person2");
 ​
 person1.foo1(); // person1
 person1.foo1.call(person2); // person2 (犯过错)
 ​
 person1.foo2(); // person1
 person1.foo2().call(person2); // person1
 ​
 person1.foo3()(); // window
 person1.foo3.call(person2)(); // window
 person1.foo3().call(person2); // person2
 ​
 person1.foo4()(); // person1
 person1.foo4.call(person2)(); // person2
 person1.foo4().call(person2); // person1
 //面试题四:
 var name = "window";
 function Person(name) {
   this.name = name;
   this.obj = {
     name: 'obj',
     foo1: function () {
       return function () {
         console.log(this.name)
       }
     },
     foo2: function () {
       return () => {
         console.log(this.name)
       }
     }
   }
 }
 ​
 var person1 = new Person("person1");
 var person2 = new Person("person2");
 ​
 person1.obj.foo1()(); // window
 person1.obj.foo1.call(person2)(); // window
 person1.obj.foo1().call(person2); // person2
 ​
 person1.obj.foo2()(); // obj(犯过错)
 person1.obj.foo2.call(person2)(); // person2
 person1.obj.foo2().call(person2); // obj(犯过错)

来挑战一下自己吧。