一次搞懂JavaScript中的this

前言

期末考试以前电话面了一次腾讯的暑期实习生,问题都比较简单,可是稍微一深刻本身的回答就不清楚了,其中有一个问题是ES6的箭头函数中this的相关知识点,想到本身连普通函数的this都没理解好就很丢人,惋惜这么好的机会没有把握住,此次索性从头深刻的学习一下,这篇文章就做为本身的学习笔记。javascript

文章大部份内容是摘抄,根据本身的学习经历和理解过程从基本的this概念入手,逐步涉及this原理和后续的扩展。html

什么是this

一句话解释,this表示函数执行时所在的运行环境(执行上下文对象),换句话说就是,谁调用的函数,this就表示是谁。若是不理解,看下面这个例子。java

var obj = {
    bar: 1,
    foo: function() {
        console.log(this.bar);
    }
};

var bar = 2;
var foo = obj.foo;

obj.foo();  // 1
foo();  // 2
复制代码

对于obj.foo()来讲,obj.foo()foo()的执行上下文对象是obj,因此this表示obj;而对于foo()来讲,foo()的执行上下文对象是全局环境,this表示全局环境。git

this的目的就是在函数体内部,指代函数当前的运行环境(context),若是还不理解,就是不明白什么是运行环境,那接着看下面的解释吧。es6

内存的数据结构

JavaScript 语言之因此有this的设计,跟内存里面的数据结构有关系。github

var obj = {
    foo: 5
};
复制代码

JavaScript引擎会先在内存里面,生成一个对象{foo: 5},而后把这个对象的内存地址赋值给obj。也就是说,变量obj是一个地址,若是要读取obj.foo,引擎先从obj拿到内存地址,而后再从该地址读出原始对象,返回它的foo属性。数组

原始对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来讲,上面例子的foo属性,其实是如下面形式保存的。浏览器

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}
复制代码

foo属性的值保存在属性描述对象的value属性里面。数据结构

函数

这样的结构是清晰的,问题在于属性的值多是一个函数。app

var obj = {
    foo: function() {}
};
复制代码

这时,引擎会将函数单独保存到内存中,而后再将函数的地址赋给foo属性的value属性。

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}
复制代码

因为函数是一个单独的值,因此它能够在不一样的环境(执行上下文)中执行。

上面这句话能够说是this这个知识点的核心了,以前一直不懂,就是由于这里了解的少,或者理解的很差,下面再详细一点说。

这里的执行上下文在更深层次上有一个执行上下文栈的概念,归纳来讲就是,浏览器永远执行在当前栈中最顶部的那个执行上下文,此时函数就是运行在这个执行上下文中。若是在一个对象或者函数内部又调用一个函数,都会建立一个新的执行上下文,并将这个新的执行上下文压入执行栈中,调用的函数就会在这个新的执行上下文中执行。这一段很差理解,咱们举个例子:

在阅读下面那段话以前,先区分一下这些概念的读法:

  • 执行上下文(Execution Context)= 执行上下文对象(能够有多个)
  • 全局执行上下文(Global Execution Context)= 全局环境(只有一个)
  • 当前执行上下文(Current Execution Context)(执行栈中最顶部的那一个)
  • 执行上下文栈(Execution Context Stack) = 执行栈(用来排列存储执行上下文)

例如,在全局中执行foo(),也就是在全局执行上下文中执行时,执行栈内只有一个全局执行上下文;当执行obj.foo()时,obj这个执行上下文将被压入执行栈中,foo()会在这个新的当前执行上下文中被执行,因此就有了函数能够在不一样的环境(执行上下文)中执行。

明白执行环境(执行上下文)的概念后,this的做用就容易理解了,this就是用来指代函数当前运行环境的!

环境变量

若是函数的当前运行环境内还定义了其它变量(环境变量),咱们就可使用this来调用了。

例如:

var f = function() {
    console.log(this.x);
};

var x = 1;
var obj = {
    f: f,
    x: 2
};

// 全局环境下执行
f();  // 1

// obj 环境下执行
obj.f();  // 2
复制代码

上面代码中,函数f在全局环境执行,this.x指向全局环境的x

obj环境执行,this.x指向obj.x

深刻理解原理后,this的概念和使用就变得清晰了。

其它示例

嵌套对象

为了加深理解,这里有一个嵌套对象的示例。

var obj = {
    bar: 1,
    obj1: {
        bar: 2,
        foo: function() {
            console.log(this.bar);
        }
    }
};

obj.obj1.foo();  // 2
复制代码

上面这段代码中,foo所在的运行环境(执行上下文对象)为obj1,此时执行栈中从下到上依次是全局执行上下文、obj执行上下文和obj1执行上下文,因此this表示obj1

函数内调用

var obj = {
    bar: 1,
    foo1: function() {
        var bar = 2;
        var foo2 = function() {
            return this.bar;
        }
        console.log(foo2());
    }
}

var bar = 3;

obj.foo1();  // 3
复制代码

上面这段代码中foo2函数在被定义后就被调用,比obj.foo1更早调用,此时执行栈中只有全局执行上下文(window),在严格模式(strict)下,执行上下文则是undefined,这也是 JavaScript 的一个大坑。必定要注意单独调用函数时,其内部this的指向!

绑定this

有时咱们想在函数中使用的环境变量并不必定是函数所在运行环境中的变量,而是某一个特定运行环境中的变量,在这种状况下,咱们就须要将函数中的this绑定咱们须要的运行环境(上下文对象)上,ECMAScript 规范给全部函数都定义了 callapply 两个方法,能够用来绑定。

callapply用法基本一致,主要区别是传参的形式不一样。

apply()

apply 方法传入两个参数:一个是做为函数上下文的对象,另一个是做为函数参数所组成的数组。

var obj = {
    bar: 1;
};

function foo(fistParam, secondParm) {
    console.log(firstParam + ' ' + this.bar + ' ' + secondParam);
};

foo.apply(obj, ['A', 'B']);  // A 1 B
复制代码

能够看到,obj 是做为函数上下文的对象,函数 foothis 指向了 obj 这个对象。参数 A 和 B 是放在数组中传入 foo 函数,分别对应 foo 参数的列表元素。

call()

call 方法第一个参数也是做为函数上下文的对象,可是后面传入的是一个参数列表,而不是单个数组。

var obj = {
    bar: 1
}

function foo(fistParam, secondParm) {
    console.log(firstParam + ' ' + this.bar + ' ' + secondParam);
};

func.call(obj, 'C', 'D');  // C 1 D
复制代码

对比 apply 咱们能够看到区别,C 和 D 是做为单独的参数传给 foo 函数,而不是放到数组中。

上面两种方法均可以经过第一个参数,把要绑定的上下文对象传递给函数。

bind()

在 ECMAScript5 中扩展了叫 bind 的方法,在低版本的 IE 中不兼容。它和 call 很类似,接受的参数有两部分,第一个参数是是做为函数上下文的对象,第二部分参数是个列表,能够接受多个参数。 它们之间的区别有如下两点。

bind 返回值是函数

var obj = {
    bar: 1
}

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

var func = foo.bind(obj);
func();  // 1
复制代码

bind 方法不会当即执行,而是返回一个改变了上下文 this 后的函数。而原函数 foo 中的 this 并无被改变,依旧指向全局对象 window

参数的使用

function func1(a, b, c) {
    console.log(a, b, c);
}
var func2 = func1.bind(null, 1);

func1('A', 'B', 'C');  // A B C
func2('A', 'B', 'C');  // 1 A B
func2('B', 'C');       // 1 B C
func1.call(null, 1);   // 1 undefined undefined
复制代码

call 是把第二个及之后的参数做为 func1 方法的实参传进去,而 func2 方法的实参实则是在 bind 中参数的基础上再日后排。

ES6中箭头函数的this

var obj = {
    bar: 1,
    foo1: function() {
        var bar = 2;
        var foo2 = function() {
            return this.bar;
        }
        console.log(foo2());
    }
}

var bar = 3;

obj.foo1();  // 3
复制代码

仍是看这个例子,当在函数foo1中单独调用内部的函数foo2时,foo2中的this可能指向window或者undefined;除此以外,咱们通常是经过对象来调用,无论如何调用,this所表明的对象老是视状况而定,这会给咱们带来必定的麻烦,如今,箭头函数帮咱们解决了这个问题。

在《ECMAScript 6 入门》一书中,阮一峰老师这样描述:

箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

这句话是存在歧义的,由于在JavaScript中函数和对象之间的界限并不清晰,看下面这个例子,foo2函数是定义在foo1中的,foo2中的this是否能够用来表示foo1呢? 这种思惟得出的结果是2(错误)。

阮老师在 ruanyf/es6tutorial issue中解释到,foo2位于foo1内部,只有当foo1函数运行后,foo2才会按照定义生成。这种解释对应书中的概念是没有问题的,但用第一种理解方式会给咱们带来必定的困扰。

那该如何理解呢?

在 issue 中有另一个解释:

全部的箭头函数都没有本身的this,都指向外层

或者将书中的描述改成

箭头函数中的this老是指向所在函数运行时的this

这两种描述就容易理解了。

const obj = {
    bar: 1,
    foo1: function() {
        const bar = 2;
        const foo2 = () => {
            return this.bar;
        }
        console.log(foo2());
    }
}

const bar = 3;

obj.foo1();  // 1
复制代码

由于箭头函数内部的this没有指向,因此当执行foo2()时,要去外层foo1寻找this,那么foo1this指向哪里呢?

要想拿到foo1的执行上下文,就需先执行foo1(),若是是obj.foo()this指向的是obj,最终的结果就是1(正确)。

笔记参考

文章内部可能在理解上和描述上存在错误,若是大佬们发现了请多多帮忙指正呀!😊

相关文章
相关标签/搜索