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[1]
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 name = "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
显示绑定方案。
我们写个简单的例子,尝试改变页面背景,切换body肤色
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
指向- code example[2]
- 推荐一篇关于阮一峰老师this[3]的博文