JavaScript this 关键字详解

简介

this 是 JavaScript 中最容易被误解的关键字之一。与其它语言不同,JavaScript 的 this 不是在定义时决定的,而是在调用时决定的。

核心规则:this 的值取决于函数的调用方式,而不是函数的定义位置。

function showThis() {
  console.log(this);
}

// 同样的 showThis 函数,不同的调用方式,this 完全不同!

一、默认绑定(Default Binding)

独立函数调用时,this 使用默认绑定。

非严格模式

// 浏览器环境
function foo() {
  console.log(this);      // window(浏览器全局对象)
  console.log(this === window);  // true
}
foo();

// Node.js 环境
function bar() {
  console.log(this);      // global(Node.js 全局对象)
  console.log(this === global);  // true
}
bar();

严格模式(推荐)

'use strict';

function foo() {
  console.log(this);  // undefined(严格模式下默认绑定为 undefined)
}
foo();

// 在严格模式下,全局函数中 this 不再是全局对象

重要区别

模式 独立函数中的 this 说明
非严格模式 全局对象(window / global) 历史遗留行为
严格模式 undefined 现代 JavaScript 推荐
// 非严格模式
function test() {
  console.log(this);  // window 或 global
}
test();

// 严格模式
'use strict';
function testStrict() {
  console.log(this);  // undefined
}
testStrict();

二、隐式绑定(Implicit Binding)

当函数作为对象的方法调用时,this 指向该对象。

基本示例

let person = {
  name: 'Alice',
  greet: function() {
    console.log('Hello, ' + this.name);
    console.log(this);  // person 对象
  }
};

person.greet();  // this 指向 person → "Hello, Alice"

只有最后一层影响 this

let obj = {
  name: 'Alice',
  inner: {
    name: 'Bob',
    greet: function() {
      console.log(this.name);
    }
  }
};

obj.inner.greet();  // this 指向 inner → "Bob"(只看最后一层)

隐式丢失(常见陷阱!)

let person = {
  name: 'Alice',
  greet: function() {
    console.log('Hello, ' + this.name);
  }
};

// 正常调用
person.greet();  // "Hello, Alice"(this = person)

// 陷阱:赋值给变量
let greetFn = person.greet;  // 只是把函数本身赋值了
greetFn();  // "Hello, "(this = window 或 undefined!)

// 原因:greetFn 是独立调用的,使用默认绑定

另一个隐式丢失的例子

let person = {
  name: 'Alice',
  greet: function() {
    console.log('Hello, ' + this.name);
  }
};

// 将方法作为参数传递(常见在回调中)
setTimeout(person.greet, 100);  // "Hello, "(this 丢失!)

// 原因:相当于 setTimeout(function() { ... }, 100)
// person.greet 只是传递了函数引用,调用时与 person 无关

链式调用中的 this

let calculator = {
  value: 0,
  add(n) {
    this.value += n;
    return this;  // 返回 this 实现链式调用
  },
  subtract(n) {
    this.value -= n;
    return this;
  },
  getResult() {
    return this.value;
  }
};

let result = calculator.add(5).subtract(2).add(10).getResult();
console.log(result);  // 13

三、显式绑定(Explicit Binding)

使用 call()、apply() 或 bind() 手动指定 this 的值。

call() — 逐个传参

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

let person = { name: 'Alice' };

// call 的第一个参数是 this,后面是函数的参数(逐个传)
greet.call(person, 'Hello', '!');   // "Hello, Alice!"
greet.call(person, 'Hi', '.');       // "Hi, Alice."

apply() — 数组传参

function add(a, b) {
  console.log(this.prefix + (a + b));
}

let obj = { prefix: '结果: ' };

// apply 的第一个参数是 this,第二个是参数数组
add.apply(obj, [3, 5]);  // "结果: 8"

call 和 apply 的区别

方法 参数传递方式 适用场景
call(this, arg1, arg2, ...) 逐个传递参数 参数已知
apply(this, [arg1, arg2, ...]) 数组传递参数 参数在数组中,或参数数量不定
function sum() {
  let args = Array.from(arguments);
  let total = args.reduce((a, b) => a + b, 0);
  console.log(this.prefix + total);
}

let data = { prefix: '总和: ' };

// call:需要知道参数数量
sum.call(data, 1, 2, 3);  // "总和: 6"

// apply:适合参数数组
sum.apply(data, [1, 2, 3, 4]);  // "总和: 10"

bind() — 永久绑定 this

bind() 返回一个新函数,其 this 被永久绑定到指定值。

function greet() {
  console.log('Hello, ' + this.name);
}

let person = { name: 'Alice' };
let greetAlice = greet.bind(person);  // 创建一个 this 永久绑定到 person 的新函数

greetAlice();  // "Hello, Alice"

// 即使尝试用 call/apply 改变 this,也不生效(硬绑定)
greetAlice.call({ name: 'Bob' });  // 仍然是 "Hello, Alice"(已被 bind 锁定)

硬绑定(Hard Binding)

function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments);  // 强制 this 指向 obj
  };
}

function greet() {
  console.log('Hello, ' + this.name);
}

let person = { name: 'Alice' };
let boundGreet = bind(greet, person);

boundGreet();  // "Hello, Alice"
boundGreet.call({ name: 'Bob' });  // 仍然是 "Hello, Alice"(硬绑定)

四、new 绑定(New Binding)

使用 new 调用函数时,该函数成为构造函数,this 指向新创建的对象。

new 操作符的执行过程

function Person(name) {
  // 1. 创建一个新空对象
  // 2. 将新对象的原型指向 Person.prototype
  // 3. 将 this 绑定到新对象(这一步!)
  // 4. 执行函数体
  // 5. 如果没返回其他对象,自动返回 this

  this.name = name;
  this.greet = function() {
    console.log('Hello, ' + this.name);
  };

  // 没有 return,隐式返回 this
}

let alice = new Person('Alice');
alice.greet();  // "Hello, Alice"
console.log(alice.name);  // "Alice"

// alice 是新对象,this 在构造函数中指向它

构造函数返回值的特殊情况

function Person(name) {
  this.name = name;

  // 如果返回的是对象(非原始值),会覆盖 new 创建的 this
  // return { name: 'Bob' };  // 取消注释后,alice.name 会变成 "Bob"
}

let alice = new Person('Alice');
console.log(alice.name);  // "Alice"(如果没有返回对象)

如果没有 new 会怎样?

function Person(name) {
  this.name = name;
}

// 正确使用 new
let p1 = new Person('Alice');
console.log(p1.name);  // "Alice"

// 忘记使用 new(非严格模式)
let p2 = Person('Bob');  // 没有 new!
console.log(p2);          // undefined(构造函数没返回)
console.log(name);          // "Bob"(this 是 window,污染了全局!)

// 严格模式下会更安全
'use strict';
function PersonStrict(name) {
  // this 是 undefined,赋值会报错
  // this.name = name;  // TypeError: Cannot set property 'name' of undefined
}
PersonStrict('Charlie');

五、箭头函数(Arrow Functions)

箭头函数没有自己的 this。它继承外层词法作用域中的 this。

基本特性

let person = {
  name: 'Alice',
  // 普通函数:this 动态绑定
  greetRegular: function() {
    console.log('Regular: ' + this.name);
  },
  // 箭头函数:this 继承外层
  greetArrow: () => {
    console.log('Arrow: ' + this.name);
  }
};

person.greetRegular();  // "Regular: Alice"(this = person)

// 问题:箭头函数的 this 继承自外层(这里是全局或 undefined)
person.greetArrow();  // "Arrow: " 或报错(this 不是 person!)

箭头函数适合的场景

let counter = {
  count: 0,
  start() {
    // 箭头函数继承 start() 的 this(即 counter)
    setInterval(() => {
      this.count++;
      console.log(this.count);
    }, 1000);
  }
};

counter.start();  // 每秒输出 1, 2, 3...(this 正确指向 counter)

箭头函数不适合的场景

// 不适合作为对象方法(无法获取对象 this)
let person = {
  name: 'Alice',
  // ❌ 箭头函数作为方法时,this 不是 person
  greet: () => {
    console.log('Hello, ' + this.name);  // this.name 可能不是 "Alice"
  }
};

// ✅ 应该使用普通函数
let person2 = {
  name: 'Bob',
  greet() {
    console.log('Hello, ' + this.name);  // this = person2
  }
};

对比表

特性 普通函数 箭头函数
有自己的 this ✅ 是 ❌ 否(继承外层)
this 绑定时机 调用时决定 定义时决定(继承外层)
适合作为方法 ✅ 适合 ❌ 不适合
适合作为回调 需手动绑定 this ✅ 自动继承外层 this
有 arguments ✅ 是 ❌ 否
可用 new ✅ 可以 ❌ 不可以(没有 [[Construct]])

六、事件监听器中的 this

DOM 事件(浏览器环境)

在 DOM 事件监听器中,this 通常指向触发事件的元素。

// 浏览器环境
let button = document.getElementById('my-btn');

// 普通函数:this 指向触发事件的元素
button.addEventListener('click', function() {
  console.log(this);        // button 元素
  console.log(this.tagName);  // "BUTTON"
});

// 箭头函数:this 继承外层(可能不是 button!)
button.addEventListener('click', () => {
  console.log(this);  // 继承自外层作用域(可能是 window 或 undefined)
});

Node.js 事件(EventEmitter)

const EventEmitter = require('events');
let emitter = new EventEmitter();

emitter.on('message', function(data) {
  console.log(this);  // emitter 对象(EventEmitter)
  console.log(data);
});

emitter.emit('message', 'Hello');

七、定时器中的 this

setTimeout / setInterval

在 setTimeout 和 setInterval 中,回调函数中的 this 默认指向全局对象(非严格)或 undefined(严格)。

// 非严格模式(浏览器)
let obj = {
  count: 0,
  start() {
    setTimeout(function() {
      this.count++;  // this 是 window,不是 obj!
      console.log(this.count);  // NaN(window.count 是 undefined)
    }, 100);
  }
};
obj.start();

// 严格模式
'use strict';
let obj2 = {
  count: 0,
  start() {
    setTimeout(function() {
      console.log(this);  // undefined(严格模式)
    }, 100);
  }
};
obj2.start();

解决方法 1:使用箭头函数(推荐)

let obj = {
  count: 0,
  start() {
    // 箭头函数继承 start() 的 this(即 obj)
    setTimeout(() => {
      this.count++;
      console.log(this.count);  // 1, 2, 3...(this = obj)
    }, 100);
  }
};
obj.start();

解决方法 2:保存 this 引用

let obj = {
  count: 0,
  start() {
    let self = this;  // 保存 this 引用
    setTimeout(function() {
      self.count++;  // 使用 self
      console.log(self.count);
    }, 100);
  }
};
obj.start();

解决方法 3:使用 bind

let obj = {
  count: 0,
  start() {
    setTimeout(function() {
      this.count++;
      console.log(this.count);
    }.bind(this), 100);  // 手动绑定 this
  }
};
obj.start();

八、优先级规则

当多种绑定规则同时适用时,按以下优先级决定 this:

优先级顺序(从高到低)

1. new 绑定        → 最高优先级
   new Foo()  → this = 新创建的对象

2. 显式绑定      → call / apply / bind
   fn.call(obj)  → this = obj

3. 隐式绑定      → 作为对象方法调用
   obj.fn()  → this = obj

4. 默认绑定        → 独立函数调用
   fn()  → this = 全局对象 或 undefined

判断决策树

函数是如何调用的?
│
├─ 使用了 new 吗?
│   └─ 是 → this = 新对象(优先级最高)
│
├─ 使用了 call / apply / bind 吗?
│   └─ 是 → this = 指定的对象(优先级第二)
│
├─ 是作为对象的方法调用的吗?(obj.fn())
│   └─ 是 → this = 该对象(优先级第三)
│
└─ 都不是
    └─ this = 全局对象(非严格)或 undefined(严格)(优先级最低)

示例验证

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

let obj1 = { name: 'obj1' };
let obj2 = { name: 'obj2' };

// 默认绑定
foo();  // window 或 undefined

// 隐式绑定(优先级高于默认)
obj1.foo = foo;
obj1.foo();  // obj1(this = obj1)

// 显式绑定(优先级高于隐式)
foo.call(obj2);  // obj2(this = obj2,覆盖了隐式)

// new 绑定(优先级最高)
let instance = new foo();  // foo {}(this = 新对象,即使 foo 上有 call/apply)

九、特殊情况和陷阱

null / undefined 作为 call / apply 的第一个参数

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

// null 或 undefined 作为第一个参数时,被忽略,使用默认绑定
foo.call(null);    // window(非严格)或 undefined(严格)
foo.apply(undefined);  // window(非严格)或 undefined(严格)

// 严格模式下
'use strict';
function bar() {
  console.log(this);
}
bar.call(null);  // null(严格模式下,this 就是 null)

间接引用导致的 this 丢失

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

let obj1 = {
  name: 'obj1',
  foo: foo
};

let obj2 = {
  name: 'obj2'
};

// 正常
obj1.foo();  // obj1

// 间接引用:赋值表达式返回的是函数本身,不是方法调用
(obj2.foo = obj1.foo)();  // window 或 undefined(this 丢失!)

回调函数中的 this 丢失

let person = {
  name: 'Alice',
  friends: ['Bob', 'Charlie'],
  listFriends() {
    this.friends.forEach(function(friend) {
      // forEach 的回调中,this 是 window 或 undefined!
      console.log(this.name + ' 的朋友: ' + friend);  // " 的朋友: Bob"(this.name 丢失)
    });
  }
};

person.listFriends();  // 输出不正确

// 解决方法 1:保存 this
listFriends() {
  let self = this;
  this.friends.forEach(function(friend) {
    console.log(self.name + ' 的朋友: ' + friend);  // "Alice 的朋友: Bob"
  });
}

// 解决方法 2:forEach 的第二个参数(thisArg)
listFriends() {
  this.friends.forEach(function(friend) {
    console.log(this.name + ' 的朋友: ' + friend);  // "Alice 的朋友: Bob"
  }, this);  // 传入 thisArg
}

// 解决方法 3:箭头函数(推荐)
listFriends() {
  this.friends.forEach(friend => {
    console.log(this.name + ' 的朋友: ' + friend);  // "Alice 的朋友: Bob"
  });
}

嵌套函数中的 this 问题

let obj = {
  name: 'Alice',
  outer() {
    console.log(this);  // obj(outer 作为方法调用)

    function inner() {
      console.log(this);  // window 或 undefined!(inner 是独立调用)
    }

    inner();
  }
};

obj.outer();

// 解决方法:箭头函数
let obj2 = {
  name: 'Bob',
  outer() {
    console.log(this);  // obj2

    let inner = () => {
      console.log(this);  // obj2(箭头函数继承 outer 的 this)
    };

    inner();
  }
};
obj2.outer();

十、综合示例

示例 1:方法借用(Method Borrowing)

let person1 = {
  name: 'Alice',
  greet() {
    console.log('Hello, ' + this.name);
  }
};

let person2 = {
  name: 'Bob'
};

// 借用 person1 的 greet 方法,但 this 指向 person2
person1.greet.call(person2);  // "Hello, Bob"

示例 2:柯里化和偏函数中的 this

function multiply(a, b) {
  return a * b;
}

// 使用 bind 固定第一个参数
let double = multiply.bind(null, 2);  // this 无关紧要,因为函数没用 this
console.log(double(5));  // 10(2 * 5)
console.log(double(10)); // 20(2 * 10)

// 如果函数中使用了 this
function greet(greeting) {
  return greeting + ', ' + this.name;
}

let alice = { name: 'Alice' };
let greetAlice = greet.bind(alice, 'Hello');
console.log(greetAlice());  // "Hello, Alice"

示例 3:安全地使用事件监听器

// 浏览器环境
let button = document.getElementById('my-btn');
let counter = {
  count: 0,
  handleClick() {
    this.count++;
    console.log('点击次数: ' + this.count);
  }
};

// ❌ 直接传递:this 丢失
// button.addEventListener('click', counter.handleClick);

// ✅ 方法 1:bind
button.addEventListener('click', counter.handleClick.bind(counter));

// ✅ 方法 2:箭头函数
button.addEventListener('click', () => counter.handleClick());

// ✅ 方法 3:包装函数
button.addEventListener('click', function() {
  counter.handleClick();  // 通过 counter 调用,this 正确
});

示例 4:修复定时器中的 this

let timer = {
  count: 0,
  start() {
    // ❌ 错误方式
    // setInterval(function() {
    //   this.count++;  // this 不是 timer!
    // }, 1000);

    // ✅ 方法 1:箭头函数(推荐)
    setInterval(() => {
      this.count++;
      console.log('计数: ' + this.count);
    }, 1000);
  },
  // ✅ 方法 2:bind
  start2() {
    setInterval(function() {
      this.count++;
      console.log('计数: ' + this.count);
    }.bind(this), 1000);
  }
};

timer.start();

示例 5:构造函数的安全模式

function Person(name) {
  // 安全模式:检查是否使用了 new
  if (!(this instanceof Person)) {
    return new Person(name);  // 没使用 new,自动补上
  }

  this.name = name;
}

let p1 = new Person('Alice');
console.log(p1.name);  // "Alice"

let p2 = Person('Bob');  // 忘记使用 new
console.log(p2.name);  // "Bob"(仍然正确,因为函数内部处理了)

总结:this 速查表

四种绑定规则

绑定类型 调用方式 this 指向 优先级
new 绑定 new Foo() 新创建的对象 1(最高)
显式绑定 fn.call(obj) / fn.apply(obj) / fn.bind(obj) obj 2
隐式绑定 obj.fn() obj 3
默认绑定 fn() 全局对象(非严格)或 undefined(严格) 4(最低)

箭头函数的 this

特性 说明
没有自己的 this 继承外层词法作用域的 this
定义时决定 this 在定义箭头函数时就确定了
无法改变 call / apply / bind 对箭头函数无效

this 判断清单

每次遇到 this 时,按顺序问自己:

  • 是否使用了 new?(是 → 新对象)
  • 是否使用了 call / apply / bind?(是 → 指定的对象)
  • 是否作为对象的方法调用?(是 → 该对象)
  • 是否是箭头函数?(是 → 外层作用域的 this)
  • 以上都不是 → 全局对象(非严格)或 undefined(严格)

常见陷阱速查

陷阱 现象 解决方法
方法赋值给变量 this 变为默认绑定 使用 bind() 或箭头函数
回调函数传参 this 丢失 传入 thisArg 或用箭头函数
定时器回调 this 不是期望的对象 箭头函数或 bind()
forEach 回调 this 是全局或 undefined 传 thisArg 或用箭头函数
嵌套函数 内层函数 this 丢失 保存 self 或用箭头函数
忘记 new 污染全局或报错 使用安全模式或 class

终极记忆:JavaScript 的 this 不关心函数定义在哪,只关心函数怎么被调用。箭头函数是个例外——它关心定义在哪(继承外层的 this)。