this关键字是JavaScript函数外部的一个对象,this是一个指针,指向调用函数的对象。看似简略的定义但却因为在解析this援用过程中可能波及到执行上下文、作用域链、闭包等简单的机制,导致this的指向问题变得异样简单。首先必须明确一点,任何简单的机制都不可能轻而易举的学懂弄通,因而,本文将与大家一起急躁回顾this对象,演绎总结this的援用机制,心愿对你有所帮忙。

一、函数到底执行了没?

要向弄懂this对象,必须先搞懂函数是什么时候执行?
先看看简略的一个例子(例1):

function fn(){    console.log('你好');}fn;fn();let f = fn;f();

下面的例子一共输入 2 次 你好
fn()f()表达式中函数被调用执行,fnlet f=fn表达式中函数并未被调用执行。

函数执行次要看是否存在函数名(),应用不带括号的函数名会拜访函数指针,并非调用该函数

再看看一个退出闭包机制的例子(例2):

function fn(){    let hi = '你好'    return function gn(){        console.log(hi);    }}fn;fn();let f = fn;f();let g = fn();g();

下面的例子仿佛较为简单了,那一共输入多少次 你好?

  1. 输入 你好 必须是函数 gn 被调用执行,因而关键在于函数 gn 什么时候被调用?
  2. 依据前一个例子,表达式fnf=fn 没有调用函数 fn,则更不会调用函数 gn
  3. 依据前一个例子,表达式fn()f() 是雷同的含意,均调用了函数fn。在闭包中,调用fn返回返回一个函数gn的函数指针,但最终并没有通过该函数指针调用gn,因而在表达式fn()f()g=fn()并没有执行函数gn
  4. 表达式g=fn(),能够将函数gn赋值给g,最初通过g()实现对函数gn的调用执行。相似于:

    let hi = ‘你好’;let g = function (){        console.log(hi);     } g();//函数执行

    因而最终该例子仅输入一次 你好


最初看看一个对象外部的函数调用例子(例3):

let o = {    hi:'好难呀',    fn: function () {        let hi = '你好'        return function gn() {            console.log(hi);        }    }}o.fn;o.fn();let f = o.fn;f();let g = o.fn();g();

这个例子中,一共输入多少次 你好?

其实无论函数放到对象外部定义还是内部定义,均能够采纳前一个例子的剖析步骤解析函数被调用执行的过程,因而,本例子中也仅输入一次 你好

全局环境中定义的function,则该函数主动成为window对象的办法,即全局环境下的fn()调用等价于window.fn()调用。

二、神奇的this

what,下面讲了一大堆的都还没有讲到this

别急,了解this的援用机制,我认为最要害的是了解函数 执行 的上下文。假使连函数什么时候执行都傻傻搞不清,那了解this对象更无从谈起,来,咱们开始持续摸索。

红宝书第四版将this对象论述为:

1.在规范函数中,this援用的是把函数当成办法调用的上下文对象
2.在箭头函数中,this援用的是定义箭头函数的上下文

(一)规范函数的this对象

一般函数(除箭头函数)外部中均有一个this对象。this在函数执行时确定所指对象。这里有两个关键点:执行时对象

  1. 一般函数在执行时能力确定this。那在定义时能确定码?不行!记住:函数执行时确定!函数执行时确定!函数执行时确定!
  2. 一般函数的this指向的是调用该函数的对象。那能够指向其余函数吗?能够指向原始数据类型吗?通通不行!记住:指向调用该函数的对象、指向调用该函数的对象、指向调用该函数的对象

尽管很多文章对this的援用分了状况探讨,但我仍旧认为了解上述两个关键点是最重要的。来,咱们通过例子进一步剖析,以下将依照掘金文章:嗨,你真的懂this吗?的分类规范进行探讨。

默认绑定

默认绑定简略说就是在全局环境中执行函数,即没有任何对象间接调用该函数。这种状况下,函数的this将指向window(非严格模式)或为undefined(严格模式)

//非严格模式下,this指向windowfunction fn1(){    console.log(this);}//严格模式下,this为undefinedfunction fn2(){    'use strict'    console.log(this);}

简略吧,可是你能精确的判断出函数是在全局环境中执行的么?请看上面例子(例4):

var hi = 'window'let o = {    hi: '对象',    gn: function (){        let hi = '函数';        console.log(this.hi);    },    fn: function () {        let hi = '函数'        return function (){            let hi = '闭包函数';            console.log(this.hi);        };    }}o.gn();let f = o.fn();f();

一旦波及对象外部办法、闭包等机制,就会导致问题变得复杂许多。你能看出一共输入了几次?有多少次输入是在全局环境中执行的呢?

依照前一章节剖析,能够晓得一共有两次输入(若是不了解能够回看第一节),别离为表达式o.gn()f()输入对象,window。剖析如下:

  1. 表达式o.gn()显然是通过对象o对函数gn进行调用,因而gn执行时的this所指向的就是o
  2. 表达式let f = o.fn();将执行fn函数并将闭包函数的函数指针赋值给f变量,此时执行的函数是fn而并非是闭包函数,因而此刻fnthis指向对象o,然而闭包函数的this当初还没有确定;
  3. 通过调用表达式f();,让闭包函数执行,此刻闭包函数并不是某个对象调用执行,因而是运行在全局环境中,所以闭包函数的this将指向window(非严格模式)

因而,不论函数如何赋值,只有该函数并未执行,this指针就不会确定所指对象。第一个关键点就是了解函数是什么时候执行的!第二个关键点就是找到函数是如何被调用的!

隐式绑定

隐式绑定是指通过某个对象调用函数时,函数的this就指向该对象。简略说就是谁调用函数,函数就指谁。
咱们看看上面这个例子,该例子出自知乎文章:JavaScript 的 this原理是什么?(例5)

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())

一看感觉很简单,但实质上还是找到哪个对象调用函数进行执行,剖析一波:

  1. 执行表达式console.log(o1.fn())时,对象o1调用执行函数fn,因而,函数fnthis指向对象o1,所以输入o1
  2. 执行表达式console.log(o2.fn())时,对象o2调用执行函数fn,因而,o2外部函数fnthis指向对象o2,但且慢,这里有个表达式return o1.fn(),不难看出这又通过o1调用了o1外部函数fn(该函数this指向o1对象),并将执行后果返回。因而绕个弯还是回到执行o1外部函数fn,输入o1
  3. 执行表达式console.log(o3.fn())时,对象o3调用执行函数fn,因而,o3外部函数fnthis指向对象o3,但o3外部函数fn并没有间接用this,而是通过赋值操作获取了o1外部的fn函数,并执行fn函数。留神,这里有个坑,最初执行fn函数是没有对象调用的,因而fn函数的this指向window,这个跟例4相似,若不了解能够回头看例4。
肯定要分清默认绑定和隐式绑定的场景,关键点还是在于判断出函数执行的工夫,而后找出哪个对象调用了该函数。

联合回调函数再看一个例子(例6):

var hi = 'window'let o = {    hi: '对象',    fn: function () {        let hi = '函数';        setInterval(function(){            console.log(this.hi);        },1000);    }}o.fn();

你感觉应该输入什么呢?咱们先剖析一波:

  1. 很显著,表达式o.fn();执行过程中,函数fnthis铁定是指向对象o
  2. 再看fn函数外面,执行了setInterval函数,特地是还传入了匿名函数作为回调函数,匿名函数在每一秒执行过程中并没有任何对象调用它,因而匿名函数的this指向window,最终输入window

联合传参再看一个例子(例7):

var hi = 'window'let o = {    hi: '对象',    fn: function () {        let hi = '函数';        console.log(this);    }}function gn(fn){    fn();}gn(o.fn)

你感觉这回输入什么呢?一直的剖析:

  1. 首先明确一点:参数的传入等价于赋值。因而gn(o.fn)等价于f = o.fn; gn(f);好家伙,又是赋值,没有执行函数的都是骗子!
  2. 函数gn外部执行传入的函数fn,并没有产生对象调用,因而此刻执行的环境就是全局环境,输入window
赋值、回调、闭包都是this的头等大敌,肯定等确定函数真的执行了,再去找关联的对象。

显示绑定

显示绑定是指通过call、apply、bind对函数的this进行重定向,间接指定函数this所指的对象。

var hi = 'window'let o = {    hi: '对象',}function fn() {        let hi = '函数';        console.log(this.hi);    }fn.call(o);

通过fn函数的call办法,能够将全局环境中执行的fn函数外部this强行指向对象o,因而输入:对象
让咱们思考一下,对于办法call、apply、bind之间有什么不同呢?

红宝书第四版解释如下:

  1. callapply作用是一样的,只是传入参数的模式不同,call向函数传入参数须要一个个列出来,而apply须要应用参数数组进行传入参数
  2. bind办法会创立一个新的函数实例,其this值会绑定到传给bind的对象。

值得注意的是,callapply办法在调用后会间接执行函数,bind办法则不会,然而bind办法将会始终绑定固定的this给新创建的实例。bind的具体用法如下:

var hi = 'window'let o = {    hi: '对象',}function fn() {        let hi = '函数';        console.log(this.hi);    }let f = fn.bind(o);f();//无论f如何调用,f外部的this始终指向对象o,输入:对象fn();//this 仍旧依照失常绑定规定进行绑定,输入:window

new绑定

new关键字会呈现在应用构造函数创立特定类型对象中,且看一下红宝书对于new关键字的操作解释:

红宝书第四版将new操作步骤解释为:

  1. 在内存中创立一个新对象
  2. 这个新对象外部[[Prototype]]个性被赋值为构造函数的prototype属性
  3. 构造函数外部的this被赋值为这个新对象(即this指向新对象)
  4. 执行构造函数外部的代码(给新对象增加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创立的新对象

上述操作流程曾经很分明了,看看上面的例子(例8):

    function fn() {            this.name = "Tony";            this.showName = function (){                console.log(this.name);            }        }    let newObj = new fn();    newObj.showName();

联合红宝书的解释,咱们能够晓得在应用new关键字时有如下步骤:

  1. 生成一个新匿名对象
  2. 该匿名对象的[[Prototype]]个性被赋值为构造函数的prototype属性(这块波及原型链常识)
  3. 构造函数fn的外部this指向该匿名函数
  4. 执行fn外部代码,给匿名函数增加属性name和办法showName
  5. 返回匿名函数,并赋给newObj

(二)箭头函数的this对象

相比于一般函数外部有一个this对象,箭头函数外部是没有this对象。你没有听错,箭头函数外部是没有this对象!
那该如何确定箭头函数的this援用呢?回顾JavaScript对于作用域链机制,当一个函数作用域中没有某个变量时,则将会在作用域链中的逐级往后寻找,直到找到某个变量或因找不到而报错。因而,箭头函数外部没有this对象,则在应用this对象时,必须要找到外层函数的this对象或者windowthis对象,而箭头函数对应的外层this关系是在箭头函数定义时确定的,因而无论箭头函数是在哪里调用,箭头函数所能找到的this曾经在定义时就确定了。

咱们通过例子来找箭头函数的this(例9):

var hi = 'window'let o = {    hi:'对象',    gn:()=>{        let hi = '箭头函数';        console.log(this.hi);    },    fn:function () {        let hi = '函数'        return ()=>{            let hi = '箭头函数';            console.log(this.hi);        };    }}o.gn();let f = o.fn();f();f = o.fn.call(window);f();

先寻找箭头函数的this

  1. 函数gn是箭头函数,而且外层没有其余的函数包裹,因而依据变量解析的作用域链规定,箭头函数的的this就是windowthis
  2. 函数fn是一个返回箭头函数的匿名函数,依据作用域链规定,在查找箭头函数this过程中,找到外层函数fnthis当做箭头函数的this

最初咱们得出:

  1. gn箭头函数的this就是windowthis;
  2. fn返回的箭头函数的this就是函数fnthis
    运行上述例子,能够取得浏览器以下输入

简略剖析一下:

  1. 第一个输入由表达式o.gn()产生,因为gn箭头函数的this就是windowthis,因而hi变量就是window;
  2. 第二个输入由表达式let f = o.fn(); f();产生,因为fn返回的箭头函数的this就是函数fnthis,通过表达式o.fn()将函数fnthis指向对象o,导致箭头函数的this也是o,最终输入对象;
  3. 第三个输入由表达式f = o.fn.call(window); f();产生,因为fn返回的箭头函数的this就是函数fnthis,通过表达式f = o.fn.call(window)将函数fnthis指向window,导致箭头函数的this也是window,最终输入window

最初请思考一个问题,能够通过call()、apply()、bind()这些办法间接扭转箭头函数的this指向吗?

三、总结

this对象是JavaScript的比较复杂的知识点,我看过一些文章探讨this对象援用问题分多类论述或者间接给出公式,混合作用域链、闭包、赋值、回调、传参等多个知识点导致了解起来过于简单。我认为,this对象设计其实很精妙,重点要把握好函数执行时确定this的实质,再通过钻研几个非凡场景下的例子,就能够较好的了解this对象的指向问题。最初你会发现一般函数和箭头函数实质上是一样,惟一的区别在于一般函数有本人的this,而箭头函数没有本人的this

因为作者程度无限,不正之处敬请斧正。谢谢

参考资料:

  1. JavaScript高级程序设计(第四版)
  2. 掘金文章:嗨,你真的懂this吗?
  3. 知乎文章:JavaScript 的 this原理是什么?