理解 JavaScript 的 this:核心概念、常见误区与改变指向的方法

在 JavaScript 编程世界里,this是一个极为关键却又常常令人困惑的概念。深入掌握this能极大地提升对这门语言的驾驭能力,为构建更复杂、高效的代码奠定坚实基础。

一、this 是什么

this是 JavaScript 中的一个关键字,它代表函数执行时的上下文对象。简单来说,当一个函数被调用时,this指向的是与该函数调用相关联的特定对象。其作用在于为函数提供了一种在不同对象环境下复用的机制,使得函数可以根据调用它的对象来执行不同的逻辑,访问不同对象的属性和方法。

二、this 的指向判断

1. 默认绑定:

当函数以独立函数形式调用时,即没有任何对象调用它,在非严格模式下,this会指向全局对象。在浏览器环境中,这个全局对象就是window。但在严格模式下,独立函数调用时this的值为undefined,这一设计有效避免了意外的全局变量污染等问题。

  • 非严格模式 :也称为松散模式,是 JavaScript 的默认模式。在这种模式下,对一些语法和行为的限制相对宽松,例如可以随意使用未声明的变量,会自动创建全局变量;函数中的this在独立调用时默认指向全局对象等。这种模式在早期 JavaScript 开发中较为常用,但也容易导致一些潜在的问题,如变量命名冲突、全局变量污染等。
  • 严格模式 :是一种更严格的 JavaScript 运行模式,可以通过在脚本或函数的开头添加"use strict";来启用。
    • 在严格模式下,有以下一些特点:不允许使用未声明的变量,否则会报错;
    • 禁止删除不可删除的属性;
    • 函数中的this在独立调用时不再默认指向全局对象,而是undefined
javascript 复制代码
// 非严格模式
function foo() {
    console.log(this);
}
foo();
// 严格模式
function bar() {
    'use strict';
    console.log(this);
}
bar();
  • foo函数是在非严格模式下定义的独立函数,调用foo()时,this指向全局对象(浏览器中是window ),在控制台会输出window对象相关信息。
  • bar函数使用了'use strict';声明进入严格模式,调用bar()时,this的值为undefined ,在控制台会输出undefined

2. 隐式绑定:

当函数作为对象的方法被调用,采用obj.method()这种形式时,this会指向调用该方法的对象。这是因为函数在调用时会隐式地与该对象建立关联,函数的调用通过对象触发,所以this绑定到了这个调用者对象上。

javascript 复制代码
const person = {
    name: 'Alice',
    age: 25,
    introduce: function() {
        console.log(this);
        console.log(`My name is ${this.name}, and I'm ${this.age} years old.`);
    }
};

person.introduce();

3. new 绑定:

当使用new关键字调用构造函数时,this会指向新创建的实例对象。构造函数利用this来初始化新对象的属性和方法,this在其中就代表正在创建的这个对象。

javascript 复制代码
function Person(name, age) {
    // 打印 this 的指向
    console.log('构造函数内 this 的指向:', this);

    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log(`你好,我叫 ${this.name},今年 ${this.age} 岁。`);
    };
}

// 使用 new 关键字调用构造函数创建对象实例
const person1 = new Person('张三', 25);
// 打印创建的对象实例
console.log('创建的对象实例:', person1);
  • Person 构造函数里,console.log('构造函数内 this 的指向:', this); 这行代码会输出 this 的指向。由于是通过 new 关键字调用 Person 构造函数来创建对象,所以此时 this 指向新创建的对象实例。

  • const person1 = new Person('张三', 25); 使用 new 关键字调用 Person 构造函数,创建了一个名为 person1 的对象实例。

  • 运行代码后,你会发现构造函数内部打印的 this 指向和后续打印的 person1 对象实例是一样的,这就证明了在构造函数中 this 确实指向新创建的对象实例。

4. 箭头函数:

箭头函数与普通函数不同,它没有自己独立的this绑定。箭头函数中的this继承自外层作用域的this指向,在定义时就已确定,不会随着调用方式的改变而改变,也无法通过callapplybind等方法进行修改。

javascript 复制代码
const obj = {
    name: 'Bob',
    sayHello: function() {
        const arrowFunc = () => {
            console.log(this.name);
        };
        arrowFunc();
    }
};
obj.sayHello();
  • sayHello 函数内部,定义了箭头函数 arrowFunc

  • 在这个例子中,arrowFunc 的外层作用域是 sayHello 函数,而 sayHello 函数中的 this 指向 obj,所以 arrowFunc 中的 this 也指向 obj

  • 当执行 arrowFunc() 时,console.log(this.name); 会输出 obj 对象的 name 属性的值,也就是 'Bob'

三、this 的设计与缺点

(一)this 的设计

this的设计基于函数调用方式来动态确定其指向的上下文对象,这种动态绑定机制是 JavaScript 灵活性的重要来源。

在面向对象编程中,它扮演着核心角色。比如在构造函数里,this指代正在创建的对象实例。以创建Person对象为例:

javascript 复制代码
function Person(name) {
    this.name = name;
    this.sayName = function() {
        console.log(`My name is ${this.name}`);
    };
}
const person = new Person('Alice');
person.sayName(); 

这里this让构造函数能够为每个新创建的实例对象初始化属性(如name )和方法(如sayName ),实现了数据和行为的封装。

在原型链继承中,this也发挥着重要作用。当通过原型链查找属性和方法时,this能在不同的对象层次中正确定位。例如:

javascript 复制代码
function Animal() {
    this.type = 'Animal';
}
Animal.prototype.speak = function() {
    console.log('I am an animal');
};
function Dog() {
    Animal.call(this);
    this.breed = 'Labrador';
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
    console.log('Woof! I am a dog');
};
const dog = new Dog();
dog.speak(); 

在这个过程中,this保证了在调用speak等方法时,能在正确的对象实例及其原型链上找到对应的实现,实现了代码的复用。

(二)缺点

1. 规则复杂,极易出错

JavaScript 中 this 的指向规则极为复杂,同一函数因调用方式的不同,this 指向会截然不同。

独立调用时,非严格模式下函数中的 this 指向全局对象(浏览器环境下为 window),严格模式下则为 undefined。当函数作为对象的方法调用时,this 指向调用该方法的对象。使用 new 关键字调用构造函数,this 指向新创建的实例对象。

箭头函数较为特殊,没有自身独立的 this 绑定,其 this 继承自外层作用域的 this 指向。例如:

javascript 复制代码
function logThis() {
    console.log(this);
}
logThis();           // 非严格模式下为window(独立调用)
const obj = {
    method: logThis
};
obj.method();        // 指向obj对象
new logThis();       // 指向新创建的空对象
const arrowLog = () => console.log(this);
arrowLog();          // 取决于外层作用域的this指向

开发者需时刻牢记这些复杂规则,在不同调用场景中精准判断 this 的指向,稍有疏忽就可能导致程序出错。

2. 调用场景引发的问题

在非严格模式下,函数以独立函数形式调用时,this 默认指向全局对象,这易引发全局变量污染问题。例如:

javascript 复制代码
function updateCounter() {
    this.count = 1; // 意外创建window.count
}
updateCounter();
console.log(window.count); // 输出1

若未意外创建 window.count,正常情况下 console.log(window.count) 应输出 undefined。在严格模式下,独立函数调用时 this 绑定为 undefined,执行 this.count = 1 会报错,可避免意外的全局变量污染。

函数作为参数传递,尤其在回调函数场景中,常出现上下文丢失情况。以 setTimeoutaddEventListener 为例:

javascript 复制代码
const user = {
    name: "小明",
    sayHi() { console.log(this.name) }
};
// 正确调用
user.sayHi(); // 小明
// 错误案例
setTimeout(user.sayHi, 100); // 输出undefined(this指向window)
const button = document.getElementById('button');
button.addEventListener('click', user.sayHi); // 点击时this指向按钮元素

setTimeout 接收回调函数作为参数,user.sayHi 作为回调函数传递时,不再作为 user 对象的方法调用,而是独立函数被 setTimeout 调用,非严格模式下 this 指向全局对象,导致输出 undefined

addEventListener 中,user.sayHi 作为回调函数,点击按钮时 this 指向触发事件的 DOM 元素,即按钮,同样导致输出 undefined,与期望的 this 指向 user 对象不符。

3. 特殊函数类型问题

箭头函数具有独特的 this 绑定特性,没有自身独立的 this,而是继承外层作用域的 this 指向。若在不恰当场景使用箭头函数,可能导致 this 指向与预期不符。例如:

javascript 复制代码
const button = document.getElementById('button');
const obj = {
    message: 'Hello',
    clickHandler: function() {
        button.addEventListener('click', () => {
            console.log(this.message); 
        });
    }
};
obj.clickHandler(); 

期望点击按钮时输出 Hello,但箭头函数继承了外层 clickHandler 函数的 this,而 addEventListener 的回调函数中 this 并非 obj,导致输出 undefined

若将箭头函数改为普通函数,或正确绑定 this,可避免此类问题。

4. 新手学习门槛高

与多数编程语言中 thisself 通常指向当前对象不同,JavaScript 中 this 的指向规则对新手而言具有很强的反直觉性。例如:

javascript 复制代码
const obj = {
    outer() {
        function inner() {
            console.log(this); // 指向window!
        }
        inner();
    }
};
obj.outer();

outer 函数作为 obj 对象的方法被调用,outer 函数内部 this 指向 obj。但内部函数 inner 以独立函数调用,在非严格模式下,this 指向全局对象 window,而非 obj,与新手对面向对象编程中 this 指向的常规认知相悖,增加了学习成本。

若想让内部函数的 this 指向外部的 obj 对象,可使用 bind 方法或箭头函数。如改为使用箭头函数:

javascript 复制代码
const obj = {
    outer() {
        const inner = () => {
            console.log(this); // 指向 obj
        };
        inner();
    }
};
obj.outer();

此时,inner 函数的 this 继承自外层作用域(outer 函数)的 this,所以指向 obj

四、this 指向混乱的原因

(一)动态绑定机制的复杂性

JavaScript 中 this 的指向并非在函数定义时就固定,而是依据函数的调用方式在运行时动态确定,这是导致 this 指向混乱的核心原因。

  • 独立函数调用 :独立形式调用时,非严格模式下 this 指向全局对象,浏览器环境中为 window;严格模式下 this 值变为 undefined

  • 作为对象方法调用 :当函数作为对象的方法被调用,如 obj.method() 形式,this 指向调用该方法的对象,因为函数调用时会隐式地与该对象关联。

  • 构造函数调用 :使用 new 关键字调用构造函数时,this 指向新创建的实例对象,构造函数通过 this 初始化新对象的属性和方法。

    如此繁多且依赖运行时调用方式的指向规则,开发者在编写和阅读代码时,稍有疏忽就容易对 this 的实际指向产生误解,进而导致 "规则复杂,极易出错" 的问题。

(二)箭头函数特性引发的混淆

箭头函数独特的 this 绑定特性在带来便利的同时,也容易引发 this 指向的混乱,对应 "特殊函数类型问题"。

如果开发者在不恰当的场景使用箭头函数,比如在事件回调函数或者对象方法中,就可能导致 this 指向与预期不符,就像前文 button.addEventListener 示例中,箭头函数继承外层 this 导致与预期输出不一致的情况。

(三)全局作用域污染带来的不确定性

在非严格模式的全局作用域中,独立调用的函数默认情况下 this 会指向全局对象。在复杂项目中,开发者可能因疏忽,导致 this 意外地指向全局,尤其在使用回调函数时未进行正确的绑定,从而引发全局变量污染,这与 "调用场景引发的问题" 相关。

(四)异步 / 回调场景的隐式丢失

在异步操作和回调函数场景中,this 很容易丢失原有的上下文,属于 "调用场景引发的问题" 范畴。

  • setTimeout 示例 :当函数作为 setTimeout 的回调函数传递时,this 指向会发生变化。如前文 user 对象的 sayHello 方法中使用 setTimeout 回调函数,期望输出 Hello, Bob,但实际输出 Hello, undefined,原因是 setTimeout 回调函数中的 this 指向了全局对象(非严格模式下),原有的 user 对象上下文丢失。

  • 事件处理器示例 :在事件处理中,this 默认指向触发事件的 DOM 元素。若回调函数使用箭头函数或者没有手动进行绑定,this 的指向也会发生改变。如前文中 app 对象的 handleClick 方法中,btnclick 事件回调函数使用箭头函数,预期点击按钮输出 Button clicked,但由于箭头函数继承外层 this,输出 undefined,因为 addEventListener 回调函数的 this 并非 app 对象。

(五)严格模式切换导致的不一致性

JavaScript 存在严格模式与非严格模式,这两种模式下全局作用域中的 this 指向不同。

如果代码中混合使用这两种模式,很可能会引发一些意外行为,导致 this 指向出现混乱,与 "规则复杂,极易出错" 问题相关。

在大型项目中,不同模块可能采用不同的模式,这种模式的不一致性会进一步加剧 this 指向混乱的问题。

五、改变 this 指向的 3 种方式及区别(call、apply、bind)

callapplybind这三种方法为开发者提供了灵活改变函数this指向的途径,使函数能够在特定的上下文环境中执行。

(一)call 方法

  1. 执行时机call方法会立即执行被调用的函数。

  2. 参数传递 :它的第一个参数是要绑定的this值,后续参数则是要传递给函数的参数,需逐个列出。

    • 例如,有一个函数function add(a, b) { return this.x + a + b; },我们可以通过add.call({x: 5}, 3, 2)来调用。在此例中,{x: 5}被绑定为函数执行时的this32则作为参数传递给add函数。
  3. 应用场景call方法适用于需要明确指定this指向并立即执行函数,且参数个数和顺序明确的情况。

    • 在实现继承时,它有着广泛的应用,比如在子类构造函数中调用父类构造函数并传递参数,同时指定this指向子类实例,从而实现属性和方法的继承。

    • Parent.call(this, name) 中,this 表示将 Parent 构造函数执行时的 this 指向当前正在创建的 Child 对象

javascript 复制代码
function Parent(name) {
    this.name = name;
}
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
const child = new Child('小明', 10);
console.log(child.name);  // 小明

(二)apply 方法

  1. 执行时机 :与call方法一样,apply方法也是立即执行函数。
  2. 参数传递apply方法的第一个参数同样是要绑定的this值,但其第二个参数是一个数组,数组中的元素即为要传递给函数的参数。
    • 以之前的add函数为例,可使用add.apply({x: 5}, [3, 2])进行调用,这里[3, 2]作为参数数组传递给add函数。
  3. 应用场景 :当需要传递一个数组作为函数参数,并且希望立即执行函数时,apply方法尤为方便。
    • 例如,在使用Math.max函数求数组中的最大值时,由于Math.max本身不直接支持数组作为参数,此时就可以借助apply方法,如Math.max.apply(null, [1, 5, 3]),将数组元素作为参数传递给Math.max函数。

另外,假设我们有一个函数用于计算多个数的平均值:

javascript 复制代码
function average() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}
const numbers = [2, 4, 6, 8];
// 使用apply将数组作为参数传递给average函数
const result = average.apply(null, numbers);
console.log(result); 

在这个例子中,average函数原本期望接收多个独立参数,但我们有一个包含数据的数组numbers。通过apply方法,我们将数组numbers作为参数传递给average函数,从而顺利计算出数组元素的平均值。

此外,在一些需要将数组元素逐个作为参数传递给其他函数的场景中,apply方法也能派上用场。

(三)bind 方法

  1. 执行时机bind方法不会立即执行函数,而是返回一个新函数,只有当调用这个新函数时,原函数才会执行。
  2. 参数传递bind方法的第一个参数是要绑定的this值,后续参数可以在绑定this时传递,也可以在调用返回的新函数时传递。
    • 例如,const newAdd = add.bind({x: 5}, 3),这里将this绑定为{x: 5},并传递了一个参数3。之后调用newAdd(2)时,效果等同于执行原add函数并传入32作为参数。
  3. 应用场景bind方法常用于需要将函数的this绑定为特定值,然后在后续某个时刻再执行函数的情况。在事件绑定中,由于事件触发时this指向可能不符合预期,使用bind可以预先绑定正确的this指向。比如:
javascript 复制代码
const button = document.getElementById('button');
const obj = {
    message: 'Button clicked',
    handleClick: function() {
        console.log(this.message);
    }
};
button.addEventListener('click', obj.handleClick.bind(obj));

在异步操作中,当需要在某个特定时刻执行函数时,也可以使用bind提前绑定this和部分参数,方便后续调用。

(四)三种方法的共同点

callapplybind这三种方式的核心共同点在于都能够强制指定函数执行时的this指向,让函数在开发者期望的特定上下文环境中执行,从而实现更灵活的编程逻辑和代码复用。

(五)应用场景总结

  1. call 和 apply :适合需要一次性调用函数并改变this指向的场景。在实现继承时,通过callapply让子类调用父类的构造函数,是一种常见且有效的方式。在处理数组相关操作时,若某个函数原本不支持数组参数,apply可以巧妙地传入数组参数,满足特定需求。
  2. bind :适用于需要延迟执行函数或者作为事件回调的场景。在事件处理中,使用bind预先绑定正确的this指向,能确保事件处理函数在触发时this指向符合预期。在异步操作中,提前使用bind绑定this和部分参数,为后续特定时刻执行函数做好准备,增强了代码的可控性。
相关推荐
朴拙数科20 分钟前
技术长期主义:用本分思维重构JavaScript逆向知识体系(一)Babel、AST、ES6+、ES5、浏览器环境、Node.js环境的关系和处理流程
javascript·重构·es6
拉不动的猪1 小时前
vue与react的简单问答
前端·javascript·面试
污斑兔1 小时前
如何在CSS中创建从左上角到右下角的渐变边框
前端
牛马baby1 小时前
Java高频面试之并发编程-02
java·开发语言·面试
星空寻流年1 小时前
css之定位学习
前端·css·学习
旭久2 小时前
react+antd封装一个可回车自定义option的select并且与某些内容相互禁用
前端·javascript·react.js
是纽扣也是烤奶2 小时前
关于React Redux
前端
阿丽塔~2 小时前
React 函数组件间怎么进行通信?
前端·javascript·react.js
yuanbenshidiaos2 小时前
面试问题总结:qt工程师/c++工程师
c++·qt·面试