彻底搞懂JavaScript中this指向
在面向对象的编程语言中几乎都有类似this的关键字,如Java、C++、Swift中的this,Python、Object-C中的self,但是Javascript中的this和它们还不太一样,面向对象的编程语言中this通常会出现在类方法中,代表着当前调用对象,而Javascript中的this则更加灵活复杂。JavaScript中this是一个很特别的关键字,被自动定义在所有函数的作用域中,MDN中提到JavaScript的this是当前执行上下文的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。那么,JavaScript中到底为什么要用到this?它的this又是以一种什么样的机制进行绑定的呢?
一、为什么使用this
我们看下面这样一段代码,如果没有this,我们想要获取到这个对象的的名字必须通过对变量的引用Tom.name
来实现,这种显式引用的方法显然是不合适的,如果有一天需要对变量名称进行修改,那么对象中的引用也必须全部进行修改。
var Tom = {
name: 'Tom',
speaking: function () {
console.log(`${Tom.name} say boh`)
},
language: function () {
console.log(`${Tom.name}'s mother tongue is Chinese`)
},
hobby: function () {
console.log(`${Tom.name} love eating, sleeping and playing peas`)
},
}
当我们有了this以后,完全可以以一种更加优雅的方式进行引用,使得代码更加简洁,复用性更强。实际上,this给我们带来的便利远不止这么简单。
var Tom = {
name: 'Tom',
speaking: function () {
console.log(`${this.name} say boh`)
},
language: function () {
console.log(`${this.name}'s mother tongue is Chinese`)
},
hobby: function () {
console.log(`${this.name} love eating, sleeping and playing peas`)
},
}
二、this到底指向什么
2.1 两个误解
2.1.1 误解一:指向自身
以下面这个计数器为例:输出的结果是0而不是10,显然在这个例子中this并不指向它自身
function counter() {
this.count++
}
counter.count = 0
for (let i = 0; i < 10; i++) {
counter(i)
}
console.log(counter.count) // 0
2.1.2 误解二: 指向其作用域
以下面这个函数嵌套调用为例:输出的结果是undefined,而并没有输出作用域上的值
function func1() {
var a = 1
func2()
}
function func2() {
var a = 2
console.log(this.a)
}
func1() // undefined
2.2 this绑定机制
事实上,this是在函数运行时进行绑定的,它和this的定义位置没关系,但是和this的调用位置和调用方式有关,他有四种绑定方式,即默认绑定、显式绑定、隐式绑定、new绑定。
2.2.1 默认绑定
在函数独立调用时会使用到默认绑定规则,可以认为它是不符合其他绑定规则之后最后的决定,在非严格模式下,默认绑定会将this绑定到全局变量window
上,而在严格模式中,this会绑定到undefined
。(注:此处不考虑nodejs)
a = 1
function func1() {
console.log(this.a)
}
function func2() {
'use strict'
console.log(this.a)
}
console.log(this)
func1() // 1
func2() // TypeError: Cannot read properties of undefined (reading 'a')
2.2.2 隐式绑定
如果函数是通过某个对象进行调用,那么this会指向调用函数的对象。
let obj = {
showThis: function () {
console.log(this===obj)
}
}
obj.showThis() // true
那么如果进行了链式调用呢?看下面这个例子,显而易见,在这种链式调用中只用最后一层调用在起作用。
let obj = {
showThis: function () {
console.log(this===obj)
},
}
let obj2 = {
obj: obj,
}
obj2.obj.showThis() //true
当然不是通过这种.
调用的方式就会绑定到.
前面的对象上,值得注意的是,有时候会存在隐式丢失的情况,下面这个例子中输出的是window
而不是obj
,因为在进行传递时丢失了隐式绑定,传入的可以看作是函数本身,和obj
没有任何关系。
function func1() {
console.log(this)
}
var obj = {
func: func1,
}
var func2 = obj.func
func2() // window
2.2.3 显式绑定
隐式绑定有诸多的限制条件,在对象的内部必须有一个指向函数的属性,并通过这个属性间接的引用函数,从而把this绑定到对象上,那么如果我们不想在对象的内部包含这样一个函数引用,又想把this绑定到这个对象上进行函数调用该怎么办呢?这个时候可以使用 call
、apply
或者bind
方法进行强制绑定,需要注意的是,这里的绑定和对象的[[Prototype]]
相关,因此箭头函数是无法进行显式绑定的。
function func() {
console.log(this);
}
func.call(window); // window
func.apply({}); // {}
func.bind(1)(); // Number {1} 这里是数字类型的对象1
这里使用bind绑定又叫做硬绑定,可以有效的解决隐式绑定丢失的问题,其已在JavaScript内部实现,其内部实现原理非常简单:
function bind(func, obj) {
return function() {
return func.apply(obj, arguments);
}
}
在很多地方中,都会给出多一个参数进行显式绑定,如setTimeout
、forEach
等,其内部也都是通过call
或apply
实现的,可以有效的帮助我们少写点代码
2.2.4 new绑定
使用new关键字进行函数调用通常被称为构造函数调用,实际上在JavaScript中除了箭头函数外都可以通过这种方法调用,调用时会自动执行以下操作:
- 创建一个全新的对象
- 这个新对象会被执行
Prototype
连接 - 这个新对象会绑定到函数调用的this上
- 如果函数没有返回其他对象,表达式会自动返回这个新对象
function Person(name) {
console.log(this);
this.name = name;
}
var tom = new Person('Tom'); // Person {}
console.log(tom); // Person {name: 'Tom'}
三、绑定优先级
如果一个函数调用位置、方法中使用了上述四条规则中的多种会使用那种优先级呢?
3.1 优先级是什么
答案是:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
3.2 显式绑定 > 隐式绑定
输出的是obj2说明显式绑定生效了,隐式绑定没有生效
function foo() {
console.log(this);
}
var obj1 = {
foo: foo
}
var obj2 = {
foo: foo
}
obj1.foo.call(obj2); // function foo() {
console.log(this)
}
var obj1 = {
name: 'obj1',
foo: foo,
}
var obj2 = {
name: 'obj2',
foo: foo,
}
obj1.foo.call(obj2) // {name: 'obj2', foo: ƒ}, 说明 显式绑定 > 隐式绑定
3.3 new绑定 > 显式绑定
new与call、apply是同时使用在JavaScript中时不允许的,会报错TypeError
,但是可以和bind硬绑定后的函数同时使用,这里输出的是foo,说明new绑定生效了
function foo() {
console.log(this)
}
var obj = {
name: "obj"
}
var bar = foo.bind(obj)
var baz = new bar() // foo {}, 说明 new绑定 > 显式绑定
四、永远有一些意外
4.1 被忽略的显式绑定
如果在显示绑定中,传入一个null
或者undefined
,那么这个显示绑定就会被忽略,使用默认绑定。
function foo() {
console.log(this)
}
foo.call(null) // window
那么如果就是想不把this绑定到任何对象上我们该怎么做呢?显式绑定一个空对象即可,注意是空对象不是null,它的创建方法为:Object.create(null)
4.2 间接引用
在使用函数的间接引用时也会使用默认绑定规则,什么是间接引用可以先从简单的例子看起:
var num1 = 2
var num2 = 3
var res = (num2 = num1)
console.log(num1, num2, res) // 2 2 2
对于函数,同样是这种形态,称之为间接引用
function foo() {
console.log(this)
}
var obj1 = {
name: "obj1",
foo: foo
};
var obj2 = {
name: "obj2"
}
obj1.foo();
(obj2.foo = obj1.foo)(); // window
五、总结
那么,我们最终如何判断this指向呢?通常通过以下几步即可:
- 判断是否为箭头函数,箭头函数的this总是指向其最近的执行上下文
- 判断是否为new绑定,new绑定优先级最高,this指向new出来的对象
- 判断是否为显式绑定,this指向绑定的对象
- 判断是否为隐式绑定,this通常指向
.
前面那个调用函数的对象 - 最后考虑是否为默认绑定,根据JavaScript执行模式判断为window或这undefined
特殊情况需考虑隐式丢失、显式绑定null、间接引用情况
参考书籍:《你不知道的JavaScript上卷》