乐趣区

关于javascript:this到底指向谁

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 指向 window
function fn1(){console.log(this);
}
// 严格模式下,this 为 undefined
function 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 原理是什么?
退出移动版