壹 ❀ 引html
能够说this与闭包、原型链同样,属于JavaScript开发中老生常谈的问题了,百度一搜,this相关的文章铺天盖地。可开发好几年,被几道this题安排明明白白的人应该不在少数(我就是其一)。我以为this概念抽象,变化无穷老是让人晕头转向,但平心它并非有多难,今天咱们就从this绑定的五种场景(默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定)出发,静下心来好好聊聊这个 this,本文开始。面试
贰 ❀ this默认绑定数组
this默认绑定咱们能够理解为函数调用时无任何调用前缀的情景,它没法应对咱们后面要介绍的另外四种状况,因此称之为默认绑定,默认绑定时this指向全局对象(非严格模式):闭包
function fn1() { let fn2 = function () { console.log(this); //window fn3(); }; console.log(this); //window fn2(); }; function fn3() { console.log(this); //window }; fn1();
这个例子中不管函数声明在哪,在哪调用,因为函数调用时前面并未指定任何对象,这种状况下this指向全局对象window。app
但须要注意的是,在严格模式环境中,默认绑定的this指向undefined,来看个对比例子:函数
function fn() { console.log(this); //window console.log(this.name); }; function fn1() { "use strict"; console.log(this); //undefined console.log(this.name); }; var name = '听风是风'; fn(); fn1() //TypeError: Cannot read property 'a' of undefined
再例如函数以及调用都暴露在严格模式中的例子:post
"use strict"; var name = '听风是风'; function fn() { console.log(this); //undefined console.log(this.name);//报错 }; fn();
最后一点,若是在严格模式下调用不在严格模式中的函数,并不会影响this指向,来看最后一个例子:性能
var name = '听风是风'; function fn() { console.log(this); //indow console.log(this.name); //听风是风 }; (function () { "use strict"; fn(); }());
叁 ❀ this隐式绑定this
1.隐式绑定spa
什么是隐式绑定呢,若是函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,看个例子:
function fn() { console.log(this.name); }; let obj = { name: '听风是风', func: fn }; obj.func() //听风是风
若是函数调用前存在多个对象,this指向距离调用本身最近的对象,好比这样:
function fn() { console.log(this.name); }; let obj = { name: '行星飞行', func: fn, }; let obj1 = { name: '听风是风', o: obj }; obj1.o.func() //行星飞行
那若是咱们将obj对象的name属性注释掉,如今输出什么呢?
function fn() { console.log(this.name); }; let obj = { func: fn, }; let obj1 = { name: '听风是风', o: obj }; obj1.o.func() //??
这里输出undefined,你们千万不要将做用域链和原型链弄混淆了,obj对象虽然obj1的属性,但它两原型链并不相同,并非父子关系,因为obj未提供name属性,因此是undefined。
既然说到原型链,那咱们再来点花哨的,咱们再改写例子,看看下面输出多少:
function Fn() {}; Fn.prototype.name = '时间跳跃'; function fn() { console.log(this.name); }; let obj = new Fn(); obj.func = fn; let obj1 = { name: '听风是风', o: obj }; obj1.o.func() //?
这里输出时间跳跃,虽然obj对象并无name属性,但顺着原型链,找到了产生本身的构造函数Fn,因为Fn原型链存在name属性,因此输出时间跳跃了。
番外------做用域链与原型链的区别:
当访问一个变量时,解释器会先在当前做用域查找标识符,若是没有找到就去父做用域找,做用域链顶端是全局对象window,若是window都没有这个变量则报错。
当在对象上访问某属性时,首选i会查找当前对象,若是没有就顺着原型链往上找,原型链顶端是null,若是全程都没找到则返一个undefined,而不是报错。
2.隐式丢失
在特定状况下会存在隐式绑定丢失的问题,最多见的就是做为参数传递以及变量赋值,先看参数传递:
var name = '行星飞行'; let obj = { name: '听风是风', fn: function () { console.log(this.name); } }; function fn1(param) { param(); }; fn1(obj.fn);//行星飞行
这个例子中咱们将 obj.fn 也就是一个函数传递进 fn1 中执行,这里只是单纯传递了一个函数而已,this并无跟函数绑在一块儿,因此this丢失这里指向了window。
第二个引发丢失的问题是变量赋值,其实本质上与传参相同,看这个例子:
var name = '行星飞行'; let obj = { name: '听风是风', fn: function () { console.log(this.name); } }; let fn1 = obj.fn; fn1(); //行星飞行
注意,隐式绑定丢失并非都会指向全局对象,好比下面的例子:
var name = '行星飞行'; let obj = { name: '听风是风', fn: function () { console.log(this.name); } }; let obj1 = { name: '时间跳跃' } obj1.fn = obj.fn; obj1.fn(); //时间跳跃
虽然丢失了 obj 的隐式绑定,可是在赋值的过程当中,又创建了新的隐式绑定,这里this就指向了对象 obj1。
肆 ❀ this显式绑定
显式绑定是指咱们经过call、apply以及bind方法改变this的行为,相比隐式绑定,咱们能清楚的感知 this 指向变化过程。来看个例子:
let obj1 = { name: '听风是风' }; let obj2 = { name: '时间跳跃' }; let obj3 = { name: 'echo' } var name = '行星飞行'; function fn() { console.log(this.name); }; fn(); //行星飞行 fn.call(obj1); //听风是风 fn.apply(obj2); //时间跳跃 fn.bind(obj3)(); //echo
好比在上述代码中,咱们分别经过call、apply、bind改变了函数fn的this指向。
在js中,当咱们调用一个函数时,咱们习惯称之为函数调用,函数处于一个被动的状态;而call与apply让函数从被动变主动,函数能主动选择本身的上下文,因此这种写法咱们又称之为函数应用。
注意,若是在使用call之类的方法改变this指向时,指向参数提供的是null或者undefined,那么 this 将指向全局对象。
let obj1 = { name: '听风是风' }; let obj2 = { name: '时间跳跃' }; var name = '行星飞行'; function fn() { console.log(this.name); }; fn.call(undefined); //行星飞行 fn.apply(null); //行星飞行 fn.bind(undefined)(); //行星飞行
另外,在js API中部分方法也内置了显式绑定,以forEach为例:
let obj = { name: '听风是风' }; [1, 2, 3].forEach(function () { console.log(this.name);//听风是风*3 }, obj);
番外-----call、apply与bind有什么区别?
1.call、apply与bind都用于改变this绑定,但call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数,这也是为何上方例子中bind后还加了一对括号 ()的缘由。
2.bind属于硬绑定,返回的 boundFunction 的 this 指向没法再次经过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
3.call与apply功能彻底相同,惟一不一样的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的状况下,call的性能要高于apply,由于apply在执行时还要多一步解析数组。
描述一请参照上面已有例子。
描述二请参照下方例子,咱们尝试修改 boundFunction 的 this 指向:
let obj1 = { name: '听风是风' }; let obj2 = { name: '时间跳跃' }; var name = '行星飞行'; function fn() { console.log(this.name); }; fn.call(obj1); //听风是风 fn(); //行星飞行 fn.apply(obj2); //时间跳跃 fn(); //行星飞行 let boundFn = fn.bind(obj1);//听风是风 boundFn.call(obj2);//听风是风 boundFn.apply(obj2);//听风是风 boundFn.bind(obj2)();//听风是风
描述三请参考如下例子:
let obj = { name: '听风是风' }; function fn(age,describe) { console.log(`我是${this.name},个人年龄是${age},我很是${describe}!`); }; fn.call(obj,'26','帅');//我是听风是风,个人年龄是26,我很是帅 fn.apply(obj,['26','帅']);//我是听风是风,个人年龄是26,我很是帅
更多关于call apply bind能够阅读博主这篇文章 js中call、apply、bind到底有什么区别?bind返回的方法还能修改this指向吗?
伍 ❀ new绑定
准确来讲,js中的构造函数只是使用new 调用的普通函数,它并非一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。
那么new一个函数究竟发生了什么呢,大体分为三步:
1.以构造器的prototype属性为原型,建立新对象;
2.将this(能够理解为上句建立的新对象)和调用参数传给构造器,执行;
3.若是构造器没有手动返回对象,则返回第一步建立的对象
这个过程咱们称之为构造调用,咱们来看个例子:
function Fn(){ this.name = '听风是风'; }; let echo = new Fn(); echo.name//听风是风
在上方代码中,构造调用建立了一个新对象echo,而在函数体内,this将指向新对象echo上(能够抽象理解为新对象就是this)。
若对于new具体过程有疑惑,或者不知道怎么手动实现一个new 方法,能够阅读博主这篇文章 js new一个对象的过程,实现一个简单的new方法
陆 ❀ this绑定优先级
咱们先介绍前四种this绑定规则,那么问题来了,若是一个函数调用存在多种绑定方法,this最终指向谁呢?这里咱们直接先上答案,this绑定优先级为:
显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定
为何显式绑定不和new绑定比较呢?由于不存在这种绑定同时生效的情景,若是同时写这两种代码会直接抛错,因此你们只用记住上面的规律便可。
function Fn(){ this.name = '听风是风'; }; let obj = { name:'行星飞行' } let echo = new Fn().call(obj);//报错 call is not a function
那么咱们结合几个例子来验证下上面的规律,首先是显式大于隐式:
//显式>隐式 let obj = { name:'行星飞行', fn:function () { console.log(this.name); } }; obj1 = { name:'时间跳跃' }; obj.fn.call(obj1);// 时间跳跃
其次是new绑定大于隐式:
//new>隐式 obj = { name: '时间跳跃', fn: function () { this.name = '听风是风'; } }; let echo = new obj.fn(); echo.name;//听风是风
柒 ❀ 箭头函数的this
ES6的箭头函数是另类的存在,为何要单独说呢,这是由于箭头函数中的this不适用上面介绍的四种绑定规则。
准确来讲,箭头函数中没有this,箭头函数的this指向取决于外层做用域中的this,外层做用域或函数的this指向谁,箭头函数中的this便指向谁。有点吃软饭的嫌疑,一点都不硬朗,咱们来看个例子:
function fn() { return () => { console.log(this.name); }; } let obj1 = { name: '听风是风' }; let obj2 = { name: '时间跳跃' }; let bar = fn.call(obj1); // fn this指向obj1 bar.call(obj2); //听风是风
为啥咱们第一次绑定this并返回箭头函数后,再次改变this指向没生效呢?
前面说了,箭头函数的this取决于外层做用域的this,fn函数执行时this指向了obj1,因此箭头函数的this也指向obj1。除此以外,箭头函数this还有一个特性,那就是一旦箭头函数的this绑定成功,也没法被再次修改,有点硬绑定的意思。
捌 ❀ 总
那么到这里,对于this的五种绑定场景就所有介绍完毕了,若是你有结合例子练习下来,我相信你如今对于this的理解必定更上一层楼了。
那么经过本文,咱们知道默认绑定在严格模式与非严格模式下this指向会有所不一样。
咱们知道了隐式绑定与隐式丢失的几种状况,并简单复习了做用域链与原型链的区别。
相对隐式绑定改变的不可见,咱们还介绍了显式绑定以及硬绑定,简单科普了call、apply与bind的区别,并提到当绑定指向为null或undefined时this会指向全局(非严格模式)。
咱们介绍了new绑定以及new一个函数会发生什么。
最后咱们了解了不太合群的箭头函数中的this绑定,了解到箭头函数的this由外层函数this指向决定,并有一旦绑定成功也没法再修改的特性。
但愿在面试题中遇到this的你再也不有所畏惧,到这里,本文结束。
参考
你不知道的js中关于this绑定机制的解析[看完还不懂算我输]