前言
this 到底指向谁 , 看完本文就好受了 !
注意
本文样例主要在 node.js 环境中测试 , 但由于以下特性会导致结果不同 , 倔友请先食用。
- 在浏览器全局环境中,使用
var声明的变量会挂载在window上 , 成为 window 的属性,但let、const声明的变量,不会挂载在window上 。 - 但是 node.js 全局环境中 , var 声明的变量不会挂载在 global 上 。
所以当 this 指向全局对象 window 和 global 时 , 可能会导致代码结果不一样 , 所以啊 , 面试官给你 js 代码的时候 , 要问清楚是在什么环境下执行 , 尤其是涉及到 var 定义的变量的时候 !!!
比如以下例子 :
- 浏览器中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var a = 111
let b = 222
const c = 333
console.log(window.a) // 111
console.log(window.b) // undefined let、const 声明变量没有挂载在 window 上
console.log(window.c) // undefined const 声明变量没有挂载在 window 上
</script>
</body>
</html>
- node中
var a = 111
let b = 222
const c = 333
console.log(global.a) // undefined
console.log(global.b) // undefined let、const 声明变量没有挂载在 window 上
console.log(global.c) // undefined
所以后面 , 我们分析完 this 指向 , 讨论代码的结果的时候 , 会分为两种环境来讨论 !
this 指向看什么 ?
this 的指向是动态的 , 在函数定义的时候不能确定指向谁 , 在调用的时候才能确定 this 的指向 , 而在调用的时候主要看调用的位置 , 以下对调用的位置讨论 。
调用位置是函数在代码中调用的位置 , 而不是声明的位置。
调用位置分为以下几种情况:
- 普通函数调用:在全局环境中调用函数
- 对象方法调用:通过对象的方法调用
- 构造器调用:使用
new运算符实例化调用 - 显式调用:通过
call、apply、bind调用,修正this指向
注意
箭头函数的 this 指向是固定的,在函数定义时就确定了,之后不会再改变。
箭头函数没有自己的 this 值,它会捕获上下文中的 this 作为自己的 this。无论箭头函数是如何调用的,它的 this 始终保持不变。
箭头函数在处理回调函数时特别有用,因为它避免了常见的 this 绑定问题。
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
console.log(this.seconds); // 这里的 `this` 始终指向 Timer 的实例
}, 1000);
}
const timer = new Timer();
普通函数与箭头函数的区别可以参考这篇文章 : juejin.cn/post/743070…
普通函数调用
普通函数调用, this 指向全局对象。
- 在浏览器
JS引擎中this指向window - 在
Nodejs环境this指向global
function f1(){
return this;
}
// 在浏览器中,全局对象是 window
//console(f1() === window) // true
//在Node中,全局对象是 global
console.log(f1() === global) // true
以上是在 nodeJs 的环境中测试
注意
- 在严格模式下,
this指向undefined
function f1(){
// 严格模式下,全局对象是 undefined
'use strict'
return this;
}
console.log(undefined===f1())
node
浏览器
对象方法调用
当函数作为对象的方法被调用时, this 指向该对象:
var obj = {
name: 'John',
getName: function () {
console.log(this === obj)
console.log(this.name === 'John')
// console.log(this)
// console.log(this.name)
return this.name
}
}
obj.getName();
node
浏览器
注意
使用对象方法调用时,this 有可能会丢失,看下面这段代码
var name = '李四'
var obj = {
name: '张三',
getName: function () {
function fn () {
console.log(this === global)
return this.name
}
return fn()
}
}
console.log(obj.getName())
node
因为在 getName 函数内部调用 fn, 此时 fn 函数执行上下文this不是指向调用的对象 obj,而是指向 global , 由于 var 的变量不会挂载在为 global的属性 , 所以输出 undefined
浏览器
将代码中的 global改为 window , 输出结果如下 :
与 node 环境原理一致 , 不是指向调用的对象 obj,而是指向 window , 但是 var 声明的 name ,挂载为 window 的属性 , 所以 this.name 就相当于 window.name , 所以输出的是 李四
构造器调用
构造器表面和普通函数一模一样,不同的地方在于被调用的方式。
使用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向这个对象
举个栗子 :
var Name = function () {
this.name = 'ganzhibin'
}
var obj = new Name()
console.log(obj.name) // ganzhibin
使用 new 运算符创建 Name 构造器,此时this 指向 obj
node
浏览器
注意
使用 new 调用构造器时,还要注意一个问题,如果构造器显式返回一个 object 类型的对象,那么此次运行结果最终是返回这个对象,而不是我们之前期待的 this:
var Name = function () {
this.name = 'ganzhibin'
return { // 显示返回一个对象
name: 'this is myName'
}
}
var obj = new Name()
// 输出 this is myName,而不是上面的 ganzhibin
console.log(obj.name) // this is myName
如果构造器不显式返回任何数据,或者返回一个非对象类型的数据,就不会存在上面这个问题
var Name = function () {
this.name = 'ganzhibin'
}
var obj = new Name()
console.log(obj.name) // ganzhibin
node 和 浏览器 结果一致
显式调用
call、apply 修正 this 指向
call 和 apply 调用函数和其他函数调用相比,它会显式改变传入函数的 this, 指向第一个传入的参数。call 和 apply 两者实现功能相同, 不同的地方在于接收参数形式不一样
call接收的是参数列表apply接收的是一个数组
var obj1 = {
name: 'name1',
getName: function () {
return this.name
}
}
var obj2 = {
name: 'name2'
}
// 对象方法调用,所以 this 指向 obj1
console.log(obj1.getName())
// 使用 call 显示调用,改变了原来 this 指向,指向了 obj2
console.log(obj1.getName.call(obj2))
node
浏览器
注意
call 、apply 第一个参数除了可以是对象引用类型,也可以是基本类型:
null或undefined:this指向window;不过,在严格模式下,this还是指向undefined。number、string、boolean:this会指向其内置构造函数Number、String、Boolean
var name = 'globalName'
var obj = {
name: 'obj name',
getName: function () {
// 'use strict' // 严格模式下,this 指向 undefined, null 会报错
return this.name
},
getThis: function () {
return this
}
}
obj.getName.call(null) // globalName
obj.getName.apply(undefined) // globalName
// number boolean string
obj.getThis.call(111) // Number {111}
obj.getThis.call(true) // Boolean {true}
obj.getThis.call('str') // String {"str"}
这里注意区分浏览器环境和 node 环境
node
浏览器
bind 修正 this 指向
bind 和 call 、apply 不同的地方在于
- 改变
this指向 bind()的参数 - 同时会返回一个新的函数 , 比如下面的 g() , h()
- bind只生效一次
function f(){
return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty
var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37(对象方法调用,指向o), azerty, azerty
bind 绑定改变 this 只能生效一次,如果链式发生多次绑定以第一次为准 !
node
浏览器
因为这里的 this 没有指向全局对象 window 或者 global , 所以结果一般是一致的 !
事件处理器
在为 DOM 元素添加事件监听器时,通常事件处理函数中的 this 会指向触发事件的 DOM 元素,而不是定义函数的对象。
Javascript
代码解读
复制代码
const button = document.querySelector('button');
button.addEventListener('click', function() {
console.log(this); // 指向 <button> 元素
});
总结
-
当 this 指向全局对象window 或者 global ,且 this 选取的是 var 声明的变量时 , 代码输出的结果 可能不一致 。
-
箭头函数的 this , 固定的,在函数定义时就确定了,之后不会再改变 。
-
除了箭头函数 , this 的指向取决于函数调用的位置 , 其中调用的位置有
- 普通函数调用:在全局环境中调用函数
- 对象方法调用:通过对象的方法调用
- 构造器调用:使用
new运算符实例化调用 - 显式调用:通过
call、apply、bind调用,修正this指向 - 事件处理器
你还行吗 ? 👀
补充栗子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// var name = 'windowName'
// var a = {
// name : 'lisi',
// fn1 : function(){
// console.log(this.name)
// },
// fn2 : function(){
// let _this = this ; // this指向
// setTimeout(function(){
// _this.fn1() //
// },100)
// }
// }
// var name = 'windowName'
// var a = {
// name : 'lisi',
// fn1 : function(){
// console.log(this.name)
// },
// fn2 : function(){
// setTimeout(() =>{
// this.fn1() // 指向词法环境中的this
// },100)
// }
// }
var name = 'windowName'
var a = {
name : 'lisi',
fn1 : function(){
console.log(this.name)
},
fn2 : function(){
setTimeout((() =>{
this.fn1() // 指向词法环境中的this
}).call(a),100)
}
}
a.fn2()
</script>
</body>
</html>
上面三种方法实现的效果一样 , 都是指向a对象 .