前言
this也算是面试必问的问题之一了。
但很多人对this理解得模模糊糊,工作时得打印出来看看才敢往下写,面试时只能回答个大概,细节经不起推敲。
今天我们就针对这个痛点,解决掉它!
现在我们来看道题:
:::warning
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number = 2;
number = number 2;
number = 3;
return function () {
var num = this.number;
this.number = 2;
console.log(num);
number = 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
:::
答案依次是:10 9 3 27 20
惊不惊喜,意不意外?如果你比较容易就答对了,那说明你对this的理解还是非常到位的。不然,就跟我一块来看看这篇文章呗~ 这道题的解析在思考1哦~
this是啥?
在讨论this的时候,一般都会说“指向xxx”。this就是一个指针,在了解具体指向之前,我们先引入几个名词:
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
而且它们的优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
。
默认绑定
- 在不能应用其它绑定规则时使用,通常是独立函数调用。
- 独立函数:是指在全局上下文中的函数,它的this指向如下:
- 非严格且处于Node环境:
globalThis
; - 非严格且处于Windows环境:
window
; - 严格模式下:
undefined
。
- 非严格且处于Node环境:
没有特殊声明的话,本文都是浏览器环境执行的结果。
function getName(){
console.log('Name: ', this.name);
}
var name = 'laohuang';
getName();
解析:调用getName()
时,它处于全局上下文,应用了默认绑定,this指向全局对象window,所以控制台会打印:Name: laohuang
。
隐式绑定
- 函数的调用是通过某个对象调的,或者说调用位置上存在对象,也即
obj.fn()
; - this指向对象属性链中最后一层。比如
obj1.obj2.obj3.fn(), this指向obj3
; - 隐式绑定存在绑定丢失的情况。请记住:
obj.fn()
是隐式绑定,但如果fn()
前啥都没有,属于默认绑定。
典型的obj.fn
function getName(){
console.log('Name: ', this.name);
}
var laohuang = {
name: 'laohuang',
getName: getName
}
var name = 'feifei';
laohuang.getName();
解析:getName
函数在laohuang
外部声明。laohuang
内部的getName
相当于做了一次赋值操作。在调用laohuang.getName()
时,调用位置是laohuang,隐式绑定会把getName里的this
绑定到laohuang
上,所以控制台会打印:Name: laohuang
。
this指向对象属性链中最后一层
function getName(){
console.log('Name: ', this.name);
}
var feifei = {
name: 'feifei',
getName: getName
}
var laohuang = {
name: 'laohuang',
friend: feifei
}
var name = 'FEHuang';
laohuang.friend.getName();
解析:this
指向对象属性链中最后一层,所以隐式绑定会把this
绑定到laohuang.frend
即feifei
上,所以控制台会打印:Name: feifei
。
隐式绑定的大陷阱 - 绑定丢失
1. 绑定丢失 - 将函数的引用给另一变量时:
function getName(){
console.log('Name: ', this.name);
}
var laohuang = {
name: 'laohuang',
getName: getName
}
var name = 'FEHuang';
var getNameCopy = laohuang.getName;
getNameCopy();
解析:var getNameCopy = laohuang.getName
将getName
的引用赋值给了getNameCopy
,getNameCopy
直接指向getName方法
。getNameCopy()
前啥都没有,所以它是隐式绑定,this
指向全局上下文。所以控制台会打印:Name: FEHuang
。
2. 绑定丢失 - 回调函数中:
function getName(){
console.log('Name: ', this.name);
}
var feifei = {
name: 'feifei',
getName: getName
}
var laohuang = {
name: 'laohuang',
getName: function() {
setTimeout(function() {
console.log('Name: ', this.name)
})
}
}
var name = 'FEHuang';
laohuang.getName(); // Name: FEHuang
setTimeout(feifei.getName, 1000); // Name: FEHuang
setTimeout(function() {
feifei.getName(); // Name: feifei
}, 1000)
解析:
laohuang.getName()
: 这里很好理解,setTimeout
的回调函数中,this
使用的是默认绑定,此时又是非严格模式,因此打印Name: FEHuang
;setTimeout(feifei.getName, 1000)
: 这里相当于将feifei.getName
的引用直接给了setTimeout
第一个变量,最后执行了这个变量。这里绑定丢失,使用了默认绑定,因此指向全局上下文,打印:Name: FEHuang
;setTimeout(function() { feifei.getName(); }, 1000)
: 虽然也是在setTimeout
的回调中,但这里是直接执行了feifei.getName()
,使用隐式绑定,this
指向feifei
。所以打印:Name: feifei
。
显示绑定
- 显示绑定就是通过
call
,apply
,bind
的方式,显式的指定this
所指向的对象。 call
,apply
和bind
的第一个参数,就是对应函数的thi
s所指向的对象。call
和apply
的作用一样,只是传参方式不同。call
和apply
都会执行对应的函数,而bind
方法不会。- 注意
call
,apply
的特殊传参会被转换:传null/undefined —> 全局上下文;原始值 —> 对象(非严格模式)/原始值(严格模式)
典型的显示绑定
function getName(){
console.log('Name: ', this.name);
}
var laohuang = {
name: 'laohuang',
getName: getName
}
var name = 'FEHuang';
var getNameCopy = laohuang.getName;
getNameCopy.call(laohuang);
解析:显示绑定直接将this
绑定到了laohuang
,所以控制台会打印:Name: laohuang
。
特殊情况 - 使用call, apply, bind时也可能会遇到绑定丢失
那么,使用了显示绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。
function getName(){
console.log('Name: ', this.name);
}
var laohuang = {
name: 'laohuang',
getName: getName
}
var name = 'FEHuang';
var getNameCopy = function(fn) {
fn();
};
getNameCopy.call(laohuang, laohuang.getName);
解析:getNameCopy.call(laohuang, laohuang.getName)
的确将this
绑定到laohuang
的this
了。但call
的第二个参数传的getName的引用
,所以在执行fn()
的时候,相当于直接调用了getName()
方法。所以控制台会打印:Name: FEHuang
。
思考: 如果希望绑定不会丢失,要怎么做?(答案在最后的思考2)
特殊情况 - call, apply, bind的特殊传参
在非严格模式下使用call
和apply
时,如果用作this
的值不是对象,则会被尝试转换为对象。null
和undefined
被转换为全局对象
。原始值
如 7 或 ‘foo’ 会使用相应构造函数转换为对象
。
1. 传null/undefined:
会将其转换成全局对象,实际使用默认绑定。
var laohuang = {
name: 'laohuang'
}
var name = 'FEHuang';
function getName() {
console.log(this.name);
}
getName.call(null); //FeHuang
解析:实际应用默认绑定,所以控制台会打印:FEHuang
。
2. 传原始值:
会将其转换成对应的对象
var doSth = function(name){
console.log(this);
console.log(name);
}
doSth.call(2, 'laohuang'); // Number{2}, 'laohuang'
var doSth2 = function(name){
'use strict';
console.log(this);
console.log(name);
}
doSth2.call(2, 'laohuang'); // 2, 'laohuang'
new绑定
new干了什么?
MDN上的介绍时这样的:
- 创建一个空的简单JavaScript对象(即{});
- 链接该对象(设置该对象的
constructor
)到另一个对象; - 将步骤1新创建的对象作为
this
的上下文; - 如果该函数没有返回对象,则返回
this
。
举例
function getName(name) {
this.name = name
}
var laohuang = new getName('laohuang');
console.log('Name: ', laohuang.name);
解析:在var laohuang = new getName('laohuang')
这步,会将getName
中的this
绑定到对象laohuang
上。所以控制台会打印:Name: laohuang
。
箭头函数
先来看看箭头函数的特点:
- 函数体内的
this
对象,继承的是外层代码块的this
。注意:箭头函数内的this不是定义时所在的对象,而是外层代码块的this。 - 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
- 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用rest
参数代替。 - 不可以使用
yield
命令,因此箭头函数不能用作Generator
函数。 - 箭头函数没有自己的
this
,所以不能用call()、apply()、bind()
这些方法去改变this
的指向. ``` var names = { getName0: function() {
}, getName1: function() {console.log(this);
return () => {
console.log(this);
}
}, getName2: () => {return function() {
console.log(this);
return () => {
console.log(this);
}
}
} }console.log(this);
// 第一段 var name0 = names.getName0(); // names对象 name0(); // names对象
// 第二段 var name1 = names.getName1(); var _name1 = name1(); // window对象 _name1(); // window对象
// 第三段 names.getName2(); // window对象
**解析**:
第一段:
●names.getName0()对应隐式绑定,this绑定在了names上,所以控制台会打印:names对象;<br />●name0()执行的就是箭头函数。箭头函数的this继承上一个代码段的this(即getName0()运行时的this,即names)。所以控制台也会打印:names对象;
第二段:
●name1是names.getName1()运行后返回的一个全新的函数,对应了上边说到的隐式绑定丢失的情况。此时应用的是默认绑定,this指向了全局对象window。所以name1()打印的是:window对象;<br />●_name1()执行的是箭头函数。<br />○如果箭头函数的this继承自定义时的对象,那_name1()应该打印names对象才对,但这里打印的是window对象,显然这种理解是**错误**的。<br />○按照箭头函数的this是继承于外层代码块的this就很好理解了。外层代码块我们刚刚分析了,this指向的是window,因此这里控制台打印:window对象。
第三段:
●names.getName2()执行的是箭头函数。由于当前的代码块names中不存在this,只能往上层查找。所以这里控制台打印:window对象。
**请牢记:箭头函数中的this继承于外层代码库块的this**
因为箭头函数里的this也有可能是动态的哟~ 不信看下面的代码:
var names = { getName1: function() { return function() { console.log(this); return () => { console.log(this); } } }, }
var name0 = names.getName1();
var n1 = name0(); // window n1(); // window
var n2 = name0.call(names); // names n2(); // names ```
总结
如何准确判断this的指向
再来复习一下,绑定的优先级是:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定。
然后我们就可以按以下步骤来判断了:
- 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
- 函数是否通过call,apply调用,或者使用了bind(显示绑定),如果是,那么this绑定的就是指定的对象。
- 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.fun()
- 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象(node环境的全局对象是globalThis,浏览器环境就是window)。
- 如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
- 如果是箭头函数,箭头函数的this继承的是外层代码块的this。
最后,this指向还需要多加练习,本文只是列举了个大概,只有不断练习才能熟练掌握哦~
有错误,欢迎指正哦~😗