JavaScript中的this,既是难点也是重点。痛苦的来说,不学又不行,学着又头痛。
虽然现在React、Vue3都是hook的思维模式,函数式编程,减少了对this的使用(少了很多的痛苦)。但是对于开发者而言,在平时编写代码的过程中,有着面向对象编程思维,this还是经常打交道的。
本篇就来彻底的聊一聊this的一系列规则,希望你有所收获。
为什么需要this?
this是必须的吗?答案是否定的,可以通过其它的方式,来实现this相同的功能。
示例:
var obj = {
name: "copyer",
getName: function () {
console.log(obj.name); // 代替:console.log(this.name)
},
setName: function (str) {
obj.name = str; // 代替:this.name = str
},
};
通过上面的代码可以看出,getName和setName方法中拿取对象obj中的name属性,可以通过 this 拿取,也可以通过obj.name 直接拿取。所以this并不是代码中的必需品。
那为什么又需要 this 呢?
想一下,如果把对象名 obj 改为 newObj,那么在上面的代码中,就需要三行代码都需要改动(如果代码量大的话,就可能存在非常多的地方需要更改)。但是如果使用了 this,就算改变了属性名,但是对其中内部代码是影响范围不是很广的,修改的代码就相对地少(甚至没有)。
小结:
- 从某些角度来说,没有this,通过其他的方式也能解决,但是会使代码变得比较的难维护。
- 如果使用了this,那就增加了学习成本。
- 需不需要使用this,看你咯,哈哈哈。
全局作用域中的this
this 存在于两个地方:全局作用域和函数作用域。大多数情况下,this出现于函数作用域中。
那么全局作用域下的this,只需要记住即可:
- 浏览器: this指向
window对象 - node环境: this指向
{}, node环境不存在window对象
在下面的内容,都是针对函数作用域中的this。
函数中this的指向?
直接说结论:this的指向跟函数定义的位置是没有关系的,跟函数的调用方式是有关系的(当前箭头函数除外,下面会讲到)。
函数在被调用的时候,就会在函数调用栈创建一个函数执行上下文,在函数编译阶段,就开始确定了this的指向。
在函数执行的时候,函数会经历两个阶段:编译阶段和执行阶段。
在编译阶段的时候,才确定了this的指向,在执行阶段使用的this就是根据编译阶段而来的。
代码演示
function foo() {
console.log(this)
}
// 调用方式一:
foo() // this: window
// 调用方式二:
var obj = {foo: foo}
obj.foo() // this: obj
// 调用方式三:
foo.apply('abc') // this: String('abc')
定义了一个函数foo,通过三种不同的方式调用,那么this的指向就不同。从而验证了上面的结论。
函数中this的绑定规则
既然知道了函数是在执行的时候,才确定this的指向。那么就来聊一聊this的四种绑定规则吧。
规则一:默认绑定
就是函数独立调用的时候:就没有绑定到某个对象上进行调用。
那么this的指向就是window。
// 案例一:
function foo() {
console.log(this) // window
}
foo()
// 案例二:
var obj = {
name: 'copyer',
bar: function() {
console.log(this) // window
}
}
var fn = obj.bar
fn()
规则二:隐式绑定
通过对象对函数进行调用,那么这时候的this就指向调用的对象
// 案例一:
var obj = {
name: 'copyer',
foo: function() {
console.log(this) // obj
}
}
obj.foo()
// 案例二:
var obj1 = {
name: 'obj1',
foo: function() {
console.log(this) // obj1
}
}
var obj2 = {
name: 'obj2',
obj1: obj1
}
obj2.obj1.foo()
规则三:显示绑定
通过 apply、call、bind 三个函数,手动指定 this 的指向。
// 案例一:
function foo() {
console.log(this);
}
foo.apply(window); // window
foo.call("123"); // String('123')
foo.bind(123)(); // Number(123)
小知识点:
相同点
- 所有函数都可以调用三个函数,因为这三个函数在原型链上(
Prototype)。- 三个函数的第一个参数,内部都看成一个对象,this就专指该对象。
不同点:
- call 和 apply 的第一个参数相同,apply 函数的第二个参数是数组(数组里面的内容分别是传递过来的参数),call 函数从第二个参数开始,传递过来的参数就是依次有序的写下去。
- bind 函数就是返回一个新的函数,用于被执行。
规则四:new 绑定
JavaScript中的函数可以当做成一个类的构造函数来使用,也就是使用 new 关键词。
new 内部实现原理
- 创建一个空对象
{} - 获取构造函数
- 链接到原型
- 绑定this,执行构造函数
- 返回新对象
function mockNew() {
let obj = {}
// 获取构造函数
let con = arguments.__proto__.constructor
// 连接原型
obj.__proto__ = con.prototype
let res = con.apply(obj, arguments)
// 返回一个新对象
return typeof res === 'object' ? res : obj
}
// https://blog.csdn.net/qq_22841387/article/details/123345400
从 mockNew 内部的实现,this的指向也是通过显示绑定(apply)实现的,this的指向就是创建的新对象
// 案列一:
function Person(name) {
this.name = name // 里面的this,就是指向实例化的person对象
}
var person = new Person('copyer')
小结
this的四种绑定规则,就是这样,也不是很难理解吧。this的四种规则足以对付绝大数函数的this指向了。理解好了四种绑定规则,再去看下面的内容哟。
规则之间的优先级
对于一个函数而言,在调用的时候,如果有多个绑定规则聚集一身,那么this该听谁的呢?谁的拳头大就指向谁(优先级)。
默认绑定的优先级最低
毫无疑问,默认值就是处于最底层。
显示绑定 > 隐式绑定
// 验证
function foo() {
console.log(this);
}
var obj = {
name: 'copyer',
foo: foo
}
obj.foo.apply('abc') // this: String('abc')
new绑定 > 隐式绑定
// 验证
function foo() {
console.log(this);
}
var obj = {
name: 'copyer',
foo: foo
}
var a = new obj.foo() //this: foo {} 实例对象
new绑定 > 显示绑定
new关键词与 apply函数或者 call函数 不能同时进行使用。原因:new关键词也是调用函数,apply函数和call函数也是调用函数,之间相互矛盾了。
所以只能比较 new 和 bind函数之间的比较。
// 验证
function foo() {
console.log(this);
}
var obj = {
name: 'copyer',
foo: foo.bind('abc') // bind返回一个新函数,this指向了String('abc')
}
var a = new obj.foo() //this: foo {} 实例对象
小结
优先级: new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
跳出规则之外的现象
上面的四种规则,可以解决绝大多数函数的this指向,但是还有少部分函数的this是没有遵循规则的。
NO1、严格模式
在严格模式下,默认绑定的this是undefined。
"use strict";
function foo() {
console.log(this); // this: undefined
}
foo()
N02、忽略显示绑定
当 apply、call、bind的第一个参数是unll或者undefined时,this指向window
function foo() {
console.log(this); // this: window
}
foo.apply(undefined)
但是在严格模式下,又恢复了该规则
"use strict"
function foo() {
console.log(this);
}
foo.apply(undefined) // this: undefined
foo.apply(null) // this: null
NO3、间接函数引用
这种代码非常的少见,但是不排除面试题中可能出现,所以还是了解下。那么直接进入正题。
var obj1 = {
name: "obj1",
getName: function () {
console.log(this);
},
};
var obj2 = {
name: "obj2",
};
(obj2.bar = obj1.getName)(); //this: window 独立函数调用
是不是很疑惑:
(obj2.bar = obj1.getName)(); // this: window
// 等价于
obj2.bar = obj1.getName;
obj2.bar() // this: obj2
其中的原因,我也不知道,哈哈哈。
小知识点
当一行中 以
[ (开头的时候,需要在上面一行代码加上一个分号;。不然JS引擎词法分析的时候,会看成一行代码;示例:
var obj2 = { name: "obj2", } // 如果没有分号 (obj2.bar = obj1.getName)() // 会被解析成 var obj2 = { name: "obj2", }(obj2.bar = obj1.getName)() // 语法报错
NO4、箭头函数(重点理解)
ES6中新增了一个函数类型:箭头函数
// 形式如下
() => {} // 代替了 function() {}
特点:
- 不能绑定
this、arguments属性。 - 箭头函数不能作为构造函数使用,即不能和new关键词一起使用
简写优化:
- 当只有一个参数时,可以省略小括号
- 当函数体只有一句代码时,可以省略大括号(如果返回一个对象,可以简写为
({}))
[1,2,3].map(v => ({id: v}))
// 等价于
[1,2,3].map(v => {
return {id: v}
})
说多了,扯远了,this才是主题。
箭头函数是没有自己的this,如果使用this,那么就会去寻找上一层作用域中的this
this的查找规则: 延着作用域链寻找this,找到停止。
对象是不会产生作用域的,也就是说没有this。
重点来啦!!!
函数的作用域(以及父级作用域)在定义函数的时候,就已经确定了,所以箭头函数的this在定义的也已经被确定了; 简单的来说箭头函数的this不是在执行的时候确定,而是在定义的时候就已经确定了(跟上面的规则,恰恰相反) 。
// 案列一:
let obj = {
name: 'copyer',
getName: function () {
let logName = () => {
console.log(this) // this: obj
}
logName()
}
}
obj.getName()
obj.getName调用函数,使用隐式绑定。所以 getName函数作用域中的this就是指向obj。
getName函数体中的logName函数是一个箭头函数,那么箭头函数中的this就会寻找上一层作用域(即父级作用域),就是getName的函数作用域,其this为obj,所以箭头函数中的this也是指向obj。
// 案例二:
let obj = {
name: 'copyer',
getName: () => {
let logName = () => {
console.log(this) // this: window
}
logName()
}
}
obj.getName()
通过obj.getName调用函数,使用隐式绑定。但是呢?getName是一个箭头函数(四种绑定规则失效),是不存在this的,那么就会寻找上一层作用域,即全局作用域,this指向window,所以getName函数作用域中的this也是指向window。
getName函数体中的logName,也是一个箭头函数,也会寻找上一层作用域,即getName函数作用域,this指向window,那么logName的函数作用域this也是指向window。
小结
箭头函数的this理解也还好吧,只要你熟悉作用域和作用域链,箭头函数的this的原理跟其是一样的。
内置函数的this指向
在JavaScript中提供了一些内置函数,其参数接受一个回调函数。因为不知道内置函数中内部是怎么调用回调函数,所以说,也就无法确定this到底是指向什么。这就只有在平时的过程中,慢慢积累,使用多了,也就知道this的指向了。
setTimeout
setInterval也是一样的。
// 案例一:setTimeout中的回调函数是个普通函数(function)
var obj = {
name: "copyer",
getName: function () {
setTimeout(function () {
console.log(this); // this: window
}, 1000);
},
};
obj.getName()
// 案例二:setTimeout中的回调函数是个箭头函数(寻找上一层作用域)
var obj = {
name: "copyer",
getName: function () {
setTimeout(() => {
console.log(this); // this: obj
}, 1000);
},
};
obj.getName()
// 案例三: 严格模式下,上面的两种情况也是一样的
猜测: setTimeout和setInterval中的回调函数属于直接调用(默认绑定),this指向window;如果回调函数是箭头函数,就会寻找上一层作用域。
dom事件
对一些dom节点进行操作(比如:onclick)
var el = document.getElementsByClassName('box')[0]
el.onclick = function () {
console.log(this) // this: dom节点
}
给dom的onclick 赋值一个函数,当鼠标点击的时候,就会调用该函数。
猜测:JS引擎内部调用onclick时,内部调用函数采用的是显示绑定this;即
onclick.apply(dom)
forEach/map/filter
三个函数,都是用于循环。
接受的参数:
- 参数一:回调函数(必选),内部自动执行
- 参数二:this的指向(可选)
// 不传递第二个参数情况下,this指向window
[1, 2, 4].forEach(function () {
console.log(this); // this: window
});
// 传递第二个参数
var obj = {name: 'copyer'}
[1, 2, 4].forEach(function () {
console.log(this); // this: obj
},obj);
猜测:forEach内部调用回调函数,是显示绑定。forEach的第二个参数默认是为window;如果没有传递第二个参数,this就指向window;如果传递了第二个参数:this就指向第二个参数的内容。
map 和 filter 也是一样的。
总结
终于总结完了this的指向问题。内容有点多,情况也比较多,需要经常性的回顾。
但是说,this有多难的话,也应该不至于,记住一下几点原则:
- 普通函数(function)是在执行的时候,才确定this的指向的。
- 箭头函数是在定义的时候,this就已经确定了(因为定义的时候,作用域已经确定了)。
- 记住四种绑定规则:new绑定、显示绑定会、隐式绑定、默认绑定;以及它们的优先级顺序。
记住上面的三点,大部分的this指向问题是没有问题的(NO Problem)。当然,还有一些特殊的情况,需要自己在平时的开发过程中,自己积累。
如果,上面的内容有问题的话,请多多指教。
附带几道面试题,加深this的使用。
面试题
// 面试题一:
var name = "window";
var person = {
name: "person",
getName: function () {
console.log(this.name);
},
};
function getName() {
var fn = person.getName();
fn(); // window
person.getName(); // person
(b = person.getName)(); // window
}
getName();
// 面试题二:
var name = 'window'
var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var person2 = { name: 'person2' }
person1.foo1() // person1
person1.foo1.call(person2) // person2
person1.foo2() // window
person1.foo2().call(person2) // window
person1.foo3()() // window
person1.foo3.call(person2)() // window (犯过错)
person1.foo3().call(person2) // person2
person1.foo4()() // person1
person1.foo4.call(person2)() // person2(犯过错)
person1.foo4().call(person2) // person1
// 面试题三:
var name = "window";
function Person(name) {
this.name = name;
this.foo1 = function () {
console.log(this.name);
};
this.foo2 = () => console.log(this.name);
this.foo3 = function () {
return function () {
console.log(this.name);
};
};
this.foo4 = function () {
return () => {
console.log(this.name);
};
};
}
var person1 = new Person("person1");
var person2 = new Person("person2");
person1.foo1(); // person1
person1.foo1.call(person2); // person2 (犯过错)
person1.foo2(); // person1
person1.foo2().call(person2); // person1
person1.foo3()(); // window
person1.foo3.call(person2)(); // window
person1.foo3().call(person2); // person2
person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1
//面试题四:
var name = "window";
function Person(name) {
this.name = name;
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person("person1");
var person2 = new Person("person2");
person1.obj.foo1()(); // window
person1.obj.foo1.call(person2)(); // window
person1.obj.foo1().call(person2); // person2
person1.obj.foo2()(); // obj(犯过错)
person1.obj.foo2.call(person2)(); // person2
person1.obj.foo2().call(person2); // obj(犯过错)
来挑战一下自己吧。