关于this这个货,常常让我感到头疼,也很难说清这货到底是什么机制,今天就详细记录一下this,了解他就跟理解闭包差不多,不理解的时候我们会感到很难受总想着避开他,当我们真正理解之后,会有种茅塞顿开的感觉,但是也不要掉以轻心,说不定哪天又给来一脚~
先看一个例子,之前的博客中也提过到的this使用:
function fn(){ console.log(this.a) } var a = 2; var o = {a:7}; // 使用之前讲到的apply fn.apply(o); // 7 fn();// 2
那么this那么简单好用么,当前不是,this是比较复杂的机制,有很多规则,不小心的话很会难受。
一、抛开上面的例子,对于this我们平时会有一些误解:
1.指向自身:
function fn(){ console.log(this.a); this.a ++; } fn.a = 0; fn(1); console.log(fn.a);//0
我们预期是输出1,因为fn被调用了1次,并且那么a++ 会导致 变成1,但是最终却是0,如果下面再来一句~
var a = 0; fn(1); console.log(a);//1
this.a 实际改变的是全局作用域a,所以例子中的 this 并没有指定为所包含的这个函数当中 = =。
如果非要调用自身,可以采用具名函数的方式(不要使用arguments.callee,在上一章提到了,被废弃的方法我们还是不要接触了):
function fn(){ fn.a ++; } fn.a = 0; fn(); fn.a;//1
或者,使用apply或者call
function fn(){ this.a ++; } fn.a = 0; fn.call(fn); fn.a;//1
说一下第一个例子为毛会是0,this 在当时指向的是全局作用域,而不是函数本身,当调用fn()方法时,this.a 会在全局作用域中声明一个 a 并执行 ++,就像下面这样
function fn(){ this.a ++; } fn(); console.log(a); // NaN (因为a只是声明,当执行RHS的时候并没有a,那么undefined+1 会是啥? NaN)
所以:this 并不是指向自身。
2.作用域
function fn(){ var a = 0; this.fn2(); } function fn2(){ console.log(this.a) } fn(); // undefined
首先this.fn2() 确实找到了fn2,在fn2中怎么会找到a=0呢,按照之前说的,它也只是在全局作用域中创建了一个a而已(实际上都没有创建,因为此处只有LHS 没有RHS,详细了解的话到之前的文章中看一下作用域,在此调用成功就当成是个意外吧 = =)。
那么var a 把它理解为私有的属性,而在fn2中想要用fn的私有属性怎么可能呢? 这段代码实际上想通过词法作用域的概念来用来fn中的a,但this并不会查到啥,除非这样(平常使用的比较多的):
function fn(){ var a = 0; fn2(a); } function fn2(a){ console.log(a) }
fn();
fn2中的a通过RHS 查询在上一层找到了 0。
so、this和词法作用的查找是冲突的,不要再想着这样用了,忘了它吧~~
那么下一句你可能在很多地方都看到过:
this实际上是在函数被调用时发生的绑定,它的指向取决与函数在哪里被调用。
二、调用位置:
如何寻找位置,先告诉你,上一层或者说最后一层~ 别急着想像,看代码:
function fn(){ fn2(); } function fn2(){ console.log('fn2'); } fn();// fn2
fn的调用位置在全局作用域下,fn2的调用位置在fn下(fn2的调用栈就是 fn - > fn2)。
所以,明白了调用位置了? 如果是多层,还有一种办法通过强大的浏览器开发者界面,如下图:(多加了一个fn3,这样可能更清晰一点)
三、绑定规则:
在调用栈中找了调用位置,接下来看看绑定规则:4种
优先级为:new > 显式 > 隐式 > 默认(为啥最后说)
1.默认规则
function fn(){ console.log(this.a); } var a =2; fn(); //2
这个例子上面基本上都用到了,它用到的时候默认规则,this指向的是全局对象(并不是一定指向全局对象的哦,后面会说到)
因为fn直接调用fn(),我们或许也可以这样理解 window.fn() ,this.a 指向window.a ,输出2~ 是不是很好理解(有点像隐式绑定这样说~)
但是有一点是需要注意的是,严格模式下不适用
function fn(){ "use strict"; console.log(this.a); } var a = 2; fn(); // TypeError: Cannot read property 'a' of undefined
this不会默认绑定到window上,除非~:
window.fn();//2
真正意义上用到了隐式绑定~~(别急,马上就说隐式绑定)
还有很重要的一点,严格模式下默认绑定只会关注函数体内部,不会关注被谁调用,像下面这样,是可以使用的:
function fn(){ console.log(this.a); } var a = 2; function fn2(){ "use strict"; fn(); } fn2();//2
2.隐式绑定:
是否调用位置有上下文对象或者是上下文对象,或者说被某个对象包含或拥有:
function fn(){ console.log(this.a); } var o = { a:2, fn: fn } // o.fn = fn; o.fn();//2
不管是先定义或者是后引用,在此隐式绑定的规则会把函数调用中的this 绑定到这个上下文对象。(回顾一下,在这里o的fn拥有所在作用域o的闭包或者说行使权、使用权,把它看成 o = {a:2,fn:function(){ console.log(this.a) }})
在声明一次:对象引用链只有上一层或则最后一层在调用位置起作用。
function fn(){ console.log(this.a) } var o = { a:0, fn:fn } var o2 = { a:2, o:o, fn:o.fn } o2.o.fn(); // 0 (绑定的是o) o.fn();//2 (这里o2的fn为函数本身,与o没有直接关系,所以this绑定的是o2这个对象,或者说叫隐式丢失)
tip:隐式丢失
// 跟上面的例子一个意思 function fn(){ console.log(this.a) } var o = { a:2, fn : fn } var x = o.fn; x();//undefined
o.fn 引用的是函数本身,并没有执行fn函数,所以x知识引用了fn函数,当执行x函数时,应用了到了上面说到的默认绑定,(在全局作用域声明了a,但是没有赋值),好吧,怕忘记了,如果像下面这样就更清晰了
// 在上面例子的基础上加2句 var a =7; x();//7
再来一个,参考书《你不知道的javascript》:
function fn(){ console.log(this.a) } function fn2(f){ f(); } var o = { a:2, fn:fn } fn2(o.fn); // undefined
fn2执行的f 是fn函数本身,跟o没有毛关系,所以最终也是使用了默认绑定。
还有一种就是window内置对象,跟上面结果一样,在此就不写例子了。
3.显示绑定
这个可能最好理解,就是指定this要绑定的上下文对象,主要用到的就是 apply、call、bind,关于这3个货,想看的可以看看之前的文章 JS 关于 bind ,call,apply 和arguments p8
这里主要说一个概念:如果你传入一个原始值比如:“”、1、true,当作this 的绑定对象,这个值会转为它的对象形式(new String()、new Number()、new Boolean()),称为装箱。
function fn(){ console.log(this.a); } fn.call({a:2});//2 fn();// undefined
如上面看到的显示绑定也不会解决丢失绑定的问题。
但是我们可以通过硬绑定来解决这个问题:
function fn(){ console.log(this.a) } var o = { a:2 } function fn2(){ fn.call(o); } fn2();//2
这样调用fn2的时候都会默认显示绑定。
它的典型行为是:创建一个包裹函数,负责接收参数并返回值。
看这个例子:
function fn(f){ console.log(this.a); console.log(f); } var o = { a:2 } function fn2(){ fn.apply(o,arguments) } fn2(6); // 2 // 6
跟上一个例子差不多,这里利用了arguments内置对象来传递参数。
还有一种是创建辅助函数:
function fn(f){ console.log(this.a); console.log(f); } var o = { a:3} function fn2(){ return function (){ fn.apply(o,arguments); } } var fn3 = fn2(); fn3(8); // 3 // 8
对比上一个例子,一个是立即执行,另一个是返回绑定后的函数本身再进行调用。或者使用bind也行
function fn(f){ console.log(this.a); console.log(f); } var o = { a:3} function fn2(){ return fn.bind(o); } var fn3 = fn2(); fn3(5); // 3 // 5
另外关于API 调用上下文在实际应用中有需要函数上就是通过call 与apply 实现了显示绑定,比如[].forEach();
4.new 绑定
首先要说的是,new不会实例化某个类(和我之前的说法有些冲突,但是实例我们会比较好理解),因为他们是被new操作符调用的普通函数。
类似这种 new String() ,正确的说法叫做“函数调用”,因为实际上js中并不存在构造函数之说,只存在函数调用。
上面是官方一点的语言,其实我们只需要知道这几点暂时:
new 会创建一个全新的对象,并且这个对象会绑定到函数调用的this,如果这个函数没有return 那么就返回这个函数本身的新对象~
function fn(){this.a = 3; } var fn2 = new fn(); fn2.a;//3
如上,new出来的新对象fn2 绑定到了fn的this上,是一个全新的对象(这跟之前的文自定义创建对象中说到的一样,如果为私有变量,则不会拥有它,或者它看不到,但是可以使用它比如下面这种:)
function fn(){ this.a = 3; var b = 4; this.fn2 = function(){ console.log(b) ; } } var fn3 = fn(); console.log(fn3);// {a: 3, fn2: ƒ} fn3.fn2();//4
这里除了this,还有闭包的相关概念,在此就不多说了。大家只要知道this绑定到了新对象上(全新的)。
好了,4种绑定说完了,接下来说下优先级:
function fn(){ console.log(this.a) } var o = {a:2,fn:fn}; var o2 = {a:5,fn:fn} o.fn();//2 o.fn.call(o1);//2
那么看显示绑定应该是优先于隐式绑定的,通过最后一行可以看出来(这里我感觉有点不好理解,或者我们可以这样理解,o.fn() 是显示绑定this所以从调用位置来看,上下文o的a为2所以this.a输出的为2;o.fn 为函数本身,所以在对函数本身进行显示绑定,所以this绑定到了o2上面)
并且显示绑定和隐式绑定都会丢失this(上面提到的)。
看下一个new 绑定和隐式绑定:
function fn(f){ this.a = f } var o = { fn:fn } var o2 ={} o.fn(0); console.log(o.a);//0 o.fn.call(o2,3); console.log(o2.a);//3 var fn2 = new o.fn(5); console.log(o.a);//0 console.log(o2.a);//3 console.log(fn2.a);//5
o.fn(0) 为隐式绑定,this绑定到o 上,o.a 为0 这点不用多说。
o.fn.call(o2,3); fn函数本身中this被显示绑定带o2上,o2对象获得a并为3;
最后fn2为一个new出来的新对象,this绑定到这个新对象上(上下文),它的a为5。(因为函数就是对象)
不是很明显,下面来一个(比较new 和显示绑定):
function fn(f){ this.a = f } var o = {} var x = fn.bind(o) x(1); o.a;//1 var y = new x(3) y.a;//3
个人感觉在判断优先级时,不能只是记住哪种规则优先级高,而是需要仔细分析,还是理解最重要
比如:
o.fn.call() ,首先o是一个对象,o对象中包涵的函数fn 在这里并没有调用它,按照之前所说,它只是表示函数本身,那么在调用call的显示绑定并执行了该函数,那么this肯定会绑定到显示绑定的第一个参数上,所以是显示优先
var fn2 = new o.fn(); 记住最关键的那句,new会创建一个新对象并绑定到this上,这样就不会迷糊了,o.fn 是函数本身,并且创建一个新对象,那么fn2 是一个全新的对象,所以这里就是new 优先
(this与call无法同时使用但是可以用bind)
var fn2 = fn.bind(o);
var bar = new fn2();
纵使怎么变,fn2是显示绑定没错,如果像上面例子 fn是这样的 function(f){ this.a = f },那么fn2.a 肯定是o的a,
但是bar 声明使用new 绑定,那么会创建一个新对象~新对象~新对象,所以,fn2里如果加上一个参数比如 new fn2(3) ,那么新声明的bar.a 肯定就是3~
不要被所谓的优先级弄晕了,记住这几条重要的规则,管它怎么变,相信都能找到最终的那个this。
这里有一个概念:第一个参数用于绑定this,剩余的参数用于传递给下层函数的这种行为被称为“部分应用”、或者“柯里化”。(bind、apply、call)
那么这里对于判断this绑定的是什么就很好查了:
1.先看new
2.再看call、apply、bind
3.看隐式调用 o.fn()
4.啥都没,那就是默认绑定(官方的语言是,如果在严格模式下,就绑定到undefined,否则绑定到全局对象)
但是:
如果把null 或则undefined作为this的绑定对象传给apply的话,嘿嘿~
调用的时候会被忽略~
function fn(){ console.log(this.a) } fn.apply(null);// undefined
其实就等于直接调用 fn()罢了。
当然我们可以另类的用这种机制:
function fn(){ for(let i = 0 ;i<arguments.length;i++){ console.log(arguments[i]); } } fn.apply(null,[1,2]); // 1 // 2
是的,可以用来做展开数组(但是这里看着没必要)(在es6里可以通过...来解决展开数组的问题,像这样fn(...[1,2]))
如果使用null 作为柯里化的这种操作很危险,为啥,看下面:
function fn(a){ this.a = a; } fn.call(null,2); console.log(a);//2
默认绑定使全局作用域的a 赋值了2(成功进行了RHS 查询)。
如果非要使用的话:可以使用空对象,如下
function fn(a){ this.a = a; console.log(this.a) } var n = Object.create(null); fn.call(n,2); console.log(a);// a is not defined // 或者使用严格模式也未尝不可,但是代码中混用严格模式与懒惰模式真的会很不好维护~~ function fn(a){ 'use strict'; this.a = a; } fn.call(null,2) ; // Uncaught TypeError: Cannot set property 'a' of null
拓展一下Object.create()
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 (请打开浏览器控制台以查看运行结果。)
另外:间接引用也会使用默认绑定
function fn(){ console.log(this.a) } var o = {a:2,fn:fn} var o2 = {} var x = o.fn; x();// undefined (o2.fn = o.fn)();//undefined
这里其实很简单,按照之前说的,o.fn 是函数本身,并没有进行绑定~
还有一种绑定叫做“软绑定”,可以给默认绑定指定一个全局对象和undefined的值,同事保留隐式绑定或者显示绑定this的能力~
说实话,平时我们不太会用到,了解一下就好,软绑定在内置方法中并不存在,如果想要使用,必须自己实现,下面给出官方的例子:
if(!Function.prototype.softBind){ Function.prototype.softBind = function (obj){ var fn=this; var curried = [].slice.call(arguments,1); var bound = function(){ return fn.apply((!this||this===(window||global))?obj:this,curried.concat.apply(curried,arguments)); };
bound.prototype = Object.create(fn.prototype); return bound; }; }
检查调用对象this绑定的对象到底是谁?如果是window或者undefined、null之类的那么this绑定就交给参数对象obj去处理,相反
如果不是,则交给this本身去处理,方法的最后一步把fn 也就是this的原型保留并传给新声明的还说bound并返回~ 😵
function fn(){console.log(this.a)} var o = {a:0}, o1 = {a:1}, o2 = {a:2}; var x = fn.softBind(o); x();// 0 o1.fn = fn.softBind(o); o1.fn();// 1 ~ 绑定的是o 但是最终o1为1不是0 // 为了与bind区分下面来一个bind var y = {} y.fn = fn.bind(o); y.fn() // 0
bind 是显示绑定,并返回一个显示绑定后的函数(this已绑定,不是函数本身),所以y.fn() 中的this.a为显示绑定对象o中的a 也就是0。
那么软绑定:
如果this绑定到全局对象或者undefined,那么把默认对象交给this (x=fn.softBind)这里,因为x() 默认其实就是 window.x(), 调用对象是window所以,在执行x()没有使用默认绑定,而是交给了obj也就是传给softBind的o去处理。
因为o1.fn = fn.softBind(o),再看fn.softBind(o), 返回的方法交给了o去处理,但是调用o1.fn 时,调用对象o1 并不是window,所以交给了o1 去处理,也就是使用了隐式绑定。
那么显示绑定呢:
o2.fn = fn.softBind(o); o2.fn();//2
同上面一句话,我就不多打一遍了。
软绑定我们平时用的很少~ 没事就不要用了,省得跟bind 搞晕掉了 = =~~
五、胖函数(箭头函数)对this的影响
箭头函数跟let一样会劫持所在的块作用域{....},是隐式的或者说不会干扰父级,在这里,它并不是使用以上4中绑定this的规则,而是根据外层作用域来决定this由谁绑定~!
function fn(){ console.log(this.a); return ()=>{ console.log(this.a); } } var o = {a:2} var x = fn.bind(o) var y = x();//2 一切正常fn的this绑定到o的a y(); // 2 胖箭头里的this 也绑定了o?
按照之前所说,bind 只会绑定函数的作用域,而不会管子孙的死活,像是下面这样
function fn(){ console.log(this.a); return function (){console.log(this.a)} } var o = {a:2} var x = fn.bind(o) var y = x();//2 y();// undefined
但是胖箭头打破了这种规则,而且谁都不鸟~并且,箭头函数在绑定后,无法被修改,及时new 也不行
function fn(){ console.log(this.a); return ()=>{ console.log(this.a); } } var o = {a:2} var x = fn.bind(o); var y = x(); //2 y();//2 // 下面开始装了,根本不鸟你显示绑定 y.call({a:5});//2
在这里,箭头函数更适合回调函数~比如定时器等等,可以根据外层(词法作用域)来绑定this。
另外:
其实之前降到的var self = this; 与之相似,道理都是一样的。
如果在代码中你觉得用self = this 用的爽,那就不要考虑用箭头函数,
如果你觉得直接用this显得niuX,那么如果遇到类似情况,可以使用胖箭头 = =~
结束。(文章主要以书《你不知道的javascript为基础》,加上大部分自己的理解,顺便做个记录,加深印象)