谈谈JavaScript中的this

148 阅读7分钟

前言

在JavaScript里面,一直有一个很神秘的关键字叫做this。对于this,在很多教程里面其实都没有讲得很细致,所以就导致了很多同行对于this感觉道不清说不明,充满着神秘。今天,我们就来探索一下this,试图揭开它神秘的面纱,探个究竟。

什么是this

this是当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。this是JavaScript里的关键字。

为什么要用this

this的作用其实只是为了提供一些遍历的属性访问方式而已。对于为什么要用this,我们这里使用反证法来说明。假如我们遇到以下场景:

Function1 需要调用function2 ,function2需要调用function3,在不用this的情况下,我们可能需要这么写:

function whoIam(context){
	console.log("i am"+context.name);
	sayHi(context)
}

function sayHi(context){
	console.log(context.name+"say hi");
	sayHello(context);
}

function sayHello(context){
	console.log(context.name+"say hello");
}

var test1 = {
  name:"test1"
}
whoIam(test1)

假如层级很多,模式很复杂,我们需要将这个对象一直传递下去,到时候代码混乱,跟踪就变得困难。假如我们使用this,我们则只需要:

function whoIam(){
	console.log("i am"+this.name);
	sayHi.call(this)
}

function sayHi(){
	console.log(this.name+"say hi");
	sayHello.call(this);
}

function sayHello(){
	console.log(this.name+"say hello");
}

var test1 = {
  name:"test1"
}
whoIam.call(test1)

this能够帮助我们解决隐式传递对象引用的问题,使得api 更加简洁且复用。

关于this的误区

对于this值的认识,一直以来有两个误区:

  1. this等于函数本身

    这个误解是自然而言,符合大多数人的第一直觉。甚至写过其他语言程序的都会习惯性认为this就等于函数本身,因为其他语言就是这样的。但是考虑以下代码:

    function foo(num) {
        console.log("foo: " + num); // 记录 foo 被调用的次数
        this.count++;
    }
    foo.count = 0;
    var i;
    for (i = 0; i < 10; i++) {
        if (i > 5) {
            foo(i);
        }
    }
    // foo: 6
    // foo: 7 
    // foo: 8 
    // foo: 9
    // foo 被调用了多少次? 
    console.log(foo.count);
    

    在这个例子里,foo一种被调用了4次,但是最后再打印foo.count的时候,他还是等于0。所以this并不等于函数本身。

  2. this等于作用域

    思考以下代码:

    function foo() {
      var a = 2;
      this.bar(); 
    }
    function bar() {
      console.log( this.a );
    }
    foo(); // ReferenceError: a is not defined
    

    按道理,如果this等于作用域的话,那么foo里面的bar应该是访问不了的,但事实上这段代码成功执行了bar,只不过因为bar无法访问到a而报错而已。所以其实this并不等于作用域

this值的计算方式

根据mdn的描述,this值的计算取决于它的调用方式

1.在全局上下文调用

无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this都指向全局对象。

// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true

a = 37;
console.log(window.a); // 37

this.b = "MDN";
console.log(window.b)  // "MDN"
console.log(b)         // "MDN"
2.函数上下文调用

在函数内部,this的值取决于函数被调用的方式。并且不同模式下结果不同

在一般宽松模式下,this的值默认指向全局对象,浏览器中就是window。

function f1(){
  return this;
}
//在浏览器中:
f1() === window;   //在浏览器中,全局对象是window

//在Node中:
f1() === globalThis;   

在严格模式下,this会保持为undefined

function f2(){
  "use strict"; // 这里是严格模式
  return this;
}

f2() === undefined; // true
3.类上下文调用

this在 类 中的表现与在函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。

在类的构造函数中,this是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中:

class Example {
  constructor() {
    const proto = Object.getPrototypeOf(this);
    console.log(Object.getOwnPropertyNames(proto));
  }
  first(){}
  second(){}
  static third(){}
}

new Example(); // ['constructor', 'first', 'second']
4.派生类

不像基类的构造函数,派生类的构造函数没有初始的 this 绑定。在构造函数中调用 super()会生成一个 this 绑定,并相当于执行如下代码,Base为基类:

this = new Base();

需要注意的是,派生类的构造函数里在super()之前使用this将会报错

class Base {}

//没有构造函数,可以
class Good extends Base {}

//构造函数直接返回一个对象,可以
class AlsoGood extends Base {
  constructor() {
    return {a: 5};
  }
}
//构造函数没有调用super也不返回对象,不可以
class Bad extends Base {
  constructor() {}
}

//派生类在调用super之前,this是没有赋值的,调用super之后,this将被复制为父类对象,也就是this = new Base()
class Bad2 extends Base {
    constructor() {
        // console.log(this);// ReferenceError
        super();
        console.log(this);// 

    }
}

new Good();
new AlsoGood();
new Bad2(); // ReferenceError
5.箭头函数

无论如何,foo 的 this 被设置为他被创建时的环境。这同样适用于在其他函数内创建的箭头函数:这些箭头函数的this被设置为封闭的词法环境的。

// 创建一个含有bar方法的obj对象,
// bar返回一个函数,
// 这个函数返回this,
// 这个返回的函数是以箭头函数创建的,
// 所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置,这反过来又设置了返回函数的值。
var obj = {
  bar: function() {
    var x = (() => this);
    return x;
  }
};

// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// 将返回的函数的引用赋值给fn。
var fn = obj.bar();

// 直接调用fn而不设置this,
// 通常(即不使用箭头函数的情况)默认为全局对象
// 若在严格模式则为undefined
console.log(fn() === obj); // true

// 但是注意,如果你只是引用obj的方法,
// 而没有调用它
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。
console.log(fn2()() == window); // true
6.对象方式调用

当函数作为对象里的方法被调用时,this 被设置为调用该函数的对象。

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

console.log(o.f()); // 37

但需要注意的是

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

var f = o.f;
console.log(f());//undefined

由于f只是function的一个引用,所以这种方式仅仅相当于单独调用function

7.原型链中的this和getter、setter中的this

原型链中的this和getter、setter中的this 指向都是调用的对象本身

var o = {
  f: function() { 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5
8.函数作为构造函数使用

当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。

function C(){
  this.a = 37;
}

var o = new C();
console.log(o.a); // logs 37


function C2(){
  this.a = 37;
  return {a:38};
}

o = new C2();
console.log(o.a); // logs 38
9.作为DOM事件处理函数

当函数被用作事件处理函数时,它的 this 指向触发事件的元素(一些浏览器在使用非 addEventListener 的函数动态地添加监听函数时不遵守这个约定)。

this强制性绑定的三种方式

1.call
function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// 第一个参数是用作“this”的对象
// 其余参数用作函数的参数
add.call(o, 5, 7); // 16
2.call
function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// 第一个参数是用作“this”的对象
// 第二个参数是一个数组,数组中的两个成员用作函数参数
add.apply(o, [10, 20]); // 34
3.bind

ECMAScript 5 引入了 Function.prototype.bind()。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

function f(){
  return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty

var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty

var o = {a:37, f:f, g:g, h:h};
console.log(o.a, o.f(), o.g(), o.h()); // 37, 37, azerty, azerty

主要再次注意的是:无论何种方式,箭头函数里面的this都是等于箭头函数被创建时的值,apply/call/bind都无法改变。

总结

this的作用是能够让开发者更加方便地使用属性和传递对象,从而也使得编写代码时更加通用。this的值跟调用方式相关,所以后续在使用this值时一定要多加注意。

参考

MDN