你不知道的JavaScript之this指向

前言

JavaScript 对 this 指向 话题 的理解是永不过时的,鉴于 JavaScript 中 this 风骚的运作方式,本文将试图将其拆解分析,烹饪再食用~。

this is all about context.,大概意思就是:this 与当前执行上下文相关。

this 说白了就是找离自己最近的对象,即拥有当前上下文(context)的对象(context object)。

换句话说,this 与函数被调用时,调用函数的对象有关。

默认绑定,全局对象

正所谓近水楼台先得月,全局对象作为对遥远的对象是作为备胎的存在,为语言边界护城河做兜底。

一般情况下,this 指向全局对象则属于默认绑定。那么什么是默认绑定呢?

this 默认绑定,通俗地可理解为函数被调用时无任何调用前缀对象的情景,由于函数调用时无调用前缀对象或函数无特定绑定,所以非严格模式下此时 this 会指向全局对象

在非严格模式下,不同终端的全局变量对象有所区别:

  • 在浏览器端,this 指向 Window 对象
  • 在 Nodejs 环境,this 指向 global 对象
  • 在函数环境,this 指向 绑定当前函数的作用域

在严格模式下:

  • use strict 环境, this 指向 undefined

🌰 在非严格模式下

{
  /* 在非严格模式下,this默认绑定 */
  console.log('window global this: ', this); // window
  function fnOuter() {
    console.log('fnOuter: ', this); // window
  }
  function windowThis() {
    console.log('windowThis: ', this); // window
    function fnInner() {
      console.log('fnInner: ', this); // window
      fnOuter();
    }
    fnInner();
  }
  windowThis();
}

上述栗子中,无论函数声明在哪,在哪调用,由于函数调用时前面并未指定任何对象,这种情况下 this 均指向全局对象 window。

但须注意的是,在严格模式下,默认绑定下的 this 会指向 undefined。

🌰 在严格模式下,再来看几个栗子,然后在心中记下答案

{
  /* 在非严格模式下,this默认绑定 */
  var mode = '在非严格模式下,this默认绑定';
  function windowThis() {
    console.log('windowThis: ', this);
    console.log('windowThis: ', this.mode);
    function fnInner() {
      console.log('fnInner: ', this);
      console.log('fnInner: ', this.mode);
    }
    fnInner();
  }

  function windowStrictThis() {
    'use strict';
    windowThis();

    function fnInner() {
      console.log('windowStrictThis: ', this);
      console.log('windowStrictThis: ', this.mode);
    }
    fnInner();
  }

  windowStrictThis();
}

建议得出答案再看下文,

一起来倒数吧,“花栗鼠。。。。。。”。

🌸🌰🐿️
🌸🌰🐿️
🌸🌰🐿️
🌰🐿️🌸
🌰🐿️🌸
🌰🐿️🌸
🐿️🌸🌰
🐿️🌸🌰
🐿️🌸🌰

好啦,来看正确输出吧,都答对了吧~

// windowThis:  Window{}
// windowThis:  在非严格模式下,this默认绑定
// fnInner:  Window{}
// fnInner:  在非严格模式下,this默认绑定
// windowStrictThis:  undefined
// windowStrictThis:  TypeError: Cannot read property 'mode' of undefined

可见在函数内部使用严格模式声明后,this 指向变为 undefined 了,同时在函数内声明严格模式只对函数内定义的变量与函数有关,跟其调用的外部函数无关。

点石成金,隐式绑定

什么是隐式绑定呢?

this 隐式绑定:如果在函数调用的时,函数前面存在调用他的对象,那么 this 就会隐式绑定到这个调用对象上。

所以隐式绑定的关键点在于函数被调用的对象是谁,说白了就是找调用这个函数前面的点.是谁,谁就是 this 所绑定的对象了。

🌰 举个栗子:

{
  var mode = 'window';
  var boss1 = {
    mode: 'boss1',
    fn() {
      console.log(this.mode);
    },
  };
  var boss2 = {
    mode: 'boss2',
    call: boss1.fn,
    o: boss1,
  };
  boss2.o.fn(); // boss1
  boss2.call(); // boss2

  var boss1Copy = boss1.fn;
  boss1Copy(); // window
}

函数隐式绑定时,如果函数调用时存在多个对象,this 指向距离自己最近的对象,也就是 . 前面的对象是谁,this 就指向谁。

那么问题来了,如果删除 boss2 上的 mode,会有什么不一样呢?

🌰 举个栗子:

{
  var mode = 'window';
  var boss1 = {
    mode: 'boss1',
    fn() {
      console.log(this.mode);
    },
  };
  var boss2 = {
    call: boss1.fn,
    o: boss1,
  };
  boss2.call(); // undefined
}

答案是输出 undefined,因为此时由于 boss1 只是 boss2 的属性,boss1 与 boss2 的原型链各不相同相同,不属于父子关系,因此符合作用域链查找规则,所以 this 须从 boss2 上找 mode 属性,当 boss2 上不存在 mode 属性时则返回 undefined。注意不要与作用域链混淆了。

🌰 下面这个例子就要小心点咯,能想出答案么?

{
  var mode = 'window';
  var boss1 = {
    mode: 'boss1 mode',
    fn() {
      console.log(this.mode);
    },
  };
  function Fn() {}
  Fn.prototype.mode = 'Fn mode';
  Fn.prototype.fnProto = function() {
    console.log(this.mode);
  };
  var boss2 = {
    mode: 'boss2 mode',
    fn: function() {
      return boss1.fn();
    },
    proto: new Fn(),
  };
  boss2.fn(); // boss1 mode
  boss2.proto.fnProto(); // Fn mode
}

答案是 boss1 mode 和 Fn mode 哦,猜对了吗。

涉及到原型链与作用域链的以一些区别,基本这里就不做解析了,请各自查漏补缺~。

隐式绑定丢失

相信细心的同学已经发现,上述例子有一个函数赋值给变量再调用的情景。当函数赋值再调用后,原本 this 指向会发生改变,函数的 this 不会指向其原对象,从而引起隐形绑定丢失问题。

常见引起隐形丢失的方式:

  1. 函数赋值变量再调用

🌰 举个栗子:

{
  var mode = 'window';
  var boss1 = {
    mode: 'boss1',
    fn() {
      console.log(this.mode);
    },
  };
  var boss2 = {
    mode: 'boss2',
    call: boss1.fn,
    o: boss1,
  };
  boss2.o.fn(); // boss1
  boss2.call(); // boss2

  var boss1Copy = boss1.fn;
  boss1Copy(); // window
}

上述案例 boss1Copy 和 boss2.call 就是函数赋值变量再调用的情况

  1. 函数以形参传递

🌰 举个栗子:

{
  var mode = 'window';
  var boss1 = {
    mode: 'boss1',
    fn() {
      console.log(this.mode);
    },
  };
  function exce(params) {
    params && params();
  }
  exce(boss1.fn); // window
}

上述例子中我们将 boss1.fn 也就是一个函数传递进 exce 中执行,这里只是单纯传递了一个函数而已,this 并没有跟函数绑在一起,此时 this 指向原对象发送 丢失从而指向了 window。

可见,隐式丢失本质上是因为函数赋值引起的,在函数赋值给变量或另一个函数形参 Fn 后,在调用 Fn 时 this 会指向离其最近的对象。

指腹为婚,显式绑定

this 显式绑定:指通过 Object.prototype.call、Object.prototype.apply、Object.prototype.bind 方法改变 this 指向。

这里我将显式绑定细分为:

  • 显式绑定:在运行时改变 this 指向

    • call
    • apply
  • 显式硬绑定:一次绑定后,永久不能改变 this 指向

    • bind

接下来看个例子,分别通过 call、apply、bind 改变了函数 log 的 this 指向。

🌰 举个栗子:

{
  function log() {
    console.log(this.name);
  }
  var boss1 = { name: 'boss1' };
  var boss2 = { name: 'boss2' };
  var boss3 = { name: 'boss3' };

  log.call(boss1); // boss1
  log.apply(boss2); // boss2
  log.bind(boss3)(); // boss3

  var logBind = log.bind(boss3);
  logBind.apply(boss1); // boss3
  logBind.bind(boss2); // boss3
}

在 JavaScript 中,当调用一个函数时,我们习惯称之为函数调用,此时函数处于一个被动的状态;而 bind、 call 与 apply 让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用

注意,如果在使用 bind、 call 与 apply 之类的方法改变 this 指向时,指向参数提供的是 null 或者 undefined 时, this 将指向全局对象。

🌰 举个栗子:

{
  var name = 'window';
  function log() {
    console.log(this.name);
  }
  var boss1 = { name: 'boss1' };
  var boss2 = { name: 'boss2' };
  var boss3 = { name: 'boss3' };

  log.call(null); // window
  log.apply(undefined); // window
  log.bind(undefined)(); // window
}

同样值得注意的是,bind 在显式改变 this 指向之后会返回一个新的绑定函数(bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。

另外 简明 补充一下 call、apply、bind 的区别:

  • bind:函数硬绑定 this 指向并返回一个全新函数 BF,且返回的 BF 无法再次被 call、apply、bind 改变 this 指向,且需要执行 BF 才会运行函数。

    • function.bind(thisArg[, arg1[, arg2[, …]]])()
  • call:改变 this 指向的同时还会执行函数,一个以散列形式的形参。

    • function.bind(thisArg[, arg1[, arg2[, …]]])
  • apply:改变 this 指向的同时还会执行函数,可接受一个数组形式的形参。

    • function.apply(thisArg,[param1,param2…])

call & apply 主要区别在于传参形式不同,在传参的情况下,call 的性能要高于 apply,因为 apply 在执行时还要多一步解析数组。

内有乾坤,new 绑定

严格来说,JavaScript 中的构造函数只是使用 关键字 new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。

一个比较容易忽略的会绑定 this 指向 的方法就是使用 new。当我们 new 一个函数时,就会自动把 this 绑定在新对象上,然后再调用这个函数。new 会覆盖 bind 的绑定让其无法生效

那么 new 对函数到底起到什么作用呢,大致分为三步:

  1. 创建一个空的简单 JavaScript 对象(即{});
  2. 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤 1 新创建的对象作为 this 的上下文 ;
  4. 如果该函数没有返回对象,则返回 this。

这个过程我们称之为构造调用。

🌰 那么 new 在构造调用时对 this 产生什么影响呢?请看栗子:

{
  function log() {
    console.log(this);
  }
  log(); // window{}
  new log(); // log{}

  var boss1 = { name: 'boss1' };
  log.call(boss1); // boss1{}
  new log.call(boss1); // Uncaught TypeError: log.call is not a constructor
  new log.bind(boss1); // Uncaught TypeError: log.call is not a constructor

  var logBind = log.bind(boss1);
  logBind(); // boss1{}
  new logBind(); // log{}
}

当 new 一个函数时,this 会被绑定为函数本身,即使函数在 bind 改变 this 指向的情况下,关键字 new 依旧会将 this 指向为函数本身。且 new 绑定与显式绑定互不兼容。

军令如山,箭头函数

ES6 的箭头函数是另类的存在,为什么要单独说呢,这是因为箭头函数中的 this 不适用上面介绍的几种绑定规则。

准确来说,箭头函数中没有 this,箭头函数的 this 指向取决于外层作用域中的 this,外层作用域或函数的 this 指向谁,箭头函数中的 this 便指向谁。

因为箭头函数里的 this 是永远指向到当前词法作用域(Lexical this)之中 ,在代码编码时就可以确定。没有其它 this 绑定方式可以覆盖。

这样的好处就是方便让回调函数的 this 使用当前的作用域,不怕引起混淆。

所以对于箭头函数,只要看它在哪里创建的就行。

🌰 有点吃软饭的嫌疑,一点都不硬朗,我们来看看栗子:

{
  function fn() {
    return () => {
      console.log('efnArrow: ', this);
    };
  }
  function callback(cb) {
    cb();
  }
  var boss1 = {
    name: 'boss1',
    fn() {
      console.log('fn: ', this);
    },
    fnArrow: () => {
      console.log('fnArrow: ', this);
    },
    ret() {
      return function() {
        console.log('ret: ', this);
      };
    },
    retArrow() {
      return () => {
        console.log('retArrow: ', this);
      };
    },
    cb() {
      callback(function() {
        console.log('cb: ', this);
      });
    },
    cbArrow() {
      callback(() => {
        console.log('cbArrow: ', this);
      });
    },
  };
  var boss2 = {
    name: 'boss2',
    fn: boss1.retArrow,
  };

  boss1.fn(); // fn: boss1{}
  boss1.fnArrow(); // fnArrow: window{}
  boss1.ret()(); // ret: window{}
  boss1.retArrow()(); // retArrow: boss1{}
  boss1.cb(); // cb: window{}
  boss1.cbArrow(); // cbArrow: boss1{}

  boss1.fn.call(boss2); // fn: boss2{}
  boss1.fnArrow.call(boss2); // fnArrow: window{}
  boss1.ret.call(boss2)(); // ret: window{}
  boss1.retArrow.call(boss2)(); // retArrow: boss2{}
  boss1.ret().call(boss2); // ret: boss2{}
  boss1.retArrow().call(boss2); // retArrow: boss1{}
  boss1.cb.call(boss2); // cb: window{}
  boss1.cbArrow.call(boss2); // cbArrow: boss2{}

  var bar = boss1.retArrow.call(boss2);
  bar(); // returnArrowLog: boss2{}
  bar.call(boss1); // returnArrowLog: boss2{}
  bar.bind(boss1)(); // returnArrowLog: boss2{}
}

对 boss1.retArrow 为啥我们第一次绑定 this 并返回箭头函数后,再次改变 this 指向没生效呢?

前面说了,箭头函数的 this 取决于外层作用域的 this,boss1.retArrow 函数执行时 this 指向了 boss1,所以箭头函数的 this 也指向 boss1。除此之外,箭头函数 this 还有一个特性,那就是一旦箭头函数的 this 绑定成功,也无法被再次修改,有点硬绑定的意思。

当然,箭头函数的 this 也不是真的无法修改,我们知道箭头函数的 this 就像作用域继承一样从上层作用域找,因此我们可以修改外层函数 this 指向达到间接修改箭头函数 this 的目的,如 boss1.retArrow.call(boss2)()成功将 this 指向 boss2。

总结

文章到这里,对于 this 的几种绑定场景就全部总结完毕了,如果你有跟着例子一起练习下来,相信聪明的你现在对于 this 的理解一定更上一层楼了。

那么通过本文,我们学习了默认绑定在严格模式与非严格模式下 this 指向会有所不同。

也知道了隐式绑定的触发条件与隐式丢失的几种情况。

相对于隐式绑定的不可见,我们还学习到显式绑定以及硬绑定,并提到非严格模式下,当绑定指向为 null 或 undefined 时 this 会指向全局。

接着重新认识了 new 绑定。

最后我们了解了不太合群的箭头函数中的 this 绑定,了解到箭头函数的 this 由外层作用域的 this 指向决定,并有一旦绑定成功也无法再修改的特性。

好了,内容如有不妥之处请指正,希望你能受益良多~

相关文献

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值