第 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
// 把方法单独拿出来用试试?
const sayHi = person.sayHi;
sayHi();  // 输出:你好,我是 undefined(或者严格模式下报错)
// 天哪!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
// 在浏览器控制台试试这个
function sayName() {
  console.log('我的名字是:' + this.name);
}

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

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

obj.delayLog();  // 1秒后输出:名字是:(空白或undefined)
// 因为 setTimeout 里的函数是普通函数调用,this 指向 window!

隐式绑定:对象方法中的 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,想手动指定它是谁。这时候就轮到 callapplybind 这三兄弟登场了!

 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 手动指定 thisnew 绑定下,用 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:数组传参

applycall 功能一模一样,唯一的区别是:参数用数组传递

 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
// 经典应用场景

// 场景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:数组追加
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push.apply(arr1, arr2);  // [1, 2, 3, 4, 5, 6]
arr1.push(...arr2);            // [1, 2, 3, 4, 5, 6, 4, 5, 6](ES6+)

// 场景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节)

callapplybind 是 JavaScript 给我们控制 this 的三大神器。callapply 都是立即调用函数,区别只是传参方式(逐一 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章)

1. this 绑定规则

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

2. call / apply / bind

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

3. this 丢失问题

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

记忆口诀

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

实战建议

  1. 永远不要把对象的方法单独赋值给变量调用
  2. 优先使用箭头函数解决回调中的 this 问题
  3. bind 适合需要稍后调用或预设参数的场景
  4. 构造函数记得用 new 调用,否则 this 会乱套
  5. class 语法可以避免很多 this 相关的坑

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