REC

揭开迷雾:全面解析 JS 中的 this 指向问题

易航
1年前发布 /正在检测是否收录...

一、简介

身为一个使用JavaScript的开发者,我们或多或少在开发中都遇到过因 this 指向不明导致的代码错误。函数中this 关键字的指向是一个复杂但又关键的概念,大多数时候它的指向不是在编写时确定的,而是在代码执行时根据函数的调用方式来决定,简单来说,this 的指向取决于函数的调用者(箭头函数除外)。

本文将全面详细的讲解this的指向规则,让你能够清晰理解并正确使用 this,避免由于错误的指向导致的程序异常,并提升对JavaScript的整体理解。

二、详解

1、 全局作用域

在全局作用域下(非严格模式),也就是最外层的JS脚本中,this指向全局对象(浏览器中为window对象)。

<script>
    console.log(this === window); // 浏览器中输出 true
</script>

2、普通函数

在调用一个独立的普通函数时(非严格模式),此时相当于通过全局对象来调用函数,this会进行默认绑定,指向全局对象(浏览器中为window对象)。

重点:普通函数中的 this 指向是动态的,取决于函数的调用方式,会因为调用方式的不同而发生改变。

// 非严格模式下 
// 两个普通函数
function showThis() {
  console.log(this);
}
const showThis2 = function() {
   console.log(this);
}
// 调用普通话函数
showThis(); // 输出 window 对象
showThis2(); // 输出 window 对象
// 相当于通过window来调用函数 因此this指向window
window.showThis(); // 输出 window
window.showThis2(); // 输出 window

如果让一个对象内部的方法指向普通函数,并通过该对象来调用函数,则this指向发生改变,指向调用函数的对象,谁进行调用,this就指向谁。

// 普通函数
function showThis() {
  console.log(this);
}
// 创建对象 并让对象的方法指向函数
const obj = {
  getThis: showThis
};
// 通过对象调用函数
obj.getThis(); // 输出 obj

3、箭头函数

箭头函数不会创建自己的this,其内部的this会继承函数定义时其外层最近的非箭头函数的 this 指向。如果是在全局作用域内使用箭头函数,则this指向全局对象;如果是在构造函数中使用箭头函数,则this指向new出来的新对象...

重点:箭头函数中的 this 指向是静态的,在函数定义时确定,后面不会因为调用方式的不同而发生改变,也无法被call/apply/bind等方法改变。

// 非严格模式下
// 箭头函数 this指向window
const showThis3 = () => {
   console.log(this);
}
// 调用箭头函数
showThis3(); // 输出 window 对象

如果让一个对象内部的方法指向箭头函数,并通过该对象来调用函数,此时虽然函数的调用者发生变化,但this指向不会改变,依旧指向全局对象。


// 箭头函数
const showThis3 = () => {
  console.log(this);
}
// 创建对象 并让对象的方法指向函数
const obj = {
  getThis: showThis3
};
// 通过对象调用函数 但this指向不会改变
obj.getThis(); // 输出 window

4、调用对象的方法

在通过对象直接调用其内部的方法时,方法中的this 会进行隐式绑定,指向当前对象。

const obj = {
  getThis: function() {
    // 对象的方法中的this 指向当前对象
    console.log(this)
  }
};
// 通过对象调用方法 this指向当前对象
obj.getThis(); // 输出 obj

如果先用一个变量指向对象的方法,然后再通过变量来调用对应的方法时,this的指向将不再指向对象,而是指向全局对象(浏览器中为window对象)。因为此时相当于进行独立函数调用,遵守独立函数的默认绑定规则。

<script>
    const obj = {
        getThis: function () {
            console.log(this)
        }
    };
    // 通过对象调用方法 this指向对象本身
    obj.getThis(); // 输出 obj
    // 使用变量指向对象的方法 但不调用
    const fun = obj.getThis;
    // 通过变量调用方法 此时相当于独立函数调用 所以this指向window
    fun(); // 输出 window
</script>

如果使用一个对象的方法指向另一个对象的方法,此时两个对象都可以调用该方法,那么具体的this指向取决于调用者,谁进行调用, this就指向谁。

<script>
    const obj = {
        getThis: function () {
            console.log(this)
        }
    };
    // 通过obj调用方法 this指向obj本身
    obj.getName(); // 输出 obj
    // 在另一个对象中使用属性指向前面对象的方法
    const obj2 = {
        // 指向obj的方法
        get2: obj.getThis
    }
    // 通过obj2调用方法 此时this指向obj2
    obj2.get2(); // 输出 obj2
</script>

还存在一种特殊情况:如果对象的方法使用的是箭头函数,那么方法中的 this 将继承定义对象时所在作用域的this指向,而非对象本身,并且后续不会因为调用者改变而改变this指向。

<script>
// 如果对象的方法使用的是箭头函数 那么this将继承定义对象时所在作用域的this指向
// 在全局作用域下定义对象
const obj = {
    getThis2: () => {
        // 此时箭头函数中的this 指向全局对象window
        console.log(this)
    }
}
// 虽然是通过obj调用的 但是其this还是会指向全局对象window
obj.getThis2(); // 输出 window
// 使用变量指向对象的方法 但不调用
const fun = obj.getThis2;
// 通过变量调用方法 this依旧指向window
fun(); // 输出 window
// 在另一个对象中使用属性指向对象的方法
const obj2 = {
    // 指向obj的方法
    get2: obj.getThis2
}
// 通过obj2调用方法 此时this依旧指向window
obj2.get2(); // 输出 window
</script>

5、调用构造函数

在使用new关键字调用构造函数时,会创建一个新的对象,此时this 会指向被创建的新对象。内部的普通函数和箭头函数内的 this 也都指向被创建的新对象,但后续普通函数的this指向是会根据调用者不同而发生改变,箭头函数则不会改变。

// 构造函数
function Person(name) {
    // 构造函数中的this指向通过new创建的新对象
    this.name = name;
    // 普通函数
    this.getThis = function () {
        console.log(this)
    }
    // 箭头函数
    this.getThis2 = () => {
        console.log(this)
    }
}
// 调用构造函数创建新对象
const person = new Person('Bob');
// 通过新对象调用方法 此时this指向新对象
person.getThis(); // 输出 person
person.getThis2(); // 输出 person
// 在另一个对象中使用属性指向new出来的新对象的方法
const obj = {
    getThis: person.getThis,
    getThis2: person.getThis2
}
// 通过obj调用方法 此时this指向会根据函数类型不同而不同
obj.getThis(); // 输出 obj
obj.getThis2(); // 输出 person

  还存在一种特殊情况,就是Function()构造函数,在使用该构造函数创建新函数时,其内部的this指向并非指向新函数本身,而是根据函数被调用的方式来决定this指向,谁调用this就指向谁。

// 非严格模式
// 在全局作用域下
const func = new Function('console.log(this)');
// 相当于通过window来调用函数 因此this指向window
func(); // 输出 window 对象

// 在对象中
const p = {
    name: 'Alice',
    get: func
}
// 通过对象中调用函数 对象内的this指向对象本身
p.get(); // 输出 Person { name: 'Alice', get: [Function] }

6、事件处理函数

在事件处理函数中,如果绑定的函数是普通函数,则相当于由DOM元素调用函数,则函数内的this指向触发事件的DOM元素;如果绑定的是箭头函数,则函数内的this会继承绑定事件处理函数所在作用域的this指向。

<button id="btn">Click me</button>
<!-- 内联的事件处理器也一样 -->
<button onclick="console.log(this)">Click me</button>

<script>
    const btn = document.getElementById('btn');
    // 绑定普通函数 相当于dom调用函数 所以this指向button元素
    btn.addEventListener('click', function () {
        console.log('普通函数this--', this); // 输出 button 元素
    });
    // 绑定箭头函数 继承绑定事件处理函数所在作用域的this 此时为全局作用域 所以this指向window
    btn.addEventListener('click', () => {
        console.log('箭头函数this--', this); // 输出 window
    });
</script>

7、回调函数

当把一个函数作为回调函数传递后,其this指向取决于回调函数的调用方式和函数的类型两者:

  • 如果函数类型为普通函数且被传递后是直接独立调用的,此时相当于独立函数调用,函数内部this指向全局对象(浏览器中为window对象)。
  • 如果函数类型为普通函数且被传递后是通过别的对象进行调用的,此时相当于通过对象调用函数,函数内部的this指向函数的调用者。
  • 如果函数类型为箭头函数,则无论被传递后是如何进行调用的,都不会修改函数内部的this指向,箭头函数内部的this指向在函数定义后确定就不会再被改变。
const obj = {
    // 普通函数      
    showThis: function () {
        console.log(this);
    },
    // 箭头函数
    showThis2: () => {
        console.log(this);
    }
}
function fun(fn) {
    // 直接独立调用回调函数
    fn();
}
function fun2(fn) {
    // 通过对象调用回调函数
    const a = {
        getThis: fn,
    }
    a.getThis();
}
// 对象普通函数中的this指向对象本身
obj.showThis(); // 输出 obj
// 对象箭头函数中的this继承定义对象时所在作用域的this指向 此时为window
obj.showThis2(); // 输出 window
// 传入一个普通函数 并且直接调用函数 相当于独立函数调用 所以this指向window
fun(obj.showThis); // 输出 window
// 传入一个箭头函数 并且直接调用函数 this指向不受影响 依旧为window
fun(obj.showThis2); // 输出 window
// 传入一个普通函数 并且通过对象调用函数 this被改变 指向其调用者
fun2(obj.showThis); // 输出调用者对象 a
// 传入一个箭头函数 并且通过对象调用函数 this指向不受影响 依旧为window
fun2(obj.showThis2); // 输出 window

数组提供的内置方法中,除了传递回调函数之外,还可以通过第二个参数指定回调函数的this指向。如果未指定this的指向,则取决于函数的调用环境内的this指向。

// 创建对象
const obj = {
    multiplier: 2
};
// 创建数组
const numbers = [1, 2, 3];
// 使用数组提供的map方法 第一个参数是回调函数 第二个参数是指定回调函数的this指向
const doubled = numbers.map(function (number) {
    // 回调函数中的this指向obj 所以this.multiplier指向obj.multiplier 值为2
    return number * this.multiplier;
}, obj);
// 最终运算结果为[2, 4, 6]
console.log(doubled);
// 未指定this指向 回调函数中的this指向window
const doubled2 = numbers.map(function (number) {
    return this;
});
// 最终运算结果为[window, window, window]
console.log(doubled2);

8、call/apply/bind 显式指定

call()apply()bind()三个方法都可以显式的指定函数的this指向,只是在一些具体细节上有所不同。例如:call()apply() 方法会在改变函数内部this指向的同时,调用函数执行一次。而bind() 不会调用函数执行,仅改变函数的this指向;在向函数传递参数时,call()bind() 传递的参数都是用逗号隔开的独立参数,而apply()是以数组的形式来传递参数。

// call方法
function greet(text) {
    console.log(`${text}, ${this.name}`);
}
const user = { name: 'Charlie' };
// 使函数的this指向user 且会调用执行一次
greet.call(user, 'Hello'); // 输出 'Hello, Charlie'

// apply方法
function introduce(age, gender) {
    console.log(`Name: ${this.name}, Age: ${age}, Gender: ${gender}`);
}
const user2 = { name: 'Dana' };
// 使函数的this指向user 且会调用执行一次
introduce.apply(user2, [25, 'Female']); // 输出 'Name: Dana, Age: 25, Gender: Female'

// bind方法
const module = {
    x: 42,
    getX: function () {
        return this.x;
    }
};
// 此时指向对象的方法 但不调用执行
const retrieveX = module.getX;
// 调用执行 此时的 this 指向window 因为相当于独立函数调用
console.log(retrieveX()); // 输出 undefined
// 修改this指向 使函数的this指向module 且不会调用执行
const boundGetX = retrieveX.bind(module);
// 手动调用执行一次
console.log(boundGetX()); // 输出 42

三、课后练习

1、下面代码的执行结果是什么?

// 全局作用域 且非严格模式
[1, 2, 3].forEach(function (item) {
    console.log(this);
});

2、下面代码的执行结果是什么?

// 全局作用域 且非严格模式
const obj = {
    a: () => {
        console.log(this);
    },
    b: function () {
        console.log(this);
    }
};

obj.a();
obj.b();

3、下面代码的执行结果是什么?

// 全局作用域 且非严格模式
function Person(name) {
    this.name = name;
    this.getName = () => {
        return this.name;
    };
}

const person1 = new Person('Alice');
const person2 = { name: 'Bob' };
person2.getName = person1.getName;
console.log(person2.getName());

欢迎大家在评论区揭晓答案

© 版权声明
本站用户发帖仅代表本站用户个人观点,并不代表本站赞同其观点和对其真实性负责。
转载本网站任何内容,请按照转载方式正确书写本站原文地址。
THE END
喜欢就支持一下吧
点赞 0 分享 赞赏
评论 抢沙发
取消 登录评论