第 18 章 this 指向

第 18 章 this 指向

JavaScript 里最让人晕头转向的概念——“this"到底指向谁?

18.1 this 绑定规则

this 是什么?

在 JavaScript 里,this 是一个像"变形金刚"一样的变量——它在不同场合会指向不同的东西。

想象 this 是一个神奇的遥控器,你在不同的地方按它,它就会操控不同的电视!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 先来看一个最让人迷惑的例子
const person = {
  name: '张三',
  sayHi: function() {
    console.log('你好,我是' + this.name);
  }
};

person.sayHi();  // 输出:你好,我是张三
// 这里的 this 指向 person 对象

但是!

1
2
3
4
5
6
7
8
// 把方法单独拿出来用试试?
const sayHi = person.sayHi;
sayHi();
// 在普通脚本(非严格模式)里:this 变成 window,window.name 是空字符串
//   输出:你好,我是
// 在严格模式或 ES 模块里:this 是 undefined,直接抛错
//   TypeError: Cannot read properties of undefined (reading 'name')
// 天哪!this 怎么变了?!

这就是 this 最反直觉的地方——它不是固定的,而是由调用方式决定的!同一个函数,换个调用姿势,this 就可能完全不同。


默认绑定:普通函数中的 this

当函数是独立调用的(不是作为对象的方法),this 的默认值是 window(浏览器环境)或者 undefined(严格模式)。

1
2
3
4
5
6
7
8
9
// 场景1:普通函数调用
function introduce() {
  console.log('我叫' + this.name);
}

const person = { name: '李四' };
introduce();  // this 指向谁?
// 在浏览器中,this.name 会变成 window.name(可能是空字符串)
// 在严格模式下,会变成 undefined
1
2
3
4
5
6
7
8
9
// 严格模式下的表现
'use strict';

function showThis() {
  console.log('this 是:', this);
}

showThis();  // 输出:this 是: undefined
// 严格模式下,默认绑定的 this 是 undefined!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 在浏览器控制台试试这个
function sayName() {
  console.log('我的名字是:' + this.name);
}

var name = '我是window的name';  // 注意:用 var 声明的才是 window 的属性
sayName();  // 输出:我的名字是:我是window的name

let name2 = '我不是window的';  // let/const 声明的变量不会挂在 window 上
function sayName2() {
  console.log(this.name2);   // undefined:window 上没有 name2 这个属性
}
sayName2();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 一个经典的坑
const obj = {
  name: '小明',
  delayLog: function() {
    setTimeout(function() {
      console.log('名字是:' + this.name);  // 这里 this 指向谁?
    }, 100);
  }
};

obj.delayLog();  // 100 毫秒后输出:名字是:(浏览器里 this 指向 window)
// 因为 setTimeout 里的函数是普通函数调用:
// 浏览器非严格模式下 this 是 window,window.name 是空字符串 → 输出「名字是:」
// 严格模式/ES 模块下 this 是 undefined → 抛 TypeError
// 在 Node.js 里 this 是 Timeout 对象 → 输出「名字是:undefined」

隐式绑定:对象方法中的 this

当函数是对象的属性,并且通过对象调用时,this 指向调用这个方法的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 隐式绑定的基本规则:谁调用,this 就指向谁
const cat = {
  name: '喵喵',
  sound: '喵~',
  speak: function() {
    console.log(this.name + '说:' + this.sound);
  }
};

cat.speak();  // 输出:喵喵说:喵~
// cat 调用了 speak,所以 this 指向 cat
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 陷阱!赋值操作会丢失 this
const dog = {
  name: '旺财',
  sound: '汪汪~',
  speak: function() {
    console.log(this.name + '说:' + this.sound);
  }
};

const speak = dog.speak;  // 把方法赋值给变量
speak();  // this 变成 window 或 undefined!
// 输出:undefined说:undefined
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 另一个经典陷阱:回调函数
const calculator = {
  value: 100,
  add: function(n) {
    this.value += n;
  },
  process: function(numbers) {
    numbers.forEach(function(n) {
      this.add(n);  // 这里的 this 是谁?
    });
  }
};

// calculator.process([1, 2, 3]);
// 报错!因为 forEach 的回调函数是普通调用,this 指向 window
graph TD
    A["this 绑定规则"] --> B["默认绑定"]
    A --> C["隐式绑定"]
    A --> D["显式绑定"]
    A --> E["new 绑定"]
    A --> F["词法绑定"]
    
    B --> B1["独立调用函数"]
    B --> B2["this = window/undefined"]
    
    C --> C1["对象方法调用"]
    C --> C2["this = 调用对象"]
    
    D --> D1["call/apply/bind"]
    D --> D2["手动指定 this"]
    
    E --> E1["new 构造函数"]
    E --> E2["this = 新建实例"]
    
    F --> F1["箭头函数"]
    F --> F2["继承外层 this"]

显式绑定:call / apply / bind 手动指定

有时候我们不想让 JavaScript 自动决定 this,想手动指定它是谁。这时候就轮到 call、apply、bind 这三兄弟登场了!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 先看一个不用显式绑定的痛苦例子
const person1 = { name: '小红', age: 18 };
const person2 = { name: '小丽', age: 20 };

function introduce(city, job) {
  console.log('我是' + this.name + ',' + this.age + '岁,来自' + city + ',职业是' + job);
}

// 普通调用会报错或输出 undefined
// introduce();  // this 是 undefined

// 用 call 指定 this
introduce.call(person1, '北京', '老师');  // 我是小红,18岁,来自北京,职业是老师
introduce.call(person2, '上海', '医生');  // 我是小丽,20岁,来自上海,职业是医生
1
2
3
// apply 跟 call 一样,只是参数传递方式不同
introduce.apply(person1, ['广州', '工程师']);  // 我是小红,18岁,来自广州,职业是工程师
introduce.apply(person2, ['深圳', '设计师']);  // 我是小丽,20岁,来自深圳,职业是设计师
1
2
3
4
5
6
7
8
// call vs apply 的区别
// call:参数一个个传
func.call(this, arg1, arg2, arg3);

// apply:参数用数组传
func.apply(this, [arg1, arg2, arg3]);

// 效果完全一样!只是传参方式不同
1
2
3
4
5
6
// bind:绑定 this,但不像 call/apply 那样立即调用
const boundIntroduce = introduce.bind(person1);
boundIntroduce('杭州', '律师');  // 我是小红,18岁,来自杭州,职业是律师

// bind 返回一个新函数,原函数不变
introduce.call(person2, '武汉', '会计');  // 我是小丽,20岁,来自武汉,职业是会计
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// bind 的真正强大之处:永久绑定
const person3 = { name: '小刚' };
function greet() {
  console.log('你好,我是' + this.name);
}

const boundGreet = greet.bind(person3);  // 永远绑定到 person3

boundGreet();  // 你好,我是小刚
boundGreet.call({ name: '其他人' });  // 你好,我是小刚(还是指向 person3!)
// 因为 bind 绑定的 this 是"永久"的,无法被覆盖

new 绑定:构造函数中的 this

当用 new 关键字调用函数时,这个函数就变成了构造函数,this 会指向新创建的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 构造函数:专门用来创建对象的函数
function Student(name, age, grade) {
  // this 指向新创建的对象
  this.name = name;    // 给新对象添加属性
  this.age = age;
  this.grade = grade;
  this.introduce = function() {
    console.log('我是' + this.name + ',' + this.age + '岁,在' + this.grade + '年级');
  };
}

// 用 new 调用构造函数
const student1 = new Student('小明', 12, '六年级');
const student2 = new Student('小红', 11, '五年级');

student1.introduce();  // 我是小明,12岁,在六年级
student2.introduce();  // 我是小红,11岁,在五年级
1
2
3
4
5
// 如果不用 new 调用构造函数会怎样?
const student3 = Student('小华', 13, '初一');
console.log(student3);  // undefined!因为构造函数没有 return
// 但是 window.name 会变成 '小华'!
// console.log(name);  // 小华 —— 变量污染了全局!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 构造函数(Class)版本
class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    console.log(this.name + '叫:' + this.sound);
  }
}

const cat = new Animal('猫咪', '喵喵~');
const dog = new Animal('小狗', '汪汪~');

cat.speak();  // 猫咪叫:喵喵~
dog.speak();  // 小狗叫:汪汪~

词法绑定:箭头函数的 this

箭头函数是 ES6 引入的” special agent",它的 this 不遵循普通的绑定规则,而是继承外层函数的 this。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 普通函数的 this
const counter1 = {
  count: 0,
  // 普通函数有自己的 this
  increment: function() {
    this.count++;
    console.log('普通函数 this:', this.count);
  }
};

counter1.increment();  // 普通函数 this: 1
counter1.increment();  // 普通函数 this: 2

// 箭头函数的 this
const counter2 = {
  count: 0,
  // 箭头函数继承外层的 this
  increment: () => {
    this.count++;  // 这里的 this 是谁?
    console.log('箭头函数 this:', this.count);
  }
};

counter2.increment();  // 箭头函数 this: NaN
// 因为箭头函数的 this 是 window 或 undefined,不是 counter2!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 箭头函数的正确用法:在需要保持 this 的回调中使用
const timer = {
  name: '倒计时器',
  seconds: 10,
  start: function() {
    // 用箭头函数,this 永远指向 timer
    setInterval(() => {
      if (this.seconds > 0) {
        this.seconds--;
        console.log(this.name + ':' + this.seconds + '秒');
      }
    }, 1000);
  }
};

// timer.start();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 对比:普通函数 vs 箭头函数
const compare = {
  name: '比较器',

  // 普通函数方法
  normalMethod: function() {
    console.log('普通函数 this.name:', this.name);
  },

  // 箭头函数方法
  arrowMethod: () => {
    console.log('箭头函数 this.name:', this.name);
  }
};

compare.normalMethod();  // 普通函数 this.name: 比较器
compare.arrowMethod();  // 箭头函数 this.name: undefined
// 箭头函数的 this 指向外层(这里是 window)
1
2
3
// 记住这个规则
// 普通函数:谁调用 this,this 就指向谁
// 箭头函数:继承定义时的外层 this,而不是调用时的
graph LR
    A["this 绑定规则总结"] --> B["默认绑定"]
    A --> C["隐式绑定"]
    A --> D["显式绑定"]
    A --> E["new 绑定"]
    A --> F["箭头函数"]
    
    B --> B1["独立调用 → window/undefined"]
    C --> C1["obj.method() → obj"]
    D --> D1["call/apply/bind → 手动指定"]
    E --> E1["new Fn() → 新实例"]
    F --> F1["继承外层 this"]

💡 本章小结(第18章第1节)

JavaScript 的 this 是个"变色龙",它的指向取决于函数的调用方式。默认绑定下,独立调用的函数 this 指向 window(严格模式是 undefined)。隐式绑定下,作为对象方法调用的函数 this 指向该对象。显式绑定下,可以用 call/apply/bind 手动指定 this。new 绑定下,用 new 调用构造函数时 this 指向新创建的实例。箭头函数比较特殊,它不绑定自己的 this,而是继承外层的 this。记住:普通函数看调用,箭头函数看定义!


18.2 call / apply / bind

call:逐一传参

call 是三兄弟中最直接的一个,它会立即调用函数,并把 this 绑定到指定的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 基本用法
function greet(age, city) {
  console.log('你好,我叫' + this.name + ',' + age + '岁,来自' + city);
}

const person = { name: '张三' };

// call(thisArg, arg1, arg2, ...)
greet.call(person, 25, '北京');
// 输出:你好,我叫张三,25岁,来自北京
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// call 的应用场景:继承
function Animal(name, sound) {
  this.name = name;
  this.sound = sound;
}

function Dog(name, breed) {
  // 用 call 调用 Animal 的构造函数,继承它的属性
  Animal.call(this, name, '汪汪~');
  this.breed = breed;
}

const dog = new Dog('旺财', '金毛');
console.log(dog.name);   // 旺财
console.log(dog.sound);  // 汪汪~
console.log(dog.breed);  // 金毛
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 用 call 借用数组方法
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };

// 数组的 slice 方法可以把这个"类数组"变成真正的数组
const realArray = Array.prototype.slice.call(arrayLike, 0);
console.log(realArray);  // ['a', 'b', 'c']

// 或者用 ES6 的 Array.from()
const realArray2 = Array.from(arrayLike);
console.log(realArray2);  // ['a', 'b', 'c']

apply:数组传参

apply 和 call 功能一模一样,唯一的区别是:参数用数组传递。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// apply 的参数是一个数组(或类数组对象)
function greet(age, city) {
  console.log('你好,我叫' + this.name + ',' + age + '岁,来自' + city);
}

const person = { name: '李四' };

// apply(thisArg, [arg1, arg2])
greet.apply(person, [30, '上海']);
// 输出:你好,我叫李四,30岁,来自上海
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// apply 的真正价值:处理数组参数
function max() {
  return Math.max.apply(null, arguments);
}

console.log(max(1, 5, 3, 9, 2));  // 9
// 相当于 Math.max(1, 5, 3, 9, 2)

// 现在有更好的方法了
console.log(Math.max(...[1, 5, 3, 9, 2]));  // 9
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 用 apply 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 以前用 concat
const merged = arr1.concat(arr2);
console.log(merged);  // [1, 2, 3, 4, 5, 6]

// 用 apply(虽然不常见)
const merged2 = Array.prototype.concat.apply([], [arr1, arr2]);
console.log(merged2);  // [1, 2, 3, 4, 5, 6]

// 现在用展开运算符更简洁
const merged3 = [...arr1, ...arr2];
console.log(merged3);  // [1, 2, 3, 4, 5, 6]

bind:绑定 this,返回新函数

bind 不会立即调用函数,而是创建一个新的函数,这个新函数的 this 被永久绑定到指定的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 基本用法
function greet(age, city) {
  console.log('你好,我叫' + this.name + ',' + age + '岁,来自' + city);
}

const person = { name: '王五' };

// 创建绑定后的新函数
const boundGreet = greet.bind(person);

// 现在调用新函数
boundGreet(28, '广州');  // 你好,我叫王五,28岁,来自广州
boundGreet(29, '深圳');  // 你好,我叫王五,29岁,来自深圳
// 无论怎么调用,this 永远指向 person
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// bind 还可以预设部分参数(偏函数)
function multiply(a, b) {
  return a * b;
}

// 创建一个乘以 2 的函数
const double = multiply.bind(null, 2);
console.log(double(5));   // 10
console.log(double(10));  // 20

// 创建一个乘以 3 的函数
const triple = multiply.bind(null, 3);
console.log(triple(5));   // 15
console.log(triple(10));  // 30
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// bind 的实际应用:事件处理
class Counter {
  constructor() {
    this.count = 0;

    // 用 bind 绑定 this
    this.increment = this.increment.bind(this);
  }

  increment() {
    this.count++;
    console.log('计数:' + this.count);
  }
}

const counter = new Counter();

// 当作事件处理器时,this 不会丢失
// button.addEventListener('click', counter.increment);
// 因为已经 bind 过了,所以点击时 this 仍然指向 counter

call vs apply vs bind 对比与场景选择

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 一图流对比
console.log('=== call vs apply vs bind 对比 ===');

function test(a, b, c) {
  console.log('this.name:', this.name);
  console.log('参数:', a, b, c);
}

const obj = { name: '测试对象' };

// call:立即调用,参数逐一传递
console.log('\n--- call ---');
test.call(obj, 1, 2, 3);

// apply:立即调用,参数用数组传递
console.log('\n--- apply ---');
test.apply(obj, [1, 2, 3]);

// bind:不立即调用,返回新函数,参数可以分开传
console.log('\n--- bind ---');
const newTest = test.bind(obj, 1);  // 预设第一个参数
newTest(2, 3);  // 只传剩下的两个参数
1
2
3
4
5
6
7
8
9
// 选择建议
// 1. call:参数不多,直接列出
someFunction.call(context, arg1, arg2, arg3);

// 2. apply:参数是数组,或者参数数量不确定
someFunction.apply(context, arrayOfArgs);

// 3. bind:需要稍后调用,或者需要预设部分参数
const boundFn = someFunction.bind(context, presetArg);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 经典应用场景

// 场景1:类数组转数组
const arrayLike = document.querySelectorAll('div');
const divs = Array.prototype.slice.call(arrayLike);  // 旧方法
const divs2 = Array.from(arrayLike);                // 新方法

// 场景2:找出数组中的最大值
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
const max = Math.max.apply(null, numbers);  // 9
const max2 = Math.max(...numbers);          // 9(ES6+)

// 场景3:把数组 arr2 追加到 arr1 末尾
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 用 apply:把 arr2 的元素逐个作为参数传给 push
arr1.push.apply(arr1, arr2);   // arr1 变成 [1, 2, 3, 4, 5, 6]

// ES6 之后更推荐展开运算符,效果完全相同、也更易读
const arr3 = [1, 2, 3];
arr3.push(...arr2);            // arr3 变成 [1, 2, 3, 4, 5, 6]

// 场景4:事件处理中保持 this
// button.addEventListener('click', handler.bind(this));
graph TD
    A["显式绑定三兄弟"] --> B["call"]
    A --> C["apply"]
    A --> D["bind"]
    
    B --> B1["立即调用"]
    B --> B2["参数逐一传递"]
    
    C --> C1["立即调用"]
    C --> C2["参数数组传递"]
    
    D --> D1["返回新函数"]
    D --> D2["不立即调用"]
    D --> D3["可预设参数"]

💡 本章小结(第18章第2节)

call、apply、bind 是 JavaScript 给我们控制 this 的三大神器。call 和 apply 都是立即调用函数,区别只是传参方式(逐一 vs 数组)。bind 不立即调用,而是创建一个新函数,适合需要稍后调用或预设参数的场景。选择建议:参数确定用 call,参数是数组用 apply,需要稍后调用或预设参数用 bind。


18.3 this 丢失问题

回调函数中的 this 丢失

this 丢失是 JavaScript 中最让人头疼的问题之一。简单来说就是:把对象的方法赋值给变量,或者传给回调函数,this 就不认识原来的对象了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 经典案例:setTimeout 回调中的 this
const person = {
  name: '小明',
  greet: function() {
    console.log('你好,我是' + this.name);
  }
};

person.greet();  // 正常工作:你好,我是小明

// 把方法传给 setTimeout
setTimeout(person.greet, 100);  // 你好,我是(空白或undefined)
// 因为 setTimeout 调用 greet 时,this 变成了 window 或 undefined!
1
2
3
4
5
// 为什么会这样?
// setTimeout(person.greet, 100) 实际上是这样的:
// setTimeout 把 person.greet 这个函数赋值给了内部
// 然后内部以普通函数的方式调用它
// 所以 this 丢失了!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// another 经典案例:数组方法的回调
const util = {
  name: '工具对象',
  process: function(items) {
    return items.map(function(item) {
      // 这里的 this 是谁?
      return item + this.suffix;
    });
  },
  suffix: '元'
};

console.log(util.process([10, 20, 30]));  // ['10undefined', '20undefined', '30undefined']
// 因为 map 的回调是普通函数调用,this 丢失了!

解决方案一:bind

bind 可以永久绑定 this,这样即使在回调中也不会丢失。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 解决方案1:用 bind 预先绑定 this
const person1 = {
  name: '小明',
  greet: function() {
    console.log('你好,我是' + this.name);
  }
};

// 创建一个绑定好 this 的版本
const boundGreet = person1.greet.bind(person1);

// 无论怎么传,this 都不会丢
setTimeout(boundGreet, 100);  // 你好,我是小明
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// bind 在数组方法中的应用
const util2 = {
  name: '工具对象',
  process: function(items) {
    return items.map(function(item) {
      return item + this.suffix;
    }.bind(this));  // 绑定 this
  },
  suffix: '元'
};

console.log(util2.process([10, 20, 30]));  // ['10元', '20元', '30元']

解决方案二:箭头函数

箭头函数不绑定自己的 this,它会继承定义时外层的 this。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 解决方案2:用箭头函数
const person2 = {
  name: '小红',
  greet: function() {
    // 用箭头函数,this 不会丢失
    setTimeout(() => {
      console.log('你好,我是' + this.name);  // this 指向 person2
    }, 100);
  }
};

person2.greet();  // 你好,我是小红
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 箭头函数在数组方法中的应用
const util3 = {
  name: '工具对象',
  process: function(items) {
    return items.map((item) => {
      // 箭头函数继承外层的 this
      return item + this.suffix;
    });
  },
  suffix: '元'
};

console.log(util3.process([10, 20, 30]));  // ['10元', '20元', '30元']
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 注意!箭头函数要写在正确的地方
const person3 = {
  name: '小刚',
  // 如果 greet 本身就是箭头函数,那外层就是 window 了!
  // greet: () => { ... }

  greet: function() {
    // 这里用箭头函数,继承 person3
    setTimeout(() => {
      console.log('你好,我是' + this.name);
    }, 100);
  }
};

person3.greet();  // 你好,我是小刚

解决方案三:变量保存 this

在 ES6 之前,还有一个经典技巧:用一个变量把 this 保存下来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 解决方案3:变量保存 this
const person4 = {
  name: '小丽',
  greet: function() {
    const self = this;  // 把 this 存到变量里
    setTimeout(function() {
      // 用 self 代替 this
      console.log('你好,我是' + self.name);
    }, 100);
  }
};

person4.greet();  // 你好,我是小丽
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 这个技巧在 ES5 时代非常流行
// 叫 "that" 或 "self" 模式
const oldSchool = {
  name: '老写法',
  init: function() {
    var that = this;  // 经典写法:var that = this;
    jQuery('#button').click(function() {
      that.doSomething();  // 用 that 代替 this
    });
  },
  doSomething: function() {
    console.log('做了点什么');
  }
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 变量保存 this 在数组方法中的应用
const util4 = {
  name: '工具对象',
  process: function(items) {
    var self = this;  // 保存 this
    return items.map(function(item) {
      return item + self.suffix;
    });
  },
  suffix: '元'
};

console.log(util4.process([10, 20, 30]));  // ['10元', '20元', '30元']

三种方案对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 假设有这样一个场景
const counter = {
  count: 0,
  // 点击后 count 加 1
  handleClick: function() {
    this.count++;
    console.log('点击次数:' + this.count);
  }
};

// 绑定到按钮上
// button.addEventListener('click', ???);
1
2
3
4
5
6
7
// 方案1:bind 绑定
// button.addEventListener('click', counter.handleClick.bind(counter));

// 方案2:箭头函数(需要在调用处用箭头包裹)
// button.addEventListener('click', () => counter.handleClick());

// 方案3:变量保存(不适用这个场景,需要改 handleClick 定义)
graph TD
    A["this 丢失解决方案"] --> B["bind 绑定"]
    A --> C["箭头函数"]
    A --> D["变量保存 this"]
    
    B --> B1["预先绑定 this"]
    B --> B2["永久生效"]
    
    C --> C1["继承外层 this"]
    C --> C2["最简洁"]
    
    D --> D1["var that = this"]
    D --> D2["ES5 时代常用"]

💡 本章小结(第18章第3节)

this 丢失通常发生在把方法传给回调函数时,因为回调函数是以普通函数方式调用的。有三种解决方案:bind 预先绑定,this 永久生效;箭头函数继承外层 this,代码最简洁;变量保存(var that = this),ES5 时代的经典技巧。现在的推荐是优先使用箭头函数,如果场合不允许,再用 bind。记住:普通函数看调用,箭头函数看定义!


18.4 绑定优先级与常见陷阱

前面几条规则同时出现时,谁说了算?答案是固定的优先级,从高到低:

new 绑定  >  显式绑定(call/apply/bind)  >  隐式绑定(obj.method)  >  默认绑定

new 与 bind 谁更强:new 赢

用 bind 硬绑定过的函数,如果再被 new 调用,绑定的 this 会被忽略,this 指向新创建的实例:

1
2
3
4
5
6
7
function Fn() {
  this.tag = '实例';
}

const Bound = Fn.bind({ tag: '被绑定的对象' });

console.log(new Bound().tag);   // "实例"(new 优先,bind 被忽略)

call / apply 传 null 和原始值会怎样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function showThis() {
  console.log(this);
}

// 非严格模式:null / undefined 会被替换成全局对象,原始值会被包装成对象
showThis.call(null);   // window(浏览器)或 globalThis(Node)
showThis.call(42);     // Number {42}

// 严格模式:传什么就是什么
function strictShow() {
  'use strict';
  console.log(this);
}
strictShow.call(null); // null
strictShow.call(42);   // 42

这也是「同样的写法在类里和普通函数里行为不同」的原因之一:class 内部的代码默认就是严格模式。

箭头函数改不了 this

箭头函数的 this 在定义时就固定了,call / apply / bind 都改不动它:

1
2
3
4
5
6
7
8
const outer = { name: '外层' };

function makeArrow() {
  return () => this.name;      // 记住 makeArrow 被调用时的 this
}

const arrow = makeArrow.call(outer);
console.log(arrow.call({ name: '想改也改不了' })); // "外层"

另外,箭头函数不能当构造函数(没有 prototype,new 会抛 TypeError),也没有自己的 arguments:

1
2
3
4
5
6
7
8
const Arrow = () => {};
// new Arrow();  // TypeError: Arrow is not a constructor

function normal() {
  const getArgs = () => arguments;   // 这里的 arguments 是外层 normal 的
  return getArgs();
}
console.log(normal(1, 2)[0]); // 1

class 方法单独拿出来用:this 是 undefined,不是 window

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Player {
  constructor(name) { this.name = name; }
  hello() { return this.name; }
}

const p = new Player('小明');
console.log(p.hello());   // "小明"

const hello = p.hello;
// console.log(hello());  // TypeError:class 内部是严格模式,this 为 undefined

// 现在的常用写法:类字段 + 箭头函数,天然绑定实例
class Player2 {
  name = '小红';
  hello = () => this.name;   // this 永远指向实例
}
console.log(new Player2().hello()); // "小红"

类字段箭头函数写起来最省心,代价是每个实例各持一份函数,实例数量很大时要权衡内存。

事件处理函数里的 this

DOM 事件监听器是个特例:用普通函数注册时,浏览器会把 this 设为监听器绑定的那个元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const btn = document.querySelector('#btn');

// 普通函数:this 就是按钮元素
btn.addEventListener('click', function () {
  console.log(this === btn);   // true
  this.classList.toggle('active');
});

// 箭头函数:继承外层的 this,拿不到按钮
btn.addEventListener('click', () => {
  // this.classList.toggle('active');  // 会报错
});

// 想用箭头函数又要拿元素,用事件对象更直观
btn.addEventListener('click', (event) => {
  event.currentTarget.classList.toggle('active');
});

链式调用:this 是「最近的那个对象」

1
2
3
4
5
6
7
8
9
const app = {
  config: {
    name: 'demo',
    printName() {
      return this.name;      // this 是 config,不是 app
    },
  },
};
console.log(app.config.printName()); // "demo"

顶层 this 在不同环境里完全不同

环境顶层 this
浏览器普通 <script>(非严格)window
浏览器 ES 模块 / 严格模式undefined
Node.js CommonJS 模块module.exports(一开始是空对象)
Node.js ES 模块undefined

所以「顶层 this 就是全局对象」这句话只在浏览器普通脚本里成立,写跨环境代码时不要依赖它。

一张速查表

调用形式this 指向
fn()window/undefined(严格模式)
obj.fn()obj
a.b.c.fn()a.b.c
fn.call(x) / fn.apply(x)x(立即执行)
fn.bind(x)()x(永久绑定,除非用 new)
new Fn()新创建的实例
箭头函数定义时外层的 this,无法被改变
element.addEventListener('click', function(){})该 element

本章小结(第18章)

1. this 绑定规则

  • 默认绑定:独立调用 → window(严格模式 undefined)
  • 隐式绑定:对象方法调用 → 调用对象
  • 显式绑定:call/apply/bind 手动指定
  • new 绑定:构造函数 → 新建实例
  • 词法绑定:箭头函数 → 继承外层

2. call / apply / bind

  • call:立即调用,参数逐一传递
  • apply:立即调用,参数数组传递
  • bind:返回新函数,不立即调用,可预设参数

3. this 丢失问题

  • 原因:把方法赋值给变量或传给回调时,变成普通函数调用
  • 解决1:bind 预先绑定
  • 解决2:箭头函数
  • 解决3:变量保存 var that = this

4. 优先级与陷阱

  • 优先级:new > 显式绑定 > 隐式绑定 > 默认绑定;new 调用会忽略 bind
  • 非严格模式下 call(null) 会变成全局对象,严格模式下保持 null
  • 箭头函数的 this 由定义位置决定,且不能作为构造函数
  • class 内部默认严格模式,方法单独调用时 this 是 undefined
  • 事件监听器用普通函数时 this 是绑定元素,用箭头函数则继承外层
  • 顶层 this 因环境而异(浏览器脚本是 window,ES 模块是 undefined)

记忆口诀

this 是个变色龙,看调用的方式来决定
默认绑定看 window,严格模式 undefined
隐式绑定看对象,谁调方法指向谁
显式绑定三兄弟,call apply bind 手动定
new 绑定造实例,构造函数返回新对象
箭头函数最特殊,继承外层不绑定自己
this 丢失别慌张,bind 箭头变量来帮忙

实战建议

  1. 对象方法要单独传递时,明确处理 this(bind 或箭头包装),不要直接赋值给变量
  2. 优先使用箭头函数解决回调中的 this 问题;但别把它用在对象方法或需要 DOM 元素 this 的监听器上
  3. bind 适合需要稍后调用或预设参数的场景,也能用于类方法的固定绑定
  4. 构造函数记得用 new 调用,否则 this 会乱套
  5. class 语法内部是严格模式,能暴露出不少 this 相关的隐患;需要时用类字段箭头函数

搞定了 this,你离 JavaScript 大神又近了一步!🎉

最后修改 September 19, 2026: 更新 (3489033b1)