从原理到实践:JavaScript中的this指向,一篇就够了

0 阅读4分钟

从原理到实践:JavaScript中的this指向,一篇就够了

前言

最近在复习JavaScript的this指向问题,写了几个小例子来加深理解。很多同学觉得this难,其实是因为this是在函数执行时确定的,而不是定义时。这个特性导致了this指向的“善变”。

今天,就让我通过这几个代码例子,带你由浅入深掌握JavaScript中的this指向。


第一章:基础概念 - this的默认绑定

1.1 全局环境下的this

在浏览器全局环境中,this指向window对象:

var name = "windowName"; // 全局变量
var func1 = function() {
  console.log('func1');
}

console.log(this.name); // "windowName"
console.log(window.name); // "windowName"

核心知识点:

  • 全局作用域下,this === window
  • var声明的全局变量会自动挂载到window对象上

第二章:谁调用我,我就指向谁

2.1 对象方法调用

var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name);
  }
}

a.func1(); // "Cherry"

关键理解: 这里的this指向了对象a。因为func1是由a调用的。

2.2 经典面试题 - 定时器中的this

// 2.html 中的例子
var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name);
  },
  func2: function() {
    setTimeout(function() {
      this.func1(); // 这里会报错!
    }, 3000)
  }
}

a.func2(); // TypeError: this.func1 is not a function

为什么报错?

定时器的回调函数是由定时器内部调用的,此时this指向了window对象。而window对象上并没有func1方法(虽然有全局变量func1,但这里调用的是对象方法)。

验证一下:

var a = {
  // ... 同上
  func2: function() {
    setTimeout(function() {
      console.log(this); // window
    }, 3000)
  }
}

第三章:解决this丢失的三种方案

3.1 方案一:保存this(that = this)

// 3.html 中的例子
var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name);
  },
  func2: function() {
    var that = this; // 保存外层的this
    setTimeout(function() {
      that.func1(); // "Cherry" ✅
    }, 3000)
  }
}

a.func2(); // 3秒后输出 "Cherry"

原理: 利用闭包的特性,内部函数可以访问外部函数的变量。that保存了正确的this引用。

3.2 方案二:bind绑定

// 2.html 中的例子
var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name);
  },
  func2: function() {
    setTimeout(function() {
      this.func1(); 
    }.bind(a), 3000) // bind永久绑定this为a
  }
}

a.func2(); // 3秒后输出 "Cherry" ✅

重要区别:

  • bind()不会立即执行,返回一个新函数,永久绑定this
  • call()/apply()立即执行函数,临时绑定this
// 对比演示
a.func1.call(a); // 立即执行
const boundFunc = a.func1.bind(a); // 返回绑定后的函数,不执行
boundFunc(); // 执行时this已经绑定为a

3.3 方案三:箭头函数

// 4.html 中的例子
var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name);
  },
  func2: function() {
    setTimeout(() => {
      console.log(this); // a对象 ✅
      this.func1(); // "Cherry" ✅
    }, 3000)
  }
}

a.func2(); // 3秒后输出 "Cherry"

箭头函数的特点:

  • 没有自己的this
  • 继承定义时所在作用域的this
  • this是静态的,不会改变

第四章:深入理解箭头函数

4.1 箭头函数没有自己的this

// 5.html 中的例子
const func = () => {
  console.log(this); // window(在浏览器环境)
}

func(); // window

4.2 箭头函数不能作为构造函数

const func = () => {
  console.log(this);
}

new func(); // TypeError: func is not a constructor ❌

为什么? 箭头函数没有自己的this,也没有prototype属性,无法进行实例化。

4.3 关于arguments对象

const func = () => {
  console.log(arguments); // ReferenceError ❌
}

// 箭头函数也没有自己的arguments对象
// 但可以这样获取参数
const func2 = (...args) => {
  console.log(args); // [1, 2, 3] ✅
}

func2(1, 2, 3);

第五章:综合实践 - 分析一段复杂代码

让我们来分析1.html中的代码,它包含了多个知识点的综合运用:

var name = "windowName";
var func1 = function() {
  console.log('func1');
}
var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name);
  },
  func2: function() {
    setTimeout(function() {
      this.func1(); // 这里原本有问题
      return function() {
        console.log('hahaha');
      }
    }.call(a), 3000) // ⚠️ 注意:这里用了call
  }
}

这里有个坑:

setTimeout的第一个参数应该是函数,但.call(a)立即执行这个函数,并把返回值作为第一个参数传给setTimeout。这里返回的是undefined,相当于:

// 实际执行效果
setTimeout(undefined, 3000)

正确写法:

// 使用bind(不会立即执行)
setTimeout(function() {
  this.func1();
}.bind(a), 3000)

// 或者使用箭头函数
setTimeout(() => {
  this.func1(); // 这里的this继承自func2
}, 3000)

第六章:面试题精选

6.1 经典组合题

var name = 'window';

var obj = {
  name: 'obj',
  fn1: function() {
    console.log(this.name);
  },
  fn2: () => {
    console.log(this.name);
  },
  fn3: function() {
    return function() {
      console.log(this.name);
    }
  },
  fn4: function() {
    return () => {
      console.log(this.name);
    }
  }
}

obj.fn1();      // 'obj' - 对象方法调用
obj.fn2();      // 'window' - 箭头函数,this指向外层window
obj.fn3()();    // 'window' - 独立函数调用
obj.fn4()();    // 'obj' - 箭头函数,this继承自fn4的this

6.2 优先级问题

function foo() {
  console.log(this.name);
}

const obj1 = { name: 'obj1', foo };
const obj2 = { name: 'obj2' };

obj1.foo();                 // 'obj1'
obj1.foo.call(obj2);       // 'obj2' - call优先于隐式绑定
const bound = foo.bind(obj1);
bound.call(obj2);          // 'obj1' - bind绑定后,call无法改变

this绑定优先级:

  1. new绑定(最高)
  2. call/apply/bind显式绑定
  3. 对象方法调用(隐式绑定)
  4. 默认绑定(独立函数调用,最低)

总结

this的指向规律其实很简单,记住这几点:

  1. 函数被调用时才能确定this指向
  2. 普通函数:谁调用我,我指向谁
  3. 箭头函数:我在哪里定义,this就跟谁一样
  4. 可以通过bind永久绑定this,call/apply临时绑定this

理解了这些,JavaScript的this问题就迎刃而解了。


练习题

// 尝试分析下面的输出
const obj = {
  name: 'obj',
  say: function() {
    setTimeout(() => {
      console.log(this.name);
    }, 100);
  }
}

obj.say(); // 输出什么?

const say = obj.say;
say(); // 输出什么?

答案和解析欢迎在评论区讨论!


如果你觉得这篇文章对你有帮助,请点赞👍收藏⭐,让更多的小伙伴看到!我们下期再见!