javascript精雕细琢(四):认亲大戏——通过console.log彻底搞清this
引言
JS中的this指向一直是个老生常谈,但是新手又容易晕的地方。我在网上浏览了很多帖子,但是发现一个通病,也是博客的局限性——重理论说明,实践性低。最多就是贴个代码或者图,增加可理解性。
所以,我就想通过代码学习黄金法则——敲就完了。以console.log,循序渐进,一步步的实践,来说明this的指向。而从我自身的理解角度来讲,这个方法效果还不错~
那么,接下来我将从两个方面——普通函数及箭头函数两个方面来说明this指向。建议将所有代码copy下来,一步步打印, 最终肯定能够理解this的指向。如果没理解,那么,再打印一遍~
代码在前
// function下的this基于执行环境的指定。简单理解就是函数前边挂谁,this就是谁,没有就是window。那么函数直接调用和匿名函数自执行由于前边什么都没有,就指向window。
// () => {}箭头函数下的this基于作用域链查找确定,向上查找this,那个作用域里有this,就调用这个this。函数直接调用和箭头函数自执行仍旧会遵循查找原则。
//--------function下的this--------
//----首先是普通的函数声明
// function test() {
// console.log(this);
// }
// test(); // 打印window,因为没有指明执行环境,那么执行环境就是window
//
//----如果是闭包呢?
// function test() {
// console.log(this);
// const log = "Lyu";
// const fn = function() {
// console.log(log); //打印Lyu
// console.log(this); //打印window
// }
//
// fn(); // 打印Lyu,window
// }
// test(); //window,Lyu,window。 首先调用test(),由于test()前什么都没挂,this就指向window。然后test函数内部,由于fn()前也什么都没有挂,this同样指向window。举这个例子是想证明,this并不会受函数额作用域及执行上下文影响,必须明确指定。
//
//----然后是匿名函数自执行?
// function test() {
// (function(){
// console.log(this);
// })()
// }
// test(); //window,因为匿名函数自执行前边就不能挂其他玩意儿,所以它始终指向window
//
//----接下来看明确指定执行环境的例子
// const obj = {
// fn: function() {
// console.log(this);
// }
// }
// obj.fn(); //打印obj,因为指定执行环境为obj的{}块级作用域内
//--如果改变一下调用方式呢?
// const fn = obj.fn; // 此时fn = function() { console.log(this) },相当于创建了一个全局函数
// fn(); //打印window,因为没有指定执行环境
//
//----事件调用下的this
// document.onclick = function() {
// console.log(this); //打印document,因为指定执行环境为document,即document在click时触发
// }
//这里相信大家都是明白的,就不再多赘述了
//----最后是通过call、apply、bind绑定下的this
//一句话说明,不再举例。前边说了,function下的this,通过指定执行环境来确定的,而call、apply、bind就是用来指定执行环境的,所以指谁,this就是谁。
//--------箭头函数下的this--------
//既然箭头函数下的this通过作用域链查找,那么作用域中如果没有声明this值,那么就向上查找
//----先说明this的创建与查找
// function Test() {
// console.log(this);
// }
// Test(); // window,未指定作用环境,所以this指向window
// new Test(); //Test{},此时,构造函数内Test()内的this被new声明,this指向构造函数创建的对象Test{},所以打印Test{}对象
//
//----接下来是箭头函数
// function Arrow() {
// window.onmousewheel = () => {
// console.log(this);
// }
// }
// Arrow(); // window,直接调用时,Arrow()函数内并没有声明this,所以滚动鼠标,this会随作用域链查找。先在Arrow函数内,没找到this。然后一直向上,最终找到window。
//--然后我们new它一下子
// new Arrow(); //此时通过new,构造函数Arrow()内的this被声明,且指向对象Arrow{},所以箭头函数在作用域链中查找时,在Arrow函数内就找到this为Arrow{}
//----接下来复杂一点,来个事件,顺便再加点匿名函数自执行
// function Go() {
// //new一个this妈妈
// window.onmousewheel = () => {
// console.log(this); // 妈妈不见了!
// (() => {
// console.log(this); // 妈妈去哪了?
// (() => {
// console.log(this); // 嘤嘤嘤,妈妈没了!
// (() => {
// console.log(this); // 走啊哥几个,找妈妈去
// })();
// })();
// })();
// }
// }
// Go(); //window,因为onmousewheel事件中及Go()函数中没有声明this,所以按照作用域链查找,找到window
//--然后我们再new它一下子
// new Go();
// 全部打印Go{},因为new操作符,Go()函数中声明了this,且指向Go{}对象。而onmousewheel事件也用箭头函数指定,仍旧遵循查找原则。就这么一层一层的找,最后都找到Go函数作用域内的this,最后全部打印Go{}。这不就是小蝌蚪找妈妈嘛!
//----接下来换个搭配方式再看一下,普通function搭配箭头函数
// function GoOn() {
// document.onclick = function() {
// console.log(this);
// (() => {
// console.log(this);
// })();
// }
// }
// GoOn();
// document、document,此时由于function中的this已经绑定到document,所以第一个打印document;
// 而由于箭头函数自执行仍旧遵循作用域链查找原则,不会指向window。所以箭头函数自执行后,根据作用域链向上查找this,找到document;
//--啥也别说了,就是new它丫的
// new GoOn()
// document、document,此时就算new操作声明了this,但是是在click事件外的作用域中,箭头函数在click中已经找到了this,不会再向上查找;
//所以仍旧打印document,document;
//
//----再看个混搭,然后我们结束搭配
// function Going() {
// document.onclick = function() {
// console.log(this);
// (function() {
// console.log(this);
// })();
//
// (() => {
// onsole.log(this);
// })
// }
// }
// Going();
// document、window、document,首先上来就指定了this为document,所以第一个打印document;
// 而function的匿名函数自执行会指向window,所以第二个打印window;
// 第三个箭头函数自执行,遵循作用域链查找原则,在onclick事件中找到this为document,所以打印document;
// --new、new、new
// new Going(); //规则不变,结果不变
//
//----好,混搭看完,接下来说个更有意思的。关于作用域的形成
//----JS的函数作用域及作用域链,是在函数创建时就被固定的
//----这么说确实不太直观,那么通过举例来说明
// function Test() {
// console.log(this)
// innerTest();
// }
// const innerTest = () => {
// console.log(this);
// }
// Test(); //window、window,如果不明白,把上边再看一遍
// new Test();
// Test{}、window,从这就可以看出端倪了。
// 为什么Test内的this指向Test{}对象了,而箭头函数中的this仍旧为window呢?innerTest函数在Test函数内执行,说好的按照作用域链查找呢?
// 请看文章中详细的解释
//
//----最后,call、apply、bind下的箭头函数
//一句话说明,箭头函数的this改不了,干啥都改不了,咋着都改不了,硬气!
1、function下的this
我将从普通的函数声明、匿名函数自执行、对象声明、事件绑定、及call等方法绑定来分别说明function下的this指向。
function下的this理解起来也简单。我们就以亲爹和干爹来比喻:
假设window是所有函数的干爹。我们是公益组织,要给JS下的函数找到它们的亲爹,而function声明的函数,都是渴望父爱的男孩;
确认亲爹的方式就是调用函数的时候在它们前边加个 .(点) 或者 ["name"],或者通过call、apply、bind其他手段确认;
而那些调用时候,前边嘛也没有的,他们亲爹没找到,那他们的干爹就当亲爹来孝顺;
这场公益认爹,就是function的this行为—函数前边有.(点)或者["name"],明确指定了亲爹的,this就是亲爹;直接调用的函数、匿名自执行的函数,这俩没找到亲爹的,this就是干爹window;而通过call、apply、bind其他渠道找到的亲爹,this同样是亲爹;
下面详细说一下这场认亲公益行, 各种情形下的this指向,一切以上边贴的代码为基础!
1) 普通function声明
最常用的函数声明无非是两种:
function test() {} 及 const test = function() {}
这两种写法的区别在于声明方式的不同,进而影响变量提升,并不会对this的指向产生影响。
这两种声明方式下的function函数,在调用时,通常就是直接调用。那么通过认爹我们就能知道,这种情况下的this就是window。
function test() {
console.log(this);
}
test(); // 打印window,因为没有指明执行环境(没找到亲爹),那么执行环境就是window(干爹)
为什么代码里我加上了闭包的说明呢?主要是为了跟箭头函数做一个区分,证明一下,function下的this跟作用域以及作用域链无关。同时跟它调用时的执行上下文也无关,就是看函数前边有没有 .(点)——必须明确它的亲爹。
2) 自执行匿名function声明函数
与函数直接调用同理,不再赘述,匿名函数自执行就理解成父母双亡,这货再也没有亲爹了,所以它的this始终指向window。
3) 对象下的function声明
对象下声明的函数,在调用时是要通过对象方法访问的,所以~肯定有爹!
但是这里边分了两种情况,一种情况是正常的通过对象调用方法,另一种跟直接调用函数无异~
const obj = {
fn: function() {
console.log(this);
}
}
obj.fn(); //打印obj,因为指定执行环境为obj的{}块级作用域内(亲爹为obj)
//如果改变一下调用方式呢?
const fn = obj.fn; // 此时fn = function() { console.log(this) },相当于普通的function创建函数
fn(); //打印window,因为没有指定执行环境(没亲爹)
4) 事件调用下的function声明
事件的一般写法上,它必须要有 .(点)或者[name],所以它 肯定有亲爹(最幸福的function函数),那么.(点)前是谁,亲爹就是谁~
document.onclick = function() {
console.log(this); //打印document,因为指定执行环境为document,即document在click时触发
}
5) call、apply、bind绑定
一句话总结:给谁,谁就是亲爹!
function test() {
console.log(this.say);
};
const obj = {say: "我是它爹"};
const father = {say: "我也是它爹"};
const result = {say: "我也是它爹,它到底几个爹"};
test.apply(obj); // 我是他爹
test.call(father); // 我也是他爹
(test.bind(result))(); // 我也是它爹,他到底几个爹
2、箭头函数下的this
首先,不明白箭头函数的,请先自行百度或者Google,不要还没开车就出车祸了;
然后,接下来我会从正常函数声明的箭头函数、匿名函数自执行的箭头函数、对象下声明箭头函数、事件调用下的箭头函数、function与箭头函数混合双打、作用域链查找、及call等方法绑定来说明箭头函数下的this指向。
那么,同上,箭头函数也来个比喻,同样用亲爹和干爹:
设定不变,window还是干爹。但是也有一点不同——那就是箭头函数她是个拜金女,就爱找有钱(this)的干爹;
而且吧,在拜金女眼里,window这个干爹是最穷的,所以不到走投无路,不找window这个干爹。而对它的亲爹,有钱(this)才行;
而this当然就是钱啦,谁有钱这箭头函数它就找谁!调用箭头函数就是找钱!
所以啊,在这场发家致富之旅中,箭头函数中this的指向也是很明确的——如果当前作用域中,没有通过call、apply、bind、new等操作明确this的指向(没钱),那么箭头函数将沿着作用域链(关系网)继续想上查找,直到找到明确的this(有钱的干爹)为止
1) 正常声明的箭头函数
同function不同,对于箭头函数,只有一种声明方式:
const arrow = () => {}
可以变换的地方就是参数和返回值部分的简写
同样的,这种声明方式下的函数,就是直接调用。那么根据前边的比喻,箭头函数的认爹方式跟function是大不相同的。直接调用箭头函数时,这个拜金女就开始见钱眼开了——它先在当前作用域中找,当前作用域下如果没有明确的this(钱),就继续沿着作用域链往上找,直到找到this为止,因为有window这个干爹保底,所以一点好处没捞到的时候,就找window。
function Arrow() {
window.onmousewheel = () => {
console.log(this);
}
}
Arrow(); // window,直接调用时,Arrow()函数内并没有明确的this(没钱),所以滚动鼠标,this会随作用域链查找(这个干爹不行,就再换个干爹)。先在Arrow函数内,没找到this。然后一直向上,最终找到window(只能保底)。
new Arrow(); //此时通过new,构造函数Arrow()内的this被声明(有钱了),且指向对象Arrow{},所以箭头函数在作用域链中查找时,在Arrow函数内就找到this为Arrow{}
2) 自执行匿名箭头函数
箭头函数是个很有原则的拜金女,不管怎么执行它,它就认钱,就认钱,就认钱(重要事情说3遍),有钱才是爹。所以就算是自执行的匿名箭头函数,它仍旧遵循找爹原则,没钱免谈,我接着向上找。
所以,它仍旧先在当前作用域中找,当前作用域下如果没有明确的this(钱),就继续沿着作用域链往上找,直到找到this为止。都没有,就找window。
function Test() {
console.log(this);
(() => {
console.log(this);
})();
}
Test(); //window、window;
new Test(); //Test{}、Test{};
//规则同上,不再赘述
3) 作为对象方法的箭头函数
按照function声明的逻辑,对象调用它下面的方法,this肯定是指向对象的。那么箭头函数是否也是如此呢?答案肯定是否定的,因为对象中并没有明确的this,而且对象还不能new,所以这就悲催了——箭头函数所存在的对象,永远不可能是它的干爹(只限于父女关系的箭头函数与对象,不包括function与箭头函数混搭的爷孙关系等等);
const obj = {
test: () => {
console.log(this);
}
fn: function() {
(() => {
console.log(this)
})
}
}
obj.test() // window,obj.test内没有明确的this(钱),所以向上找到obj,结果obj也没有钱,所以最后只能委曲求全,找window
obj.fn() // obj,obj.fn中由于function的存在,this指向obj,所以一发命中,直接找obj认爹
4) 事件调用下的箭头函数
其实作为一个拜金女,箭头函数的生活还是挺无趣的,规则太单一。就拿这个事件调用来说吧,还是一个套路。不管我是不是你亲生的,反正你没钱,我就不认你。
function Go() {
//没想到唯一的希望也是身无分文……唉,又得window了
window.onmousewheel = () => {
console.log(this); // 亲爹看来你也没钱啊!
(() => {
console.log(this); // 又一个穷货!
(() => {
console.log(this); // 这也没钱!
(() => {
console.log(this); // 钱呢!
})();
})();
})();
}
}
Go(); // window,因为onmousewheel事件中及Go()函数中没有明确的this(钱),所以按照作用域链查找,找到window(走投无路)
new Go(); // 有钱了
// 全部打印Go{},因为new操作符,Go()函数中声明了this,且指向Go{}对象。
// 而onmousewheel事件也用箭头函数指定,仍旧遵循查找原则。就这么一层一层的找,最后都找到Go函数作用域内的this(钱),最后全部打印Go{}(逮着一个有钱的可劲造,全造它一个)。
// Go{}对象左拥右抱,帝王生活让人向往!
5) function与箭头函数混搭及JS静态作用域
俗话说得好哇,一山不容二虎,除非一公一母!还有就是男女搭配,干活不累!
function与箭头函数这一男一女遇上后,那是干柴遇烈火,一拍即合,合作起来非常愉快!
在function这个拉皮条缺父爱的男孩帮助下,箭头函数找干爹变得容易起来~
以开头代码中挖的坑为例,顺带说一下JS中的静态作用域
function Test() {
console.log(this)
innerTest();
}
const innerTest = () => {
console.log(this);
}
Test(); // window、window
new Test(); // Test{}、window,从这就可以看出端倪了。
按照上边一路顺下来的思路理解的话,第二次new操作之后,应该打印Test{}和Test{}对不对?
让我们捋一下思路:
在Test函数里调用innerTest函数,innerTest函数是一个箭头函数。那么我在Test里调用它的时候,这拜金女肯定是一步一步的往上找this(钱);
第一次无new直接调用没毛病,Test里没this(钱),所以找了window;
可是第二次new操作后,Test有this(钱)了,为啥箭头函数没找Test?难道嫌它丑?
一张图说明情况:
从Chrome控制台打印的作用域中可以看出,innerTest的作用域链中根本没有Test函数,所以它压根不会在Test中查找this。
这就表明了JS作用域与作用域链的一个问题——静态。即函数的作用域及作用域链,在函数声明时形成,并且保持不变。因为innerTest是在全局声明的,所以它的作用域链只有Script及Global,就算再Test函数内调用,也不会改变,除非在Test函数内再声明一个函数,那么该函数的作用域及作用域链中就包含了Test函数,不管有没有通过闭包调用Test函数中的变量(不调用Test函数内变量的话,Chrome浏览器控制台中打印不出来闭包作用域)。
6) call、apply、bind绑定
一句话总结:我对this(钱)很专一的!
箭头函数的this指向,无法通过call、apply、bind改变!贼专一!
function Test() {
const fn = () => {
console.log(this);
}
fn(); // Test{}
const say = function() {
console.log(this);
}
say() // window
function Replace() {
console.log(this); // Replace{}
fn.call(this); // Text{}
say.call(this); // Replace{}
}
new Replace();
}
new Test();
结语
至此,这场认亲大戏就到此完毕。整体内容还是有点多的,我相信大多数人是没耐心读完的,所以我尽量想写的幽默有趣一点。就像开夜路怕困,会话会变多、抽烟解困,有的人肯定会反感这种文风,但我也没那么多读者~哈哈哈。
最后,有的点挖的还是不够深的,没办法,水平真是有限,挖不动了。如果能给到各位启发,希望你能继续挖下去~
如有错误或阐述不充分之处,欢迎指正~