前言

this也算是面试必问的问题之一了。
但很多人对this理解得模模糊糊,工作时得打印出来看看才敢往下写,面试时只能回答个大概,细节经不起推敲。
今天我们就针对这个痛点,解决掉它!

现在我们来看道题: :::warning var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number = 2;
number = number
2;
number = 3;
return function () {
var num = this.number;
this.number = 2;
console.log(num);
number
= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number); ::: 答案依次是:10 9 3 27 20
惊不惊喜,意不意外?如果你比较容易就答对了,那说明你对this的理解还是非常到位的。不然,就跟我一块来看看这篇文章呗~ 这道题的解析在思考1哦~

this是啥?

在讨论this的时候,一般都会说“指向xxx”。this就是一个指针,在了解具体指向之前,我们先引入几个名词:

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定

而且它们的优先级new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

默认绑定

  • 在不能应用其它绑定规则时使用,通常是独立函数调用
  • 独立函数:是指在全局上下文中的函数,它的this指向如下:
    • 非严格且处于Node环境:globalThis
    • 非严格且处于Windows环境:window
    • 严格模式下: undefined

没有特殊声明的话,本文都是浏览器环境执行的结果。

  1. function getName(){
  2. console.log('Name: ', this.name);
  3. }
  4. var name = 'laohuang';
  5. getName();

解析:调用getName()时,它处于全局上下文,应用了默认绑定,this指向全局对象window,所以控制台会打印:Name: laohuang

隐式绑定

  • 函数的调用是通过某个对象调的,或者说调用位置上存在对象,也即obj.fn()
  • this指向对象属性链中最后一层。比如obj1.obj2.obj3.fn(), this指向obj3
  • 隐式绑定存在绑定丢失的情况。请记住:obj.fn()是隐式绑定,但如果fn()前啥都没有,属于默认绑定。

典型的obj.fn

  1. function getName(){
  2. console.log('Name: ', this.name);
  3. }
  4. var laohuang = {
  5. name: 'laohuang',
  6. getName: getName
  7. }
  8. var name = 'feifei';
  9. laohuang.getName();

解析getName函数在laohuang外部声明。laohuang内部的getName相当于做了一次赋值操作。在调用laohuang.getName()时,调用位置是laohuang,隐式绑定会把getName里的this绑定到laohuang上,所以控制台会打印:Name: laohuang

this指向对象属性链中最后一层

  1. function getName(){
  2. console.log('Name: ', this.name);
  3. }
  4. var feifei = {
  5. name: 'feifei',
  6. getName: getName
  7. }
  8. var laohuang = {
  9. name: 'laohuang',
  10. friend: feifei
  11. }
  12. var name = 'FEHuang';
  13. laohuang.friend.getName();

解析this指向对象属性链中最后一层,所以隐式绑定会把this绑定到laohuang.frendfeifei上,所以控制台会打印:Name: feifei

隐式绑定的大陷阱 - 绑定丢失

1. 绑定丢失 - 将函数的引用给另一变量时:

  1. function getName(){
  2. console.log('Name: ', this.name);
  3. }
  4. var laohuang = {
  5. name: 'laohuang',
  6. getName: getName
  7. }
  8. var name = 'FEHuang';
  9. var getNameCopy = laohuang.getName;
  10. getNameCopy();

解析var getNameCopy = laohuang.getNamegetName的引用赋值给了getNameCopygetNameCopy直接指向getName方法getNameCopy()前啥都没有,所以它是隐式绑定this指向全局上下文。所以控制台会打印:Name: FEHuang

2. 绑定丢失 - 回调函数中:

  1. function getName(){
  2. console.log('Name: ', this.name);
  3. }
  4. var feifei = {
  5. name: 'feifei',
  6. getName: getName
  7. }
  8. var laohuang = {
  9. name: 'laohuang',
  10. getName: function() {
  11. setTimeout(function() {
  12. console.log('Name: ', this.name)
  13. })
  14. }
  15. }
  16. var name = 'FEHuang';
  17. laohuang.getName(); // Name: FEHuang
  18. setTimeout(feifei.getName, 1000); // Name: FEHuang
  19. setTimeout(function() {
  20. feifei.getName(); // Name: feifei
  21. }, 1000)

解析

  • laohuang.getName(): 这里很好理解,setTimeout的回调函数中,this使用的是默认绑定,此时又是非严格模式,因此打印Name: FEHuang
  • setTimeout(feifei.getName, 1000): 这里相当于将feifei.getName的引用直接给了setTimeout第一个变量,最后执行了这个变量。这里绑定丢失,使用了默认绑定,因此指向全局上下文,打印:Name: FEHuang
  • setTimeout(function() { feifei.getName(); }, 1000): 虽然也是在setTimeout的回调中,但这里是直接执行了feifei.getName(),使用隐式绑定,this指向feifei。所以打印:Name: feifei

显示绑定

  • 显示绑定就是通过call,apply,bind的方式,显式的指定this所指向的对象。
  • call,applybind的第一个参数,就是对应函数的this所指向的对象。callapply的作用一样,只是传参方式不同。callapply都会执行对应的函数,而bind方法不会。
  • 注意call,apply的特殊传参会被转换:传null/undefined —> 全局上下文;原始值 —> 对象(非严格模式)/原始值(严格模式)

典型的显示绑定

  1. function getName(){
  2. console.log('Name: ', this.name);
  3. }
  4. var laohuang = {
  5. name: 'laohuang',
  6. getName: getName
  7. }
  8. var name = 'FEHuang';
  9. var getNameCopy = laohuang.getName;
  10. getNameCopy.call(laohuang);

解析:显示绑定直接将this绑定到了laohuang,所以控制台会打印:Name: laohuang

特殊情况 - 使用call, apply, bind时也可能会遇到绑定丢失

那么,使用了显示绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。

  1. function getName(){
  2. console.log('Name: ', this.name);
  3. }
  4. var laohuang = {
  5. name: 'laohuang',
  6. getName: getName
  7. }
  8. var name = 'FEHuang';
  9. var getNameCopy = function(fn) {
  10. fn();
  11. };
  12. getNameCopy.call(laohuang, laohuang.getName);

解析getNameCopy.call(laohuang, laohuang.getName)的确将this绑定到laohuangthis了。但call的第二个参数传的getName的引用,所以在执行fn()的时候,相当于直接调用了getName()方法。所以控制台会打印:Name: FEHuang
思考: 如果希望绑定不会丢失,要怎么做?(答案在最后的思考2)

特殊情况 - call, apply, bind的特殊传参

在非严格模式下使用callapply时,如果用作this的值不是对象,则会被尝试转换为对象。nullundefined被转换为全局对象原始值如 7 或 ‘foo’ 会使用相应构造函数转换为对象

1. 传null/undefined:
会将其转换成全局对象,实际使用默认绑定。

  1. var laohuang = {
  2. name: 'laohuang'
  3. }
  4. var name = 'FEHuang';
  5. function getName() {
  6. console.log(this.name);
  7. }
  8. getName.call(null); //FeHuang

解析:实际应用默认绑定,所以控制台会打印:FEHuang

2. 传原始值:
会将其转换成对应的对象

  1. var doSth = function(name){
  2. console.log(this);
  3. console.log(name);
  4. }
  5. doSth.call(2, 'laohuang'); // Number{2}, 'laohuang'
  6. var doSth2 = function(name){
  7. 'use strict';
  8. console.log(this);
  9. console.log(name);
  10. }
  11. doSth2.call(2, 'laohuang'); // 2, 'laohuang'

new绑定

new干了什么?

MDN上的介绍时这样的:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(设置该对象的constructor)到另一个对象;
  3. 将步骤1新创建的对象作为this的上下文;
  4. 如果该函数没有返回对象,则返回this

举例

  1. function getName(name) {
  2. this.name = name
  3. }
  4. var laohuang = new getName('laohuang');
  5. console.log('Name: ', laohuang.name);

解析:在var laohuang = new getName('laohuang')这步,会将getName中的this绑定到对象laohuang上。所以控制台会打印:Name: laohuang

箭头函数

先来看看箭头函数的特点:

  • 函数体内的this对象,继承的是外层代码块的this注意:箭头函数内的this不是定义时所在的对象,而是外层代码块的this。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  • 箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向. ``` var names = { getName0: function() {
    1. console.log(this);
    2. return () => {
    3. console.log(this);
    4. }
    }, getName1: function() {
    1. return function() {
    2. console.log(this);
    3. return () => {
    4. console.log(this);
    5. }
    6. }
    }, getName2: () => {
    1. console.log(this);
    } }

// 第一段 var name0 = names.getName0(); // names对象 name0(); // names对象

// 第二段 var name1 = names.getName1(); var _name1 = name1(); // window对象 _name1(); // window对象

// 第三段 names.getName2(); // window对象

  1. **解析**:
  2. 第一段:
  3. names.getName0()对应隐式绑定,this绑定在了names上,所以控制台会打印:names对象;<br />●name0()执行的就是箭头函数。箭头函数的this继承上一个代码段的this(即getName0()运行时的this,即names)。所以控制台也会打印:names对象;
  4. 第二段:
  5. name1names.getName1()运行后返回的一个全新的函数,对应了上边说到的隐式绑定丢失的情况。此时应用的是默认绑定,this指向了全局对象window。所以name1()打印的是:window对象;<br />●_name1()执行的是箭头函数。<br />○如果箭头函数的this继承自定义时的对象,那_name1()应该打印names对象才对,但这里打印的是window对象,显然这种理解是**错误**的。<br />○按照箭头函数的this是继承于外层代码块的this就很好理解了。外层代码块我们刚刚分析了,this指向的是window,因此这里控制台打印:window对象。
  6. 第三段:
  7. names.getName2()执行的是箭头函数。由于当前的代码块names中不存在this,只能往上层查找。所以这里控制台打印:window对象。
  8. **请牢记:箭头函数中的this继承于外层代码库块的this**
  9. 因为箭头函数里的this也有可能是动态的哟~ 不信看下面的代码:

var names = { getName1: function() { return function() { console.log(this); return () => { console.log(this); } } }, }

var name0 = names.getName1();

var n1 = name0(); // window n1(); // window

var n2 = name0.call(names); // names n2(); // names ```

总结

如何准确判断this的指向

再来复习一下,绑定的优先级是:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定。
然后我们就可以按以下步骤来判断了:

  • 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
  • 函数是否通过call,apply调用,或者使用了bind(显示绑定),如果是,那么this绑定的就是指定的对象。
  • 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.fun()
  • 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象(node环境的全局对象是globalThis,浏览器环境就是window)。
  • 如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
  • 如果是箭头函数,箭头函数的this继承的是外层代码块的this。

最后,this指向还需要多加练习,本文只是列举了个大概,只有不断练习才能熟练掌握哦~
有错误,欢迎指正哦~😗