一文打尽this,对执行上下文说Yes

this到底指向谁

  关于this指向的具体细节和规则后面再慢慢分析,这里可以先“死记硬背”一下几条规律:

  • 在函数体中,非显式或隐式地简单调用函数时,再严格模式下,函数内的this会被绑定到undefined上,再非严格模式下则会被绑定到全局对象window上。
  • 一般使用new方法调用构造函数时,构造函数内的this会被绑定到新创建的对象上。
  • 一般通过call/apply/bind方法显式调用函数时,函数体内的this会被绑定到指定参数的对象上。
  • 一般通过上下文调用函数时,函数体内的this会被绑定到该对象上。
  • 在箭头函数中,this的指向时由外层(函数或全局)作用域来决定的。

实战例题分析

全局环境中的this

  function f1() {
    console.log(this);
  }
  function f2 () {
    'use strict'
    console.log(this);
  }
  f1(); //Window
  f2(); //undefined

  函数在浏览器全局环境中被简单调用,在非严格模式下this指向window,在通过'use strict'指明严格模式的情况下this指向undefined

  const foo = {
    bar: 10,
    fn: function () {
      console.log(this);
      console.log(this.bar);
    }
  }
  var fn1 = foo.fn;
  fn1(); //Window  undefined

  这里的this还是指向window。虽然fn函数在foo对象中用来作为对象的方法,但是在赋值给fn1之后,fn1仍然是在window的全局环境中执行的。
  将上面的代码调用改为一下形势:

  const foo = {
    bar: 10,
    fn: function () {
      console.log(this);
      console.log(this.bar);
    }
  }

  foo.fn(); //{bar: 10, fn: ƒ} 10

  这时this指向的是最后调用它的对象,在foo.fn()语句中,this指向foo对象。请记住,在执行函数时不考虑显式绑定,如果函数中的this是被上一级的对象调用的,那么this指向就是上一级对象;否则指向全局环境。

上下文对象调用中的this

  参考以上结论,运行下面代码,最终将会返回true

  const student = {
    name:'mgd',
    fn: function () {
      return this;
    }
  }
  console.log(student.fn() === student); //true

  当存在更复杂的调用关系时 ,如以下代码中的嵌套关系,this会指向最后调用它的对象,代码输出是mgd

  const person = {
    name: 'lpb',
    brother: {
      name: 'mgd',
      fn: function () {
        return this.name;
      }
    }
  }
  console.log(person.brother.fn()); //mgd

  至此,this的上下文对象调用已经介绍的比较清楚了。再看一道高阶题目:

  const o1 = {
    text: 'o1',
    fn: function () {
      return this.text;
    }
  } 
  const o2 = {
    text: 'o2',
    fn: function () {
      return o1.fn();
    }
  }

  const o3 = {
    text: 'o3',
    fn: function () { 
      var fn = o1.fn;
      return fn();
     }
  }

  console.log(o1.fn());
  console.log(o2.fn());
  console.log(o3.fn());

  答案是o1o1undefined,你猜对了吗?下面来分析一下:

  • 第一个console输出o1很简单,难点在第二个和第三个console上,关键还是看调用this的那个函数
  • 第二个console中的o2.fn()最终调用的还是o1.fn(),所以结果为o1
  • 第三个console中的o3.fn()通过var fn = o1.fn;的赋值进行了“裸奔”调用,这里的this指向window,运行结果是undefined

  假如你在面试时已经能回答到这些了,面试官可能会追问:需要让第二个console打印o2怎么做?
  如果你回答bindcallapply来对this进行干预,面试官接着就会问你如果不用这些方法呢?
  回答肯定是有的,这个问题考察的是对基础知识的掌握深度和编程思维,方法如下:

  const o1 = {
    text: 'o1',
    fn: function () {
      return this.text;
    }
  } 
  const o2 = {
    text: 'o2',
    fn: o1.fn
  }
  console.log(o2.fn()) // o2

  以上方法同样使用了那个重要的结论, this指向最后调用它的对象。在上面的代码中,我们提前进行了赋值操作,将函数fn挂载到o2对象上,fn最终作为o2对象的方法被调用。

通过bind、call、apply改变this指向

  与之相关的常见的考察点是:call/bind/apply的区别
  这样的问题相对基础,直接上答案:它们都是用来改变相关函数this指向的,但是callapply是直接进行相关函数的调用的;bind不会执行相关函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的this指向,可以手动调用它。如果再说具体一点,就是callapply之间的区别主要体现在参数设定上,详情请阅读红宝书第四版。
  下面看一道例题并分析:

  const foo = {
    name: 'mgd',
    logName: function () {
      console.log(this.name);
    }
  }

  const bar = {
    name: 'lpb'
  }

  foo.logName.call(bar);

  以上代码的执行结果为lpb,这不难理解。但是对call/bind/apply的高级考察往往需要面试者结合构造函数及组合来实现继承。

构造函数和this

  先上代码,带着问题去思考:

  function Foo () {
    this.bar = 'mgd';
  }
  const instance = new Foo();
  console.log(instance.bar); //mgd

  这样的场景往往伴随着一个问题:new操作符调用构造函数时做了什么呢?以下给出简单回答,仅供参考

  • 创建一个对象
  • 将构函数的this指向这个新的对象
  • 为这个对象添加属性、方法等。
  • 最终返回新的对象。

上述过程用代码表述:

  var obj = {};
  obj.__proto__ = Foo.prototype;
  Foo.call(obj);

  需要指出的是,如果在构造函数中出现显式return的情况,那么需要注意,其可以细分为两个场景:

  1. 执行下面代码输出undefined,此时instance返回的是空对象o
  function Foo () {
    this.user = 'mgd';
    const o = {};
    return o;
  }
  const instance = new Foo();
  console.log(instance.user); //undefined
  1. 执行下面代码输出mgd,也就是说inatance此时返回的是目标对象实例:
  function Foo () {
    this.user = 'mgd';
    return 1;
  }
  const instance = new Foo();
  console.log(instance.user); //mgd

  所以,如果构造函数中显式返回一个值,且返回的是一个对象(复杂类型),那么this就指向这个返回的对象;如果返回的不是一个对象(基本类型),那么this指向实例。

箭头函数中的this

  在箭头函数中,this的指向是由外层(函数或全局)作用域来决定的。
  下面这段代码中的,this出现在setTimeout中的匿名函数中,因此this指向window对象:

  const foo = {
    fn: function () {
      setTimeout(function() {
        console.log(this); 
      });
    } 
  }
  foo.fn() //Window 

  如果需要让this指向这个对象,则可以使用箭头函数来完成,代码如下:

  const foo = {
    fn: function () {
      setTimeout(() => {
        console.log(this); 
      });
    } 
  }
  foo.fn() //{fn: ƒ}

  单纯的箭头函数中的this指向问题非常简单,但是如果综合所有情况,并结合this的优先级进行考察,那么这时的this指向并不容易确定

this优先级

  通常把callbindapplynewthis进行绑定的情况称为显式绑定,把根据调用关系确定this指向的情况称为隐式绑定。
  那么显式绑定和隐式绑定谁的优先级更高?下面揭晓:
  执行下面的代码:

  function foo (a) {
    console.log(this.a);
  }

  const obj1 = {
    a: 1,
    foo: foo
  }

  const obj2 = {
    a: 2,
    foo: foo
  }

  obj1.foo.call(obj2);
  obj2.foo.call(obj1);

  输出分别是2、1,也就是说:callapply的显式绑定一般来说优先级更高。 再看下面的代码:

  function foo (a) {
    this.a = a;
  }

  const obj1 = {};

  var bar = foo.bind(obj1);
  bar(2);
  console.log(obj1.a);

  上述代码通过绑定bindbar函数中的this绑定为obj1对象。执行bar(2)后,obj1.a值为1,即执行bar(2)后,obj1对象为{a:2}
  当再使用bar作为构造函数时,例如下面的代码,则会输出3:

  var baz = new bar(3);
  console.log(baz.a);

  bind函数本身是通过bind方法构造的函数,其内部已经将this绑定为obj1,当它再次作为构造函数通过new被调用时,返回的实例就已经与obj1解绑了。也就是说,new绑定修改了bind绑定中的this指向,因此new绑定的优先级比显式bind绑定的更高。
  再来看一段代码:

  function foo () {
    return a => {
      console.log(this.a);
    }
  }

  const obj1 = {
    a: 2
  }

  const obj2 = {
    a: 3
  }

  const bar = foo.call(obj1);
  bar.call(obj2);

  输出结果为2.由于foo中的this绑定到了obj1上,所以bar(引用箭头函数)中的this也会绑定到obj1上,箭头函数的绑定无法被修改。
  如果将foo写成如下箭头函数的形式,则会输出123:

  var a = 123;
  const foo = () => a => {
    console.log(this.a);
  }
  const obj1 = {
    a: 2
  }
  const obj2 = {
    a: 3
  }
  var bar = foo.call(obj1);
  bar.call(obj2); //123

  将上面代码中第一处变量a的声明修改一下,即变成一下这种,猜一猜结果是什么?

const a = 123;

  答案为undefined,原因是const声明的变量不会挂载到window全局对象上。因此,this指向window时,自然找不到a变量了。

开放性例题分析

实现一个 bind函数:

  Function.prototype.bind = Function.prototype.bind || function (context) {
    let that = this;
    let args = Array.prototype.slice.call(arguments, 1);
    return function bound () {
      let innerArgs = Array.prototype.slice.call(arguments);
      let finalArgs = args.concat(innerArgs);
      return that.apply(context, finalArgs);
    }
  }

  这样的实践已经非常不错了。但是,就如之前在this优先级分析那里所展示的规则:bind返回的函数如果作为构造函数搭配new关键字出现的话,绑定的this就会被忽略。
  为了实现这样的规则,开发者需要考虑如何区分这两种调用方式。具体来讲就是要在bound函数中进行this instanceof判断。
  另外一个细节是,函数具有length属性,用来表示形参的个数。在上述实现方式中,形参的个数显然会失真。所以,改进的实现方式需要对length属性进行还原。可是难点在于,函数的length属性值是不可重写的。

总结

  我们看到this的用法纷繁多象,趋势不容易彻底掌握,需要在阅读之外继续消化吸收。只有‘死记’,才能‘活用’。一个优秀的前端工程师,不完全在于回答面试题目的正确率,更在于如何思考问题、解决问题。如果不懂this指向,那就动手实践一下;如果不了解原理,就翻出规范看一下。与诸君共勉,希望在不久的将来,能彻底掌握this

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值