JavaScript this 指向:别再死记硬背,这样理解最轻松!

73 阅读5分钟

JavaScript this 指向:别再死记硬背,这样理解最轻松!

前言:从一个生活场景说起

想象一下你在不同的场合介绍自己:

// 在公司里
function introduce() {
  console.log("我是" + this.name + ",在" + this.company + "工作");
}

// 场景1:作为员工自我介绍
var employee = {
  name: "小明",
  company: "阿里",
  intro: introduce
};
employee.intro(); // "我是小明,在阿里工作" - this 指向 employee

// 场景2:换个环境说这个介绍
var say = employee.intro;
say(); // "我是undefined,在undefined工作" - this 指向 window

同一个"介绍"的方式,在不同场合下,说的是不同的人! 这就是 this 的核心。


一、为什么 this 会指向全局?一个"忘记带名片"的比喻

1.1 先看一个最简单的例子

var myName = "全局名字";

function sayName() {
  console.log(this.myName);
}

sayName(); // 输出:"全局名字"

为什么指向全局?

想象一下:

  • obj.method() 就像是小明在公司会议上说:"我是公司的小明"
  • func() 就像是小明在街上突然喊:"我是谁?"
  • 街上的路人(全局环境)问:"你说的是谁的名字?"
  • 小明说:"我没带名片,就随便喊喊"
  • 于是大家默认他说的是"街上的名人"(全局变量)

1.2 再看我的学习笔记中的例子

// 这是你的 1.html 中的例子
var bar = {
  myName: 'time.geekbang.com',
  printName: function () {
    console.log(myName);        // 找变量(作用域链查找)
    console.log(this.myName);   // 找"当前名片"上的名字
  }
}

var myName = '极客邦';
bar.printName(); 
// 1. "极客邦" - 变量查找,找到全局的
// 2. "time.geekbang.com" - this 指向 bar,用 bar 的名字

// 关键来了!
var _printName = bar.printName; // 只复制了"说话方式",没复制"名片"
_printName();
// 1. "极客邦" - 还是找到全局变量
// 2. undefined - this 指向 window,window.myName 不存在(如果是 var myName='极客邦',这里会输出"极客邦")

二、this 的 5 种"说话场合"(对应你的笔记)

2.1 场合一:作为员工说话(对象方法)

// 你的 2.html 例子
var myObj = {
  name: '极客时间',
  showThis: function() {
    console.log(this); // 指向 myObj
  }
}
myObj.showThis(); // 在"公司"说,说的是"公司的事"

比喻:在公司的会议室里,你说"我们公司",指的是你所在的公司。

2.2 场合二:在街上说话(普通函数)

// 你的笔记中提到:"普通函数运行时。this指向全局对象上"
function foo() {
  console.log(this); // window
}
foo();

// 严格模式就是:"别乱说话!"
'use strict';
function strictFoo() {
  console.log(this); // undefined
}
strictFoo();

比喻:在街上大喊"我",没有人知道你说的是谁,默认就是"街上的人"(全局)。

2.3 场合三:借别人身份说话(call/apply)

// 你的 2.html 第二个例子
function foo() {
  this.myName = '极客时间';
}

let bar = {
  myName: '极客邦',
  test1: 1
};

foo.call(bar); // "借用 bar 的身份说话"
console.log(bar.myName); // "极客时间" - bar 的名字被改了!

比喻:你拿着别人的名片说:"我是XXX",别人就把你当成那个人了。

2.4 场合四:新人自我介绍(构造函数)

// 你的 4.html 例子(修正版)
function CreateObj() {
  // new 的时候:创建一个"新人",让 this 指向他
  this.name = '极客时间';
  console.log(this); // 指向新创建的对象
}

var myObj = new CreateObj();
console.log(myObj.name); // "极客时间"

比喻:新人入职时说"我是新来的小明",这时候的"我"就是指向这个新人自己。

2.5 场合五:按钮被点击时说话(事件处理)

// 你的 5.html 例子
document.getElementById('link').addEventListener('click', function() {
  console.log(this); // 指向被点击的 <a> 元素
});

比喻:按钮被按时说"我被按了",说的就是按钮自己。


三、最容易出错的"名片丢失"问题

3.1 的经典案例

var myObj = {
  name: "极客时间",
  showThis: function() {
    this.name = '极客邦'
    console.log(this);
  }
}

// 第一种:拿着名片说话
myObj.showThis(); // 输出 myObj,this.name 被改为"极客邦"

// 第二种:名片丢了!
var foo = myObj.showThis; // 只复制了"说话方式"
foo(); // 输出 window,this.name 把 window.name 改了!

关键理解

  • myObj.showThis 是"小明的说话方式"
  • var foo = myObj.showThis 只是复制了"怎么说话"
  • 没有复制"小明的名片"
  • 所以 foo() 说话时,不知道说谁,默认说全局

3.2 箭头函数:自带名片夹

var myObj = {
  name: "极客时间",
  showThis: function() {
    // 普通函数:名片可能会丢
    setTimeout(function() {
      console.log(this.name); // undefined,名片丢了
    }, 100);
    
    // 箭头函数:把名片夹在身上
    setTimeout(() => {
      console.log(this.name); // "极客时间",名片还在
    }, 100);
  }
}
myObj.showThis();

四、给你的学习笔记补充的"避坑指南"

4.1 为什么会有这种设计?

"js的作者忘了处理这种情况...偷懒...直接让js指向全局?"

更准确的理解是: 早期 JavaScript 想让网页脚本编写更简单

// 1995年的网页可能是这样的
<button onclick="changeColor()">变红色</button>
<script>
// 如果没有全局 this,每个函数都要这么写:
function changeColor() {
  document.body.style.color = 'red';
  // 如果 this 不指向 window,就得写成 window.document...
}
</script>

4.2 对象在内存的位置

//bar 是一个对象,定义在全局作用域
//对象在堆内存 在变量环境的栈内存中存放的只是对象在堆内存中的地址

对象在堆里,变量只是存了个地址。同样:

  • this 也是个"地址",指向当前说话的对象
  • 函数调用时,才确定这个地址指向谁

4.3 三个实用的记忆口诀

  1. "点前是谁就是谁"

    obj.method(); // this → obj
    
  2. "没人点就找全局"

    method(); // this → window(非严格模式)
    
  3. "箭头函数看爸爸"

    obj.method = () => {
      console.log(this); // 看定义时的外层 this
    };
    

五、实际开发中的"生存法则"

5.1 法则一:不确定时,先打印看看

function test() {
  console.log('this 是:', this);
  console.log('this 的类型:', typeof this);
}

// 各种情况都试试:
test();                    // 独立调用
obj.test = test; obj.test(); // 作为方法
new test();                // 构造函数
test.call(document.body);  // 指定 this

5.2 法则二:用 bind 把名片钉死

var obj = {
  name: '小明',
  say: function() {
    console.log('我是' + this.name);
  }
};

// 名片容易丢
var say = obj.say;
say(); // "我是undefined"

// 用胶水粘住名片(bind)
var boundSay = obj.say.bind(obj);
boundSay(); // "我是小明" - 名片粘住了!

5.3 法则三:class 中注意 this

class Button {
  constructor() {
    this.text = '点击';
    
    // ❌ 这样会丢名片
    this.element.addEventListener('click', this.handleClick);
    
    // ✅ 用 bind 粘住
    this.element.addEventListener('click', this.handleClick.bind(this));
    
    // ✅ 或者用箭头函数(自动粘)
    this.element.addEventListener('click', () => {
      this.handleClick();
    });
  }
  
  handleClick() {
    console.log(this.text); // 需要正确的 this
  }
}

总结:一张图搞定 this

谁在调用函数?            → this 指向谁?
-----------              -----------
对象.方法()               → 这个对象
方法()                   → 全局(或 undefinednew 函数()               → 新创建的对象
函数.call(对象)          → call 的第一个参数
箭头函数                 → 定义时的外层 this

最后记住this 就像是你说话时的"我"。

  • 在会议室说"我" → 指你自己
  • 在街上说"我" → 街上的人不知道你说谁,默认指"街上的人"
  • 拿着别人名片说"我" → 指名片上的人
  • 箭头函数说"我" → 永远指你爸爸是谁,你就是谁

多写代码,多打印 console.log(this),很快你就会形成直觉。加油!