JavaScript中的this关键词

this是我们在JS经常使用到的关键字之一。经常在面试的时候,会被问到this代表的具体含义,今天也本着学习的目的,仔细整理this的知识点。

本次this知识点的整理也算是比较系统全面的总结。

引子:this指向它的调用者

我们可以看一个例子

  1. var obj = {
  2. a: '我是obj',
  3. foo:function(){
  4. console.log(this.a)
  5. }
  6. }
  7. var a = '我是window'
  8. var foo = obj.foo
  9. obj.foo() // 我是obj
  10. foo() // 我是window

this指向他的调用者,其调用者是函数运行时所在的环境。两个foo的运行环境是不同的,obj.foo的运行环境是obj,第二个是全局环境window,所以两者的结果不一样。
为什么产生这种差异,这就需要了解内存的存储方式

函数的存储

函数是一个复杂数据类型,在js中是以对象的形式存在,区别与基本数据类型,是一种引用数据类型,函数的声明创建需要在内存堆(heap)中开辟出一块地址,再将内存地址赋值给变量。
11.this关键词 - 图1

比如

  1. var obj = { foo: 5 }

11.this关键词 - 图2
这样理解:代码将一个对象赋值给变量objJS会现在内存里面生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj

也就是说obj是一个地址,后面读取obj的过程是:JS会先获取到obj中指向的地址,再从该地址读出原始的对象,返回它的foo属性。
原始的对象在堆中都是以字典结构保存数据,每一个属性名对应一个属性面熟对象。上述的foo实际上是按照如图的形式保存
11.this关键词 - 图3

函数的调用

当堆中foovalue指向一个函数的地址,如下:
11.this关键词 - 图4
我们可以看见在JS中,函数会单独的保存在内存中,然后采用引用地址的方式赋值。

为什么要反复的说函数的调用、执行,因为函数的调用和它所在的执行环境(执行上下文[Execution Context])有关。函数的调用影响了this的指向问题。

那么在JS中有多少种函数的调用:

直接调用, 包括函数内部的嵌套调用,递归调用

  1. function foo() {}
  2. foo()

作为对象的方法调用

  1. var obj = {
  2. value : 0,
  3. increment : function (inc) {
  4. this.value += typeof inc === 'number' ? inc :1;
  5. }
  6. }
  7. obj.increment();
  8. console.log(obj.value); //1
  9. obj.increment(2);
  10. console.log(obj.value); //2

使用 call 和 apply 动态调用

callapplyFunction的原型方法,它们能够将特定函数当做一个方法绑定到指定对象上,并进行调用

  1. function.call(thisobj, args...)
  2. function.apply(thisobj, [args])

function表示要调用的函数;
参数thisobj表示绑定对象,即this指代的对象;
参数args表示要传递给被调用函数的参数。
call方法可以接收多个参数列表,而apply只能接收一个数组或者伪类数组,数组元素将作为参数列表传递给被调用的函数。

new 命令间接调用

使用new命令可以实例化对象,这是它的主要功能,但是在创建对象的过程中会激活并运行函数。因此,使用new命令可以间接调用函数。

  1. function f(x,y) { //定义函数
  2. console.log("x =" + x + ", y =" + y);
  3. }
  4. new f(3,4);

使用new命令调用函数时,返回的是对象,而不是return的返回值。如果不需要返回值,或者return的返回值是对象,则可以选用new间接调用函数。

当我们了解了函数的执行方式再去分析this就会方便一些。因为函数的执行上下文决定了this的指向问题。

this

引入

JavaScript中允许在函数体内部,引用当前环境的其他变量。

  1. var f = function () {
  2. console.log(x);
  3. };

上面代码中,函数体里面使用了变量x。该变量就是由行运环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。这时,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境

定义

当前执行代码的环境对象

顺带说一下this在严格模式和非严格模式对于全局环境的定义稍微有不同。在非严格模式指向window,在严格模式指向undefined

全局环境

无论是否在严格模式下,在全局执行环境中(在任何函数体外部this都指向全局对象。在全局执行环境中使用this,表示Global对象,在浏览器中就是window

  1. console.log(this); //Window
  2. console.log(typeof this); //object
  3. console.log(this === window); //true

函数(运行内)环境

函数内部直接出现this

在函数执行环境中使用this时,如果函数没有明显的作为非window对象的属性,而只是定义了函数,不管这个函数是不是定义在另一个函数中,这个函数中的this仍然表示window

  1. // "use strict"
  2. function A(){
  3. //在A函数中定义一个B函数
  4. function B(){
  5. console.log(this); //Window
  6. console.log(typeof this); //object
  7. console.log(this === window); //true
  8. }
  9. //在A函数内部调用B函数
  10. B();
  11. }
  12. //调用A函数
  13. A();

当然上述的结果是有待商榷的,因为一般地,在非严格模式下是window,在严格模式下,this将保持他进入执行环境时的值,所以此时this将会是默认的undefined

在严格模式中,this在上述的环境中确实为undefined,有一些浏览器最初在支持严格模式时没有正确实现这个功能,于是它们错误地返回了window对象

那么,当我们想把this的值从一个环境传递到另一个环境,就可以使用callapply方法

  1. // 将一个对象作为call和apply的第一个参数,this会被绑定到这个对象。
  2. var obj = { a: 'Custom' };
  3. // 这个属性是在global对象定义的。
  4. var a = 'Global';
  5. function whatsThis(arg) {
  6. return this.a; // this的值取决于函数的调用方式,默认为undefined[严格]
  7. }
  8. whatsThis(); // 'Global'
  9. whatsThis.call(obj); // 'Custom' this->obj
  10. whatsThis.apply(obj); // 'Custom' this->obj

当一个函数在其主体中使用this关键字时,可以通过使用函数继承自Function.prototypecallapply方法将this值绑定到调用中的特定对象。

  1. function add(c, d) {
  2. return this.a + this.b + c + d;
  3. }
  4. var o = {a: 1, b: 3};
  5. // 第一个参数是作为‘this’使用的对象
  6. // 后续参数作为参数传递给函数调用
  7. add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
  8. // 第一个参数也是作为‘this’使用的对象
  9. // 第二个参数是一个数组,数组里的元素用作函数调用中的参数
  10. add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

使用callapply函数的时候要注意,如果传递给this的值不是一个对象,JavaScript会尝试使用内部ToObject操作将其转换为对象。因此,如果传递的值是一个原始值比如7foo,那么就会使用相关构造函数将它转换为对象,所以原始值7会被转换为对象,像new Number(7)这样,而字符串foo转化成 new String('foo')这样,例如:

  1. function bar() {
  2. console.log(Object.prototype.toString.call(this));
  3. }
  4. //原始值 7 被隐式转换为对象
  5. bar.call(7); // [object Number]

作为对象的方法

当函数作为对象里的方法被调用时,它们的this是调用该函数的对象。

  1. //定义一个对象obj,并为它添加属性name,添加方法objFun
  2. var obj = {
  3. name: 'jack',
  4. objFun: function(){
  5. console.log(this); // Object {name: "jack"}
  6. console.log(typeof this); //object
  7. console.log(this === window); //false
  8. console.log(this.name); //jack
  9. }
  10. };
  11. //调用obj对象的方法
  12. obj.objFun(); //this 绑定到当前对象,也就是obj对象

请注意,这样的行为,根本不受函数定义方式或位置的影响。在前面的例子中,我们在定义对象obj的同时,将函数内联定义为成员objFun 。但是,我们也可以先定义函数,然后再将其附属到obj.objFun。这样做会导致相同的行为:

  1. var obj ={ name: 'jack' }
  2. function objFun() {
  3. console.log(this);
  4. console.log(typeof this);
  5. console.log(this === window);
  6. console.log(this.name);
  7. }
  8. obj.objFun = objFun
  9. obj.objFun();
  10. // Object {name: "jack"}
  11. // object
  12. // false
  13. // jack

这表明 函数是从objobjFun成员调用 才是重点。

当我们简单的修改一下:

  1. //定义一个对象obj,并为它添加属性name,添加方法objFun
  2. var obj = {
  3. name: 'jack',
  4. objFun: function(){
  5. console.log(this); // window
  6. console.log(typeof this); //object
  7. console.log(this === window); //true
  8. console.log(this.name); //undefined
  9. }
  10. };
  11. var test = obj.objFun;
  12. test(); // 等价于window.test()

这时候,你会神奇的发现,上面例子中的this又等于window 了,这也是常见的this丢失问题,到底是什么原因导致的呢?下面我们来分析分析:

在绝大多数情况下,函数的调用方式决定了this的值,this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。每次你调用一个函数this总是重新求值(但这一过程发生在函数代码实际执行之前),函数内部的this值实际上是由函数被调用的父作用域提供,更重要的是,它依赖实际函数的语法。在这里我们可以明显的知道test()等价于window.test()windowtest提供了父作用域(执行环境)。

this的绑定只受最靠近的成员引用的影响。

原型链中的thisgettersetter中的this都是同样适用函数调用者这一个概念

bind方法

ES5引入了bind方法来设置函数的this值,而不用考虑函数如何被调用的。说白了,调用bind方法会创建一个与原函数具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到bind中第一个参数上面,无论后生成的函数是如何调用的(只生效一次)。

  1. function cat() {
  2. return this.catName
  3. }
  4. const cat1 = cat.bind({ catName: 'lucy' })
  5. console.log(cat1()) // lucy
  6. const cat2 = cat.bind({ catName: 'mimi' })
  7. console.log(cat2()) // mimi
  8. /* 这个里面bind只生效一次 */

箭头函数

ES6中,箭头函数出现的作用不仅仅是让函数的书写变得很简洁,可读性很好之外,最大的优点就是解决了this执行环境所造成的的一些问题。比如:匿名函数this指向问题(匿名函数的执行环境具有全局性),以及setTimeoutsetInterval中使用this所造成的问题。

在箭头函数中,this与封闭词法环境的this保持一致。在全局代码中,它将被设置为全局对象:

  1. var globalObject = this;
  2. var foo = (() => this);
  3. console.log(foo() === globalObject); // true

上述的描述比较专业,可以变化的稍微好理解一点
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层’继承’this

  1. const obj = {
  2. name: 'jack',
  3. a: function() {
  4. console.log(this.name) // jack
  5. window.setTimeout(() => {
  6. console.log(this.name) // jack
  7. })
  8. }
  9. }
  10. obj.a.call(obj) //第一个this是obj对象,第二个this还是obj对象

注:函数obj.a没有使用箭头函数,因为它的this还是obj,而setTimeout里的函数使用了箭头函数,所以它会和作用域链的上一层的this保持一致,也是obj;如果setTimeout里的函数没有使用箭头函数,那么它打出来的应该是window对象(非严格模式)。

在箭头函数中,如果将this传递给callbind、或者apply,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数thisArg应该设置为null

  1. const obj = {
  2. name: 'jack',
  3. a: () => {
  4. console.log(this.name)
  5. }
  6. }
  7. obj.a() //打出来的是window
  8. obj.a.call('123') //打出来的结果依然是window对象

为什么总是window对象,或者说为什么会被忽略呢?

因为箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this

我们可以做一个普通函数和箭头函数的比较:

  1. const obj = {
  2. name: 'jack',
  3. a: function() { console.log(this.name) },
  4. b: {
  5. name: 'lucy',
  6. c: function() { console.log(this.name) }
  7. }
  8. }
  9. obj.a() // 打印:jack,相当于obj.a.call(obj)
  10. obj.b.c() // 打印:lucy,相当于obj.b.call(obj.b)

  1. const obj = {
  2. name: 'jack',
  3. a: function() { console.log(this.name) },
  4. b: {
  5. name: 'lucy',
  6. c: () => { console.log(this.name) }
  7. }
  8. }
  9. obj.a() //没有使用箭头函数打出的是jack
  10. obj.b.c() //打出的是window对象(非严格模式)!

打出window对象的原因,是window对象就是它的上一层this,而你的多层嵌套只是对象嵌套,这时候没有作用域链的嵌套,实际上对箭头函数来说,还是只有自己一级的作用域,和上一层的window作用域

构造函数方式(new)

当一个函数用作构造函数时(适用new关键词),他的this被绑定到正在构造的新对象(实例对象)。

我们需要简单的了解new操作符在调用构造函数的时候都发生了什么

构造函数和普通函数比较重要的两个区别:

  1. 构造函数首字母一般大写,约定俗成,便于区分;
  2. 构造函数的调用使用new操作符,普通函数的调用有很多方式,但是都用不到new

使用new操作符创建对象是发生的事情:

  1. 创建一个Object对象实例。
  2. 将构造函数中的执行对象赋值给新生成的这个实例。
  3. 执行构造函数中的代码。
  4. 返回新生成的对象实例。

原本构造函数是window对象的方法,默认作用域是全局作用域,如果不用new操作符而直接调用,那么构造函数的执行对象就是window,即this指向了window。现在用new操作符后,this就指向了新生成的对象。执行构造函数中的代码,如下:

  1. function Dog() {
  2. this.name = 'Tony'
  3. }
  4. function Cat() {
  5. this.name = 'Mimi'
  6. /* 如果函数具有返回对象的return语句,则该对象是new表达式的结果。
  7. 否则表达式的结果就是当前绑定的this的对象,我们从打印上面看this.name = 'Mimi'好像没执行,其实是执行了,只是对外部没有产生任何影响,如果我们将其移动到return的下面,还是能够看到改变的,只是忽略了它的含义 */
  8. return { word: 'I am mimi.' }
  9. }
  10. var dog = new Dog()
  11. console.log(dog.name) // Tony
  12. var cat = new Cat()
  13. console.log(cat.name) // I am mimi

虽然构造函数返回的默认值是this所指的那个对象,但它仍可以手动返回其他的对象(如果返回值不是一个对象,则返回this对象)。

总结

还有一些比较少见的用途,这里就不枚举了。总的来说,this这个关键词清楚了调用它的函数的执行环境再去理解this本身就会轻松许多!
现在面试的时候让你谈一谈this就不会1分钟解决战斗了吧!

【参考资料】

  1. this
  2. JavaScript 的 this 原理
  3. 你真的懂javascript中的 “this” 吗?
  4. JAVASCRIPT中THIS指的是什么?
  5. 箭头函数中的this
  6. JS函数调用(4种方法)