What's this:JS关键字this的两种指向和六种调用

166 阅读6分钟

1. 概述

this是JS中的一个关键字,其指向一直是一个难点问题。时而指向全局,时而指向某对象,容易让人摸不着头脑。本文将对this主要使用场景进行概括,分析其指向的规律。其实this九成使用都在函数之中,少数在全局,当然还有this在DOM中的使用未提及。在非严格模式下,this指向的都是一个对象,这个对象或者指向Window(浏览器环境)或者指向某具体对象,后面就尝试把这个二选一的问题讲清楚。具体分六种情况,全局一种和函数中五种this使用情况。

2. 全局中的this

有些早期的博文和书籍中认为this只能在函数中使用,但实际上我们呢在全局环境下也可以直接写thisthis的值指向全局对象,不过这种写法基本不被使用,比较鸡肋。具体表现在浏览器和Node环境下略有不同。

浏览器下:window对象

console.log(this)

image.png

Node: global对象

image.png

image.png 本文其他例子就不描述node环境下的行为了,默认浏览器环境。

3. 函数中的this

函数中使用this是重中之重,本文写作的契机也是在看了《javaScript语言精粹》中函数部分的内容后想进行总结记录。该书中认为,每个函数都有两个隐藏的参数,分别是thisarguments。这两个参数都可以在函数中直接使用,无论该函数是否有形参。而不同的函数使用方法,this这个关键字的指向也各不相同。书中列举了四种方法,我这儿再加上ES6中的箭头函数进行总结。

3.1 函数调用this

所谓函数调用,就是调用时这个函数前面啥也没有,没有点号,没有对象,直接调用。最简单的例子:

function fn(){
    console.log(this)
}
fn() // window

函数调用this指向window。

这条就是唯一准则,哪怕像下面的例子一样好像有个对象在前面:

var name = 'window';
let obj = {
  name: 'obj',
  sayName: function () {
    let inner = function () {
      console.log(this.name)
    }
    inner(); // 在这儿执行,前面没对象
  }
}
obj.sayName(); // window

3.2 方法调用this

当使用this的函数作为某个对象的成员属性被调用时,称为方法调用,this指向该对象。简单而言就是:

谁调用我,我指向谁。

这一点是js中this使用的关键,后文所讲的许多问题都将体现这一点。

具体而言又有两点细节:

  1. this指向最近一个调用函数的那个对象。
var o = {prop: 37};

function independent() {
  return this.prop;
}

o.f = independent;

console.log(o.f()); // 37,此处this指向o

o.b = {g: independent, prop: 42};
console.log(o.b.g()); // 42,此处的this指向o.b
  1. 方法调用不论实质只论字面,通过原型链调用也指向字面上那个对象。
var o = {
  f: function() {
    return this.a + this.b;
  }
};
var p = {
    a: 3,
    b: 2,
    __proto__: o
}

console.log(p.f()); // 5

此外,构造函数调用thiscall/apply显示绑定this都可以用方法调用来解构,可以说其实质就是方法调用。

3.3 构造函数调用this

构造函数中几乎必定使用到this,其中的this指向新产生的那个对象。

function Box(color) {
  this.color = color;
}
let red = new Box("red");
console.log(red);

其实质可等价于以下代码:

function Box(color) {
  this.color = color;
}
// 任何一个函数定义时都会同时产生一个原型对象,此处为Box.prototype.
// 这个对象有个属性为constructor,指向Box。
// Box.prototype.constructor === Box

function newObj(fn, color) {
  let _res = {
    __proto__: fn.prototype, // fn.prototype有个叫constructor的属性
  };
  _res.constructor(color); // 这里还是一个方法调用,通过原型链调用
  return _res;
}

let red = newObj(Box, "red");
console.log(red);

以上也可作为面试时手写“new”的答案。

3.4 call/apply调用this

当某函数作为回调函数使用时,函数作为参数自然是无法绑定任何对象,所以在其执行时this会指向全局对象,这通常并非我们需要的结果。示例如下:

var obj1 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }, 0)
  }
}
var a = 3

obj1.foo1() // 2
obj1.foo2() // window, 3

显式绑定可用以解决回调函数中的this丢失问题,具体示例如下:

var obj1 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout((function () {
      console.log(this)
      console.log(this.a)
    }).bind(this), 0)
  }
}
var a = 3

obj1.foo1() // 2
obj1.foo2() // obj1, 2

react中常常需要绑定this,也是这个原因。

class Foo extends Component {
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <button onClick={this.handleClick.bind(this)}>Click Me</button>;
  }
}

此外,forEach、map、filter函数的第二个参数也能显式绑定this。

function foo (item) {
  console.log(item, this.a)
}
var obj = {
  a: 'obj'
}
var arr = [1, 2, 3]

arr.forEach(foo, obj) // 显式绑定

使用call/Apply的显示绑定this,也可以使用方法调用来进行简单的模拟。下面就简单模拟一个Apply。

let obj = {
  a: 1,
};
function add(b) {
  console.log(this.a + b);
}

add.apply(obj, [2]);

// 用myApply简单模拟一个Apply
function myApply(fn, obj, params) {
  obj._fn = fn;
  obj._fn(...params);
  delete obj._fn;
}
myApply(add, obj, [3]);

3.5 箭头函数中的this

很多人说箭头函数没有this,其实不是没有,是没有自己的this,它的this来自于它的环境。

箭头函数this指向跟调用他的方法没有关系,只取决于其被定义时的环境。

所以不管箭头函数是方法调用,还是直接调用,他都跟前面那个对象没有必然关系,一切取决于这个函数被定义时的环境。如下例中,虽是方法调用,其中this仍指向于window。

var obj = {
  name: 'obj',
  foo1: () => {
    console.log(this.name)
  }
}
var name = 'window'
obj.foo1() // window

这里我们说的“被定义”时的环境,是指得到这个箭头函数时的环境,如果取得箭头函数时该函数所处环境中this并不指向某对象(仅仅是引用了某个函数),则还是指向于window。具体案例如下:

var name = "window";
var obj = {
  name: "obj",
  foo2: function () {
    return () => {
      console.log(this.name);
    };
  },
};
let fn = obj.foo2;
fn()(); // window
obj.foo2()(); // obj

MDN中有个例子可以很好地说明箭头函数中this的指向。

var obj = {
  bar: function() {
    var x = (() => this);
    return x;
  }
};

var fn = obj.bar();

console.log(fn() === obj); // true

// 但是注意,如果你只是引用obj的方法,而没有调用,便不会使this转移到obj上
var fn2 = obj.bar;
console.log(fn2()() == window); // true

用ES5的语法解构上述代码,箭头函数等价于以下代码:

var obj = {
  bar: function () {
    var x = function () {
      return this;
    };
    return x.bind(this);
    // 这个this就是bar中的this,指向obj
  },
};

4. this经典面试题及解析

Q1:多层函数,内层函数不绑定外层函数的this

var o = {
  f1: function () {
    console.log(this);
    var f2 = function () {
      console.log(this);
    }();
  }
}

o.f1()

A1:f2并不作为某一个对象的方法调用,又无call/apply等显式的对象绑定,所以其中的this指向全局对象window

o //对象o
Window //全局对象

Q2:

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

A2:obj.foo这个函数是通过obj这个对象的某个成员得到,但没有调用,此处仅是得到了foo这个函数的地址,与直接使用foo函数无异。所以不存在方法调用,其中this仍指向全局对象window。

Window
2

A3:箭头函数中的this指向其外部环境的this

var obj = {
  name: 'obj',
  foo1: () => {
    console.log(this.name)
  },
  foo2: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
var name = 'window'
obj.foo1()
obj.foo2()()

Q3:这里第一个结果中的this虽然时来自于obj的方法调用,但调用的是箭头函数,所以这个this的指向仍是obj本身所处的环境,即全局对象window。第二个结果常规方法调用,为obj。第三个结果是箭头函数绑定的foo2中的this,指向obj。

window
obj
obj

5.总结

总结一下this使用中的关键点:

  • 方法调用,指向调用的对象。
  • 箭头函数,指向函数外边的环境。
  • 若是函数作为参数,或是重命名赋值,则会失去对于对象的绑定。