捕捉JavaScript中this的指向

JavaScript的this机制很复杂,虽然从一开始从事前端工做就和它打交道,但一直未能弄清楚,道明白。在工做中遇到this相关问题,就知道var self = this,一旦去面试遇到各类this相关面试题目时脑子就一片空白,拿不定结果。本文综合了一些书籍和网上文章对this的分析和讲解,提供一些实例来分析各类场景下this是如何指向的。javascript

全局做用域

在浏览器宿主环境中,this指向window对象,而且在全局做用域下,使用var声明变量其实就至关于操做全局thishtml

this === window; // true

var foo = 'bar';
this.foo === window.foo; // true

在严格模式下,this会绑定到undefined前端

var a = 2;
function foo() {
  'use strict';
  
  console.log(this.a);
}

foo(); // TypeError: this is not undefined

若是在变量的声明过程没有使用let或者var,会隐式建立一个全局变量,但这个变量和普通全局变量的区别在于它是做为window的一个属性建立的。两者在使用delete操做符上有明显的区别:变量不能够删除,而对象的属性是能够删除的java

var a = 2;
b = 3;
a; // 2
b; // 3
delete a;
delete b;
a; // 2
b; // Uncaught ReferenceError: b is not defined

局部做用域

这里的做用域主要是指在对象函数中的this指向。面试

函数调用

做为函数调用时,函数中的this默认指向window浏览器

var a = 1;
function foo() {
  console.log(this.a);
}

foo(); // 1

若是在当即执行函数中使用了this,它一样指向windowapp

var a = 1;
(function() {
  var a = 2;
  console.log(this.a);
})(); // 1

方法调用

做为方法调用时,函数中的this老是指向方法所在的对象。函数

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

obj.foo();

构造函数调用

构造函数调用将一个全新的对象做为this变量的值,并隐式返回这个新对象做为调用结果。也就是说指向新生成的实例。this

function Foo(name) {
  this.name = name;
  this.getName = function() {
    console.log(this.name);
  }
}

var a = new Foo('a');
a.getName(); // "a"

使用call和apply方法

能够经过call()apply()方法显示改变函数的this指向。prototype

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

foo(); // 1
foo.call(obj); // 2
foo.apply(obj); // 2

使用bind方法

bind()方法建立一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供以前提供一个给定的参数序列,而后返回由指定的this值和初始化参数改造的原函数拷贝。

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

var bar = foo.bind(obj);
bar();

箭头函数中调用

ES6引入了箭函数的概念,在箭头函数中因为没有this绑定,因此它的默认指向是由外围最近一层非箭头函数决定的。

var a = 1;
function Foo(a) {
  this.a = a;
  this.getA = function() {
    var x = () => {
      this.a = 3; // 改变了外围函数Foo属性a的值
      console.log(this.a); // 3
    }
    x();
    console.log(this.a); // 3
  }
}

var foo = new Foo(1);
foo.getA();

问题的产生

上面列举了在正常状况下this的指向结果。可是在实际开发过程当中,对于不一样场景,不一样的声明方式、调用方式、赋值和传值方式都会影响到this的具体指向。

调用方式引发的改变

函数的调用方式最多见的是方法调用构造函数调用,或者使用apply/bind/call调用,也能够是当即执行函数。

var a = 10;
var obj = {
  a: 20,
  fn: function() {
    var a = 30;
    console.log(this.a);
  }
}

obj.fn(); // 20
obj.fn.call(); // 10
(obj.fn)(); // 20
(obj.fn, obj.fn)(); // 10
(obj.fn = obj.fn)(); // 10
new obj.fn(); // undefined

对于applycall第一个参数若是不传或者传递undefinednull则默认绑定到全局对象,因此obj.fn.call()的调用实际上把this指向了window对象。

对于(obj.fn)(),咋一看,是当即执行函数,那么它的this确定指向了window对象,其实否则,这里obj.fn只是一个obj对象方法的引用,并无改变this的指向。

对于(obj.fn, obj.fn)(),这种操做比较少见,工做中也不会去这样写。这里首先咱们须要了解逗号操做符会对每一个操做数求值,并返回最后一个操做数的值,其次是这里使用了逗号操做符,里面必然是一个表达式,这种状况下里面的函数this指向其实已经改变了,指向了全局。对于(obj.fn = obj.fn)()this一样指向全局。所以能够大胆猜想:若是(x)();中x是一个表达式,而且返回一个函数(引用),那么函数x中的this指向全局window。这里还更多的方式来达到一样目的,好比:(true && obj.fn)() 或者 (false || obj.fn)()。总的来讲,咱们经过这种方式建立了一个函数的“间接引用”,从而致使函数绑定规则的改变。

对于new obj.fn()的结果其实也没有什么好说的,函数使用new操做符调用后返回一个新的实例对象,因为该对象并无一个叫a的属性,因此返回undefined

函数做为参数(变量)传递时

不少时候,函数的定义在一个地方,而对象定义方法时只是引用了该函数。一样在调用对象方法时,先把它赋值给一个变量(别名),而后使用函数别名进行调用。使用时有可能致使this绑定的改变。

示例一

var a = 10;
function foo() {
  console.log(this.a);
}

var obj = {
  a: 20,
  foo: foo
}

var bar = obj.foo; // 函数别名
bar(); // 10

虽然barobj.foo的一个引用,可是实际上,它引用的是foo函数自己,所以应用了函数的默认绑定规则。

示例二

var a = 10;
function foo() {
  console.log(this.a);
}

function doFoo(cb) {
  cb(); // cb 实际上引用的仍是foo
}

var obj = {
  a: 20,
  foo: foo
}

doFoo(obj.foo); // 10
setTimeout(obj.foo, 100); // 10

这里咱们将obj.foo以参数的形式传递给函数doFoo和内置函数setTimeout。参数传递实际上就是一种赋值,和示例一的结果是同样的。所以,调用回调函数的函数会丢失this的指向

改变构造函数的默认返回对象

构造函数使用new操做符调用后会返回一个新的实例对象,可是在定义构造函数时,能够在函数中返回任何值来覆盖默认该返回的实例,这样一来极可能致使实例this的指向改变。

var a = 10;
function f() {
  this.a = 20;
  function c() {
    console.log(this.a);
  }
  return c();
}

new f(); // 10

这里咱们将构造函数foo的默认返回值改为返回一个函数c执行后的结果。当调用new f()后,内部函数c中的this实际上指向的是全局。可是若是咱们将return c()改为return new c()的话,那么new foo()执行的结果是返回一个构造函数c的实例,因为实例对象中并无属性a,所以结果为undefined

方法的接收者引发的问题

在方法的调用中由调用表达式自身来肯定this变量的绑定。绑定的this变量的对象被称为调用接收者

var buffer = {
  entries: [],
  add: function(s) {
    this.entries.push(s);
  },
  concat: function() {
    return this.entries.join('');
  }
}

var source = ['123', '-', '456'];
source.forEach(buffer.add); // Uncaught TypeError: Cannot read property 'push' of undefined

因为方法buffer.add()的接收者不是buffer自己,而是forEach方法。事实上,forEach方法的实现使用全局对象做为默认的接收者。因为全局没有entries属性,所以会抛出一个错误。

要解决上面的问题,一个是使用forEach方法提供的可选参数做为函数的接收者。

source.forEach(buffer.add, buffer);

其次是使用bind方法来指定接收者

source.forEach(buffer.add.bind(buffer));

对象的实例属性和原型属性

这里想要说明的是,在一个对象的实例中,this便可以访问实例对象的值,也能够获取原型上的值。

function Foo() {}
Foo.prototype.name = 'bar';
Foo.prototype.logName = function() {
  console.log(this.name);
}
Foo.prototype.setName = function(name) {
  this.name = name;
}
Foo.prototype.deleteName = function() {
  delete this.name;
}

var foo = new Foo();
foo.setName('foo');
foo.logName(); // "foo"

foo.deleteName();
foo.logName(); // "bar"

delete foo.name;
foo.logName(); // "bar"

当执行foo.setName('foo')后,给实例对象foo增长了一个属性name,同时覆盖了原型中的同名属性。当执行foo.deleteName()时,其实是将新增值删除了,还原了初始状态。执行delete foo.name时,试图删除的仍是新增的属性,可是如今已经不存在这个值了。若是须要删除原始值,能够经过delete foo.__proto__.name来实现。

总结

本文只是介绍了一部分有关this的问题,更多知识点能够参考《详解this》以及MDNthis

相关文章
相关标签/搜索