JS 关于this p9

关于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为基础》,加上大部分自己的理解,顺便做个记录,加深印象)