前言
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: FEHuangsetTimeout(feifei.getName, 1000); // Name: FEHuangsetTimeout(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的第一个参数,就是对应函数的this所指向的对象。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指向还需要多加练习,本文只是列举了个大概,只有不断练习才能熟练掌握哦~
有错误,欢迎指正哦~😗
