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