this
是一个比较迷惑的人是东西,尽管你对this
有很多的了解,但是面试题里面考察this
指向总会让你有种猜谜的感觉,知道一些,但是还是会出错,或许你猜对了,但是又好像解释不太清楚。
嗯,不是你一个人这样,很多人都这样,包括我自己,本质上就是面试埋下的坑,让你跳进去,你想跳过去,那还是不太容易,真正对知识的理解与应用,绝不只是停留在概念与理念,也不是为了完成一道面试题,答不对也没关系,如果面试官给你耐心解释了这道题,那也是一次不错的学习机会。
正文开始...
在阅读本文之前,主要会从以下几点对this
的思考
this
是什么时候产生的- 迷惑的
this
在函数中的指向问题 箭头函数
中this
- 常用改变
this
的指向方案
this这个是什么
- 全局this
为了了解this
,我们先看下this
,新建一个index.html
与1.js
console.log(this, Object.getPrototypeOf(this));
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>this</title>
</head>
<body>
<div id="app"></div>
<script src="./1.js"></script>
</body>
</html>
当我们在浏览器打开时,我们会发现this
是一个window
对象
如果我们在终端
直接运行1.js
呢
{} [Object: null prototype] {}
在node
环境下,全局的this
居然是一个{}
对象
- 严格模式下函数内部的
this
现在我们在js
的最顶部使用use strict
采用严格模式。
我们在函数内部写一个this
"use strict"
console.log(this, Object.getPrototypeOf(this));
var publicName = "Maic";
function hello() {
console.log(this) // undefined
console.log(this.publicName) // undefined
}
hello();
在严格模式
下函数内部会是undefined
,并且访问publicName
会直接报错
为啥use strict
严格模式下全局this
无法访问
于是查找资料寻得,严格模式主要有以下特征
未提前申明的变量不能使用,会报错
不能用
delete
删除对象的属性定义的变量名不能重复申明
函数内部的
this
不再指向全局对象
还有其他的更多的参考js-script
this的指向
在这之前我们很基础的了解到在非严格模式下this
指向的是window
或者{}
对象,在普通函数中this
的指向是window
全局对象
而你通常会看到this
的指向并不都是指向全局对象,而是动态变化的,正因为它会变化,所以令人十分费脑壳
- 非严格模式普通函数
this
指向
function hello() {
console.log(this) // window
// console.log(this.publicName);
}
hello();
在普通函数内部this
指向的是window
对象
- 构造函数的
this
指向
...
function Person() {
this.age = 10;
this.name = 'Web技术学苑';
console.log(this, '111')
}
const person = new Person();
console.log(person, '222'); // Person { age: 10, name: 'Web技术学苑' }
至此你会发现,构造函数内部的this
居然就是实例化的那个对象person
- 对象定义的内部函数
const userInfo = {
publicName: 'Jack',
getName: function () {
console.log(this.name, '--useInfo') // Jack
}
}
userInfo.getName();
不出意外打印都知道肯定publicName
肯定是Jack
,内部的this
也是指向userInfo
箭头函数的this
但是如果改成下面这种呢
var publicName = "Maic";
const userInfo = {
publicName: 'Jack',
getName: () => {
console.log(this.publicName, '---useInfo')
}
}
userInfo.getName();
这是一个很迷惑的问题,箭头函数不是没有自己的this
吗,而且这里是userInfo.getName()
这不是一个隐式调用吗?应也是userInfo
这个对象才对,但是并不是,当改成箭头函数后,内部的this
居然变成了全局的window
对象了
我们看下babel
对上面一段代码编译成es5
的代码
es6代码
var publicName = 'Maic';
const userInfo = {
publicName: 'Jack',
getName: () => {
console.log(this.publicName, '---useInfo')
}
}
userInfo.getName();
编译后的代码,大概就是下面这样的了
var _this = this;
var publicName = "Maic";
var userInfo = {
publicName: "Jack",
getName: function getName() {
console.log(_this.publicName, "---useInfo");
}
};
userInfo.getName();
其实箭头函数是非常迷惑人的,而且外面是一个被调用的是一个对象,所以时常会给人一种幻觉,我们常听到一句this
指向的是被调用的那个对象,那么这里箭头函数
的this
指向的是window
,而const
定义的变量会被转换成var
那怎么能让getName
指向的是本身自己的useInfo
呢
var publicName = 'Maic';
const userInfo = {
publicName: 'Jack',
getName: function(){
console.log(this.publicName, '---useInfo') // Jack
}
}
userInfo.getName();
你看当我把箭头函数改成普通函数,这个普通函数内部的this
就指向userInfo
了
this
指向被调用的那个对象貌似这句话后又在此时好像又是正确的
我们接下来看下下面一种情况
var publicName = 'Maic';
const userInfo = {
publicName: 'Jack',
getName: function(){
console.log(this.publicName, '---useInfo') // Jack
}
}
var user = userInfo.getName;
user();
那么此时getName
内部的this
又是谁呢? 此时你会发现打印的是Maic
此时会发现this
指向的是window
,也就是说指向的那个被调用者
,那被调用者
是谁?
上面那段代码同等于下面,你仔细看
var publicName = 'Maic'; // var 定义,实际上等同于window.publicName = publicName
function getName () {
console.log(this.publicName, '---useInfo') // Jack
}
const userInfo = {
publicName: 'Jack',
getName
}
// var user = userInfo.getName;
// or 等价于
// window.user = userInfo.getName;
// or 进一步等价
window.user = function getName () {
console.log(this.publicName, '---useInfo') // Jack
}
// user();
// or 等价于
window.user();
所以你现在是不是很清晰明白this
指向的也是被调用的那个对象window
了
但是有一点必须申明,必须在非严格模式下,此时的this
才会指向window
。
迷失中的this指向
在这之前我们了解到非严格模式下
- 普通函数内部的
this
指向的是window
对象 - 构造函数内的
this
指向的是实例化的那个对象 - 普通申明的对象,如果调用的方法是箭头函数,那么内部
this
指向的是全局对象,如果不是那么指向的是被调用本身的那个对象
我们再来看下那些面试题中很迷惑的this
var user = {
name: 'Maic',
a: {
name: 'Tom',
b: function () {
console.log(this.name)
}
}
}
console.log(user.a.b()) // Tom
没错,你看到的这个打印是Tom
,这里直接调用的是b
这个方法,被调用的是user.a
这个对象,所以在b
这个方法内部的this
指向了a
对象
如果是箭头函数呢
var publicName = "Maic";
...
var user = {
name: 'Jack',
a: {
name: 'Tom',
b: () => {
console.log(this.name)
}
}
}
console.log(user.a.b()) // Maic
我们会发现通过babel
转换后会是这样的
var _this = this;
var user = {
name: "Jack",
a: {
name: "Tom",
b: function b() {
console.log(_this.name);
}
}
};
所以依然箭头函数内部依然是个全局对象window
我们接下来看一道真实的面试题
var obj = {
a: 1,
b: function () {
console.log(this.a)
},
c: () => {
console.log(this.a)
}
}
var a = 2;
var objb = obj.b;
var objc = {
a: 3
}
objc.b = obj.b;
const t = objc.b;
obj.b(); // 1
obj.c(); // 2
objb(); // 2
objc.b(); // 3
obj.b.call(null); // 2
obj.b.call(objc); // 3
t() // 2
我想信绝大大部分第一个obj.b()
肯定是可以正确答出来,但是后面的貌似有些迷惑人,时常会让你掉进坑里
我们先看结论打印的依次肯定是
1
2
2
3
2
3
2
obj.b()
的调用实际上在之前例子已经有讲,b
方法是一个普通方法,内部this
指向的就是被调用的obj
对象,所以此时内部访问的a
属性就是对象obj
var objb = obj.b
,当我们看到这样的代码时,其实这段代码可以拆分以下
function b() {
console.log(this.b)
}
window.objb = b;
本质上就是将对象obj
的一个方法b
赋值给了window.objb
的一个属性
所以objb()
的调用也是window.objb()
,objb
方法内部this
自然指向的就是window
对象,而我们用var a = 2
这个默认会绑定在window
对象上
obj.c()
,因为c
是一个箭头函数,所以内部的this
就是指向的全局对象
obj.b.call(null)
这个null
是非常迷惑人,通常来说call
不是改变函数内部this
的指向吗,但是这里,如果call(null)
实际上会默认指向window
对象
objc.b()
这打印的是3,其实与objb
的赋值有异曲同工之笔
...
var objc = {
a: 3
}
objc.b = obj.b;
本质上就在objc
动态的新增了一个属性b
,而这个属性b
赋值了一个方法,也就是下面这样
objc.b = function() {
console.log(this.a)
}
objc.b() // 3
如果是const t = objc.b
,至此你会发现,当我们执行t()
时,此时打印的却是2
那是因为const t
定义的变量会编译成var
从而t变量变成一个全局的window对象下的属性,本质上等价下面
...
// const t = objc.b
var a = 2;
/*
等价于下面
var t = function() {
console.log(this.a)
}
*/
// 本质上就是
window.t = function() {
console.log(this.a)
}
- 多层对象嵌套下的
this
var nobj = {
name: '1',
a: {
name: '2',
b: {
name: '3',
c: function () {
console.log(this.name)
}
}
}
}
console.log(nobj.a.b.c()); //3
以上的结果是3,实际上我们从之前案例中明白,非严格模式下this
指向被调用
那个对象
所以你可以把上面那段代码看成下面这样
...
console.log((nobj.a.b).c()); //3
//or 相当于
/*
*
var n = nobj.a.b;
n.c()
*/
改变this对象的指向
这个相信很多小伙伴已经耳熟能祥了,call
,apply
,bind
,能手撕call
,apply
,bind
的文章已经不计其数
这里就只讲解如何使用,以及他们在业务中的一些具体使用场景
- call
用一段伪代码举证以下
// index.vue
import configOption from './config'
export default {
name: 'index',
computed: {
optionsBtnGroup() {
return configOption.call(this)
}
},
methods: {
handleEdit(id) {
console.log(id)
},
handleDelete(id) {
console.log(id)
}
}
}
对应的template
可能就是下面这样几个按钮
<div>
<a href="javascript:void(0)" v-for="(item, index) in optionsBtnGroup" :key="index" @click="item.handle(item.id)">{{item.text}}</a>
</div>
我们再来看下config.js
export default () => {
const options = [
{
text: '编辑',
id: 123,
handle: (id) => {
this.handleEdit(id)
}
},
{
text: '删除',
id: 234,
handle: (id) => {
this.handleDelete(id)
}
}
]
}
正因为在计算属性中用了call
所以在config.js
中才能访问外部methods
的方法,有些人看到这样的代码肯定会说,两个按钮这么搞配置,代码反而多了这么多,还不如模版上放两个按钮完事
是的,确实是,当我们为了使用call
而使用反而增加了业务代码的维护成本,正常情况还是建议不要写出上面那段坏代码的味道
,我们只要明白在什么时候可以用,什么可以不用就行,不要为了使用而使用,反而本末倒置。
但是有时候如果业务复杂,你想隔离业务的耦合,达到通用,call
能帮你减少不少代码量
- apply
apply
也是可以改变this
对象
const userInfo = {
publicName: 'Jack',
getName: () => {
console.log(this.publicName, '---useInfo')
}
}
function test(...args) {
console.log(args); // ['hello', 'world']
console.log(this.publicName);
}
test.apply(userInfo, ['hello', 'world'])
apply
会立即执行该函数,如果传入的首个参数是null
或者undefined
,那么此时内部this
指向的是window
另外还有一个方法可以让函数立即执行,也能改变当前函数this
指向
...
var publicName = 'Maic';
function test(...args) {
console.log(args);
console.log(this.publicName);
}
Reflect.apply(test, {publicName: 'aaa'}, [1,2,3]) // aaa [1,2,3]
Reflect.apply(test, window, ['a', 'b', 'c']) // Maic ['a', 'b', 'c']
- bind
这也是可以改变this
指向,不过会返回一个新函数,我们常常在react
中发现这样用bind
显示绑定方案。
我们写个简单的例子,尝试改变页面背景,切换肤色
document.body.addEventListener('click', function () {
console.log(this) // body
if (this.style.backgroundColor === 'red') {
this.style.backgroundColor = 'green'
} else {
this.style.backgroundColor = 'red';
}
})
可以切换背景肤色
以上貌似没有问题,但是你可能会写这样的代码
document.body.addEventListener('click', () => {
console.log(this)
if (this.style.backgroundColor === 'red') {
this.style.backgroundColor = 'green'
} else {
this.style.backgroundColor = 'red';
}
})
此时内部的this
一定指向的window
,而且内部访问style
报错
于是你会改成这样
const fn = function () {
if (this.style.backgroundColor === 'red') {
this.style.backgroundColor = 'green'
} else {
this.style.backgroundColor = 'red';
}
}
document.body.addEventListener('click', fn)
是的,这样是可以的,本质上就是一个fn
的形参,内部this
指向仍然是document.body
于是为了借助bind
,你可以这么做
const body = document.body;
const fn = function () {
if (this.style.backgroundColor === 'red') {
this.style.backgroundColor = 'green'
} else {
this.style.backgroundColor = 'red';
}
}.bind(body)
body.addEventListener('click', fn)
这么做也是ok的
不知道你有没有疑问,为什不像下面这么做呢?
const body = document.body;
const fn = function () {
if (this.style.backgroundColor === 'red') {
this.style.backgroundColor = 'green'
} else {
this.style.backgroundColor = 'red';
}
}
body.addEventListener('click', fn.bind(this))
如果你仔细看下,其实fn
内部this
指向是window
,所以这是一个常会犯的错误。
还有为啥不是像下面这样
const body = document.body;
const fn = function () {
if (this.style.backgroundColor === 'red') {
this.style.backgroundColor = 'green'
} else {
this.style.backgroundColor = 'red';
}
}
body.addEventListener('click', fn.bind(body))
以上功能没有任何问题,但是我们每次点击都会调用bind,从而返回一个新的函数,所以这种方式虽然效果一样,但是性能远不如第一种,为了更好理解,你可以写成下面这样
const body = document.body;
const fn = function () {
if (this.style.backgroundColor === 'red') {
this.style.backgroundColor = 'green'
} else {
this.style.backgroundColor = 'red';
}
}
const callback = fn.bind(body)
body.addEventListener('click', callback)
总结
了解
this
怎么产生的,通常情况this
在非严格模式下,指向的是全局window
对象,在严格模式下,普通函数内的this
不是全局对象迷惑的
this
指向问题,正常情况this
指向的是被调用的那个对象,但是如果是箭头函数,那么指向的是全局对象window
bind
,call
,apply
改变this
指向推举一篇关于阮一峰老师this的博文