JavaScript 中的 this 指向详解

116 阅读8分钟

在前面的文章中,我们深入了解了 JavaScript 执行栈和执行上下文 以及 作用域和作用域链。今天我们来聊聊 JavaScript 中另一个重要概念——this 指向。

还记得我们在执行上下文文章中提到的吗?当 JavaScript 创建执行上下文时会做三件事:

  1. 生成变量对象(VO)
  2. 确定作用域(Scope)
  3. 确定 this 的值

今天我们就来深入了解第三点——this 的指向机制。

什么是 this?

想象一下现实生活中的场景:

  • 在家里,你说"我的房间",指的是你自己的房间
  • 在公司,你说"我的办公桌",指的是你在公司的办公桌
  • 在朋友家,你说"我坐这里",指的是你当前所在的位置

this 就像是代码中的"我",它总是指向当前的执行环境。但是,this 的指向不是在写代码时确定的,而是在函数被调用时确定的。

this 的核心特点

  • 动态绑定:this 的值在函数调用时才确定
  • 总是对象:this 总是指向一个对象(严格模式下可能是 undefined)
  • 调用方式决定指向:不同的调用方式会导致不同的 this 指向

this 指向的五种情况

简单来说,this 的指向遵循以下规律:

  1. 默认绑定:独立函数调用时,this 指向全局对象(严格模式下为 undefined)
  2. 隐式绑定:作为对象方法调用时,this 指向调用对象
  3. 显式绑定:通过 call/apply/bind 调用时,this 指向指定对象
  4. new 绑定:通过 new 调用时,this 指向新创建的对象
  5. 箭头函数:this 继承外层作用域的 this(静态绑定)

接下来我们通过实际例子来理解这些规律。

1. 默认绑定 - 独立函数调用

默认绑定是最简单的情况

非严格模式下的默认绑定

function sayHello() {
    console.log('Hello, I am', this);
}

// 独立函数调用
sayHello(); // Hello, I am Window (浏览器) 或 Global (Node.js)

当函数独立调用时(没有任何前缀),this 会指向全局对象:

  • 浏览器环境:window
  • Node.js 环境:global

严格模式下的默认绑定

function sayHelloStrict() {
    'use strict';
    console.log('Hello, I am', this);
}

sayHelloStrict(); // Hello, I am undefined

在严格模式下,独立函数调用时 this 为 undefined,这样设计是为了避免意外修改全局对象。

容易混淆的情况

这里有个很容易踩坑的地方:

const user = {
    name: '小明',
    greet: function() {
        console.log('你好,我是', this.name);
    }
};

// 情况1:作为方法调用
user.greet(); // 你好,我是 小明

// 情况2:赋值后独立调用
const greetFunc = user.greet;
greetFunc(); // 你好,我是 undefined (严格模式) 或 你好,我是 (非严格模式)

关键理解:虽然 greetFuncuser.greet 是同一个函数,但调用方式不同,this 指向就不同!

  • user.greet():通过对象调用,this 指向 user
  • greetFunc():独立调用,this 指向全局对象或 undefined

2. 隐式绑定 - 对象方法调用

隐式绑定就像你在某个场所时,"我"自然指向你在那个场所的身份。

基本的隐式绑定

const student = {
    name: '张三',
    age: 18,
    introduce: function() {
        console.log(`我叫${this.name},今年${this.age}岁`);
    }
};

student.introduce(); // 我叫张三,今年18岁

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

多层对象的隐式绑定

当对象有多层嵌套时,this 指向最后一层调用它的对象:

const company = {
    name: '科技公司',
    department: {
        name: '开发部',
        team: {
            name: '前端组',
            introduce: function() {
                console.log(`我是${this.name}`);
            }
        }
    }
};

company.department.team.introduce(); // 我是前端组

记住:无论调用链有多长,this 总是指向最后一个点前面的对象。

隐式绑定的丢失

这是一个非常容易出错的地方!让我们看几个例子:

情况1:赋值导致绑定丢失

const person = {
    name: '李四',
    sayName: function() {
        console.log('我的名字是', this.name);
    }
};

// 直接调用 - this 指向 person
person.sayName(); // 我的名字是 李四

// 赋值后调用 - this 绑定丢失
const getName = person.sayName;
getName(); // 我的名字是 undefined

情况2:作为参数传递时绑定丢失

const user = {
    name: '王五',
    greet: function() {
        console.log('Hello,', this.name);
    }
};

function executeCallback(callback) {
    callback(); // 这里是独立调用!
}

executeCallback(user.greet); // Hello, undefined

情况3:复杂的调用链

const obj1 = {
    name: 'obj1',
    getName: function() {
        return this.name;
    }
};

const obj2 = {
    name: 'obj2',
    getObj1Name: function() {
        return obj1.getName(); // 注意:这里是 obj1.getName()
    }
};

const obj3 = {
    name: 'obj3',
    getNameIndirect: function() {
        const fn = obj1.getName; // 赋值给变量
        return fn(); // 独立调用
    }
};

console.log(obj1.getName());        // obj1 - 直接调用
console.log(obj2.getObj1Name());    // obj1 - obj1.getName() 中 this 指向 obj1
console.log(obj3.getNameIndirect()); // undefined - fn() 是独立调用

关键理解

  • obj1.getName():this 指向 obj1
  • fn = obj1.getName; fn():this 指向全局对象或 undefined

3. DOM 事件中的 this

在 DOM 事件处理中,this 有特殊的指向规律。

事件处理函数中的 this

当使用传统的函数作为事件处理器时,this 指向绑定事件的元素

<button id="myButton">点击我</button>
<ul id="colorList">
  <li>红色</li>
  <li>蓝色</li>
  <li>绿色</li>
</ul>
const button = document.getElementById('myButton');
const colorList = document.getElementById('colorList');

// 按钮点击事件
button.addEventListener('click', function() {
    console.log('按钮被点击了');
    console.log('this 指向:', this); // this 指向 button 元素
    console.log('按钮文本:', this.textContent); // 点击我
});

// 列表点击事件(事件委托)
colorList.addEventListener('click', function(event) {
    console.log('this 指向:', this); // this 指向 ul 元素
    console.log('触发事件的元素:', event.target); // 实际被点击的 li 元素
    
    if (event.target.tagName === 'LI') {
        console.log('你选择了:', event.target.textContent);
    }
});

this vs event.target

这是一个重要的区别:

  • this:指向绑定事件的元素
  • event.target:指向实际触发事件的元素
// 当点击 li 元素时:
// this → ul 元素(事件绑定在 ul 上)
// event.target → li 元素(实际被点击的元素)

事件处理中的常见问题

在事件处理函数内部定义其他函数时,要注意 this 的指向:

const button = document.getElementById('myButton');

button.addEventListener('click', function() {
    console.log('外层 this:', this); // button 元素
    
    // 问题:内层函数的 this 指向
    const innerFunction = function() {
        console.log('内层 this:', this); // Window 或 undefined(严格模式)
    };
    innerFunction();
    
    // 解决方案1:保存 this 引用
    const self = this;
    const innerFunction2 = function() {
        console.log('内层 this:', self); // button 元素
    };
    innerFunction2();
    
    // 解决方案2:使用箭头函数(后面会详细讲)
    const innerFunction3 = () => {
        console.log('箭头函数 this:', this); // button 元素
    };
    innerFunction3();
});

4. 显式绑定 - 改变 this 指向

有时候我们需要主动改变函数内部 this 的指向,JavaScript 提供了三个方法:call、apply 和 bind。

call 方法

call 方法可以指定函数执行时 this 的指向,并立即执行函数:

// 基本语法:func.call(thisArg, arg1, arg2, ...)

const person1 = {
    name: '张三',
    age: 25
};

const person2 = {
    name: '李四',
    age: 30
};

function introduce(hobby, city) {
    console.log(`我是${this.name},今年${this.age}岁,喜欢${hobby},住在${city}`);
}

// 使用 call 改变 this 指向
introduce.call(person1, '编程', '北京');
// 输出:我是张三,今年25岁,喜欢编程,住在北京

introduce.call(person2, '音乐', '上海');
// 输出:我是李四,今年30岁,喜欢音乐,住在上海

apply 方法

apply 方法与 call 类似,区别在于参数传递方式:

// 基本语法:func.apply(thisArg, [arg1, arg2, ...])

// 使用 apply,参数以数组形式传递
introduce.apply(person1, ['编程', '北京']);
// 输出:我是张三,今年25岁,喜欢编程,住在北京

// apply 的实用场景:数组操作
const numbers = [5, 6, 2, 3, 7];

// 找出数组中的最大值
const max = Math.max.apply(null, numbers);
console.log('最大值:', max); // 7

// 找出数组中的最小值
const min = Math.min.apply(null, numbers);
console.log('最小值:', min); // 2

// 数组合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4, 5, 6]

bind 方法

bind 方法不会立即执行函数,而是返回一个新的函数,新函数的 this 被永久绑定:

// 基本语法:func.bind(thisArg, arg1, arg2, ...)

const person = {
    name: '王五',
    greet: function() {
        console.log(`你好,我是${this.name}`);
    }
};

// 直接调用
person.greet(); // 你好,我是王五

// 赋值给变量会丢失 this
const greetFunc = person.greet;
greetFunc(); // 你好,我是undefined(或报错)

// 使用 bind 绑定 this
const boundGreet = person.greet.bind(person);
boundGreet(); // 你好,我是王五

实际应用场景

1. 事件处理中保持 this

class Counter {
    constructor() {
        this.count = 0;
        this.element = document.getElementById('counter');
        this.button = document.getElementById('increment');
        
        // 错误的方式:this 会丢失
        // this.button.addEventListener('click', this.increment);
        
        // 正确的方式:使用 bind 绑定 this
        this.button.addEventListener('click', this.increment.bind(this));
    }
    
    increment() {
        this.count++;
        this.element.textContent = this.count;
        console.log('当前计数:', this.count);
    }
}

const counter = new Counter();

2. 回调函数中的 this 问题

const user = {
    name: '小明',
    hobbies: ['读书', '游泳', '编程'],
    
    showHobbies() {
        console.log(`${this.name}的爱好:`);
        
        // 问题:forEach 回调中的 this 指向 window
        this.hobbies.forEach(function(hobby) {
            console.log(`${this.name}喜欢${hobby}`); // this.name 是 undefined
        });
        
        console.log('\n修复后:');
        // 解决方案1:使用 bind
        this.hobbies.forEach(function(hobby) {
            console.log(`${this.name}喜欢${hobby}`);
        }.bind(this));
        
        // 解决方案2:使用箭头函数(推荐)
        this.hobbies.forEach((hobby) => {
            console.log(`${this.name}喜欢${hobby}`);
        });
    }
};

user.showHobbies();

3. 函数借用

// 借用数组方法处理类数组对象
function convertToArray() {
    // arguments 是类数组对象,没有数组方法
    console.log('arguments:', arguments);
    console.log('是否为数组:', Array.isArray(arguments)); // false
    
    // 借用数组的 slice 方法转换为真正的数组
    const realArray = Array.prototype.slice.call(arguments);
    console.log('转换后:', realArray);
    console.log('是否为数组:', Array.isArray(realArray)); // true
    
    return realArray;
}

convertToArray(1, 2, 3, 4, 5);

call、apply、bind 的区别总结

| 方法 | 执行时机 | 参数传递 | 返回值 | 使用场景 | |------|----------|----------|--------|-----------|| | call | 立即执行 | 逐个传递 | 函数执行结果 | 临时改变this并执行 | | apply | 立即执行 | 数组传递 | 函数执行结果 | 参数为数组时使用 | | bind | 不执行 | 逐个传递 | 新函数 | 永久绑定this |

function test(a, b) {
    console.log(this.name, a, b);
}

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

// call:立即执行,参数逐个传递
test.call(obj, 1, 2); // 测试对象 1 2

// apply:立即执行,参数数组传递
test.apply(obj, [1, 2]); // 测试对象 1 2

// bind:返回新函数,不立即执行
const boundTest = test.bind(obj, 1, 2);
boundTest(); // 测试对象 1 2

5. 箭头函数中的 this

箭头函数是 ES6 引入的新特性,它在 this 绑定方面有着独特的行为。

箭头函数 this 的特点

核心特点:箭头函数没有自己的 this,它会继承外层作用域的 this。

// 传统函数 vs 箭头函数
const obj = {
    name: '测试对象',
    
    // 传统函数
    traditionalMethod: function() {
        console.log('传统函数 this:', this.name); // 测试对象
        
        setTimeout(function() {
            console.log('传统函数内部 this:', this.name); // undefined(this 指向 window)
        }, 1000);
    },
    
    // 箭头函数
    arrowMethod: function() {
        console.log('外层 this:', this.name); // 测试对象
        
        setTimeout(() => {
            console.log('箭头函数内部 this:', this.name); // 测试对象
        }, 1000);
    }
};

obj.traditionalMethod();
obj.arrowMethod();

实际应用场景

1. 定时器中的 this

class Timer {
    constructor() {
        this.seconds = 0;
        this.minutes = 0;
    }
    
    start() {
        // 使用箭头函数,this 指向 Timer 实例
        setInterval(() => {
            this.seconds++;
            if (this.seconds >= 60) {
                this.minutes++;
                this.seconds = 0;
            }
            console.log(`${this.minutes}:${this.seconds.toString().padStart(2, '0')}`);
        }, 1000);
        
        // 如果使用传统函数,this 会指向 window
        // setInterval(function() {
        //     this.seconds++; // 报错!this 不是 Timer 实例
        // }, 1000);
    }
}

const timer = new Timer();
timer.start();

2. 事件处理中的 this

class ButtonHandler {
    constructor(buttonId) {
        this.clickCount = 0;
        this.button = document.getElementById(buttonId);
        this.init();
    }
    
    init() {
        // 箭头函数保持 this 指向
        this.button.addEventListener('click', () => {
            this.handleClick();
        });
        
        // 传统函数需要 bind
        // this.button.addEventListener('click', this.handleClick.bind(this));
    }
    
    handleClick() {
        this.clickCount++;
        console.log(`按钮被点击了 ${this.clickCount} 次`);
    }
}

const handler = new ButtonHandler('myButton');

3. 数组方法中的 this

const processor = {
    prefix: '处理:',
    
    processItems(items) {
        // 箭头函数中的 this 指向 processor
        return items.map(item => {
            return this.prefix + item;
        });
        
        // 传统函数需要额外处理
        // return items.map(function(item) {
        //     return this.prefix + item;
        // }.bind(this));
    }
};

const result = processor.processItems(['苹果', '香蕉', '橙子']);
console.log(result); // ['处理:苹果', '处理:香蕉', '处理:橙子']

箭头函数的限制

1. 无法通过 call、apply、bind 改变 this

const obj1 = { name: '对象1' };
const obj2 = { name: '对象2' };

// 传统函数可以改变 this
const traditionalFunc = function() {
    console.log('传统函数 this:', this.name);
};

traditionalFunc.call(obj1); // 传统函数 this: 对象1
traditionalFunc.call(obj2); // 传统函数 this: 对象2

// 箭头函数无法改变 this
const arrowFunc = () => {
    console.log('箭头函数 this:', this.name);
};

arrowFunc.call(obj1); // 箭头函数 this: undefined(this 指向全局)
arrowFunc.call(obj2); // 箭头函数 this: undefined(this 指向全局)

2. 不能作为构造函数

// 传统函数可以作为构造函数
function TraditionalConstructor(name) {
    this.name = name;
}

const instance1 = new TraditionalConstructor('测试'); // 正常工作

// 箭头函数不能作为构造函数
const ArrowConstructor = (name) => {
    this.name = name;
};

// const instance2 = new ArrowConstructor('测试'); // 报错!

嵌套箭头函数的 this

const nested = {
    name: '嵌套对象',
    
    method: function() {
        console.log('外层 this:', this.name); // 嵌套对象
        
        const inner = () => {
            console.log('第一层箭头函数 this:', this.name); // 嵌套对象
            
            const deeper = () => {
                console.log('第二层箭头函数 this:', this.name); // 嵌套对象
                
                setTimeout(() => {
                    console.log('定时器中的箭头函数 this:', this.name); // 嵌套对象
                }, 100);
            };
            
            deeper();
        };
        
        inner();
    }
};

nested.method();

最佳实践建议

  1. 需要动态 this 时使用传统函数:对象方法、事件处理器、构造函数
  2. 需要继承外层 this 时使用箭头函数:回调函数、定时器、数组方法
  3. 简化代码时使用箭头函数:简短的函数表达式
// 推荐的使用方式
const api = {
    baseUrl: 'https://api.example.com',
    
    // 对象方法使用传统函数
    request: function(endpoint) {
        return fetch(this.baseUrl + endpoint)
            .then(response => response.json()) // 箭头函数用于回调
            .then(data => {
                return this.processData(data); // 箭头函数中的 this 指向 api
            })
            .catch(error => {
                console.error('请求失败:', error);
            });
    },
    
    // 数据处理方法
    processData: function(data) {
        return data.map(item => ({ // 箭头函数用于数组处理
            ...item,
            processed: true
        }));
    }
};

6. new 绑定 - 构造函数调用

当使用 new 关键字调用函数时,会发生特殊的 this 绑定。

new 操作符的工作原理

当使用 new 调用函数时,JavaScript 会执行以下步骤:

  1. 创建一个新的空对象
  2. 将这个空对象的原型指向构造函数的 prototype
  3. 将构造函数的 this 绑定到这个新对象
  4. 执行构造函数代码
  5. 如果构造函数没有返回对象,则返回这个新对象
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.introduce = function() {
        console.log(`我是${this.name},今年${this.age}岁`);
    };
}

// 使用 new 创建实例
const person1 = new Person('张三', 25);
const person2 = new Person('李四', 30);

person1.introduce(); // 我是张三,今年25岁
person2.introduce(); // 我是李四,今年30岁

console.log(person1.name); // 张三
console.log(person2.name); // 李四

new 绑定的优先级

new 绑定的优先级非常高,甚至可以覆盖 bind 绑定:

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

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

// 使用 bind 绑定 this
const BoundPerson = Person.bind(obj);
BoundPerson('测试'); // 此时 obj.name 变为 '测试'
console.log(obj.name); // 测试

// 使用 new 调用 bind 后的函数
const instance = new BoundPerson('实例');
console.log(instance.name); // 实例(new 的优先级更高)
console.log(obj.name); // 测试(obj 没有被影响)

7. this 绑定的优先级

当多种绑定规则同时出现时,需要了解它们的优先级:

优先级排序(从高到低)

  1. new 绑定 > 显式绑定(bind) > 显式绑定(call/apply) > 隐式绑定 > 默认绑定
  2. 箭头函数比较特殊,它没有自己的 this,无法被任何方式改变
function test() {
    console.log(this.name);
}

const obj1 = { name: '对象1' };
const obj2 = { name: '对象2' };

// 1. 默认绑定
test(); // undefined(严格模式)

// 2. 隐式绑定
obj1.test = test;
obj1.test(); // 对象1

// 3. 显式绑定覆盖隐式绑定
obj1.test.call(obj2); // 对象2

// 4. bind 绑定
const boundTest = test.bind(obj1);
boundTest(); // 对象1
boundTest.call(obj2); // 对象1(bind 无法被 call 覆盖)

// 5. new 绑定覆盖 bind 绑定
function Constructor(name) {
    this.name = name;
}

const BoundConstructor = Constructor.bind(obj1);
const instance = new BoundConstructor('新实例');
console.log(instance.name); // 新实例
console.log(obj1.name); // undefined(obj1 没有被影响)

8. 常见陷阱和解决方案

1:方法作为回调函数

class Timer {
    constructor() {
        this.time = 0;
    }
    
    tick() {
        this.time++;
        console.log('时间:', this.time);
    }
    
    start() {
        // 陷阱:this 绑定丢失
        // setInterval(this.tick, 1000); // 报错
        
        // 解决方案1:箭头函数
        setInterval(() => this.tick(), 1000);
        
        // 解决方案2:bind
        // setInterval(this.tick.bind(this), 1000);
    }
}

const timer = new Timer();
timer.start();

2:事件处理中的 this

class ButtonController {
    constructor(buttonId) {
        this.clickCount = 0;
        this.button = document.getElementById(buttonId);
        this.init();
    }
    
    init() {
        // 陷阱:事件处理函数中的 this 指向 button 元素
        // this.button.addEventListener('click', this.handleClick); // 错误
        
        // 解决方案1:箭头函数
        this.button.addEventListener('click', (e) => this.handleClick(e));
        
        // 解决方案2:bind
        // this.button.addEventListener('click', this.handleClick.bind(this));
    }
    
    handleClick(event) {
        this.clickCount++;
        console.log(`点击次数: ${this.clickCount}`);
    }
}

3:对象方法的解构赋值

const user = {
    name: '用户',
    greet() {
        console.log(`你好,我是${this.name}`);
    }
};

// 陷阱:解构赋值导致 this 丢失
const { greet } = user;
greet(); // 你好,我是undefined

// 解决方案:保持方法调用形式
user.greet(); // 你好,我是用户

// 或者使用 bind
const boundGreet = user.greet.bind(user);
boundGreet(); // 你好,我是用户

总结

通过本文的学习,我们深入了解了 JavaScript 中 this 的各种绑定规则。让我们回顾一下核心要点:

this 绑定的七种情况

  1. 默认绑定:独立函数调用时,this 指向全局对象(非严格模式)或 undefined(严格模式)
  2. 隐式绑定:作为对象方法调用时,this 指向调用对象
  3. DOM 事件绑定:事件处理函数中,this 指向绑定事件的元素
  4. 显式绑定:通过 call、apply、bind 主动指定 this 的指向
  5. 箭头函数:没有自己的 this,继承外层作用域的 this
  6. new 绑定:构造函数调用时,this 指向新创建的对象
  7. 严格模式影响:严格模式下默认绑定为 undefined

优先级规则

当多种绑定规则同时出现时,优先级从高到低为:

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

箭头函数的 this 无法被改变,始终继承定义时的外层作用域。