捕捉JavaScript中this的指向

发布时间:2019-08-20 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了捕捉JavaScript中this的指向脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

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

全局作用域

在浏览器宿主环境中,this指向window对象,并且在全局作用域下,使用var声明变量其实就相当于操作全局this

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操作符上有明显的区别:变量不可以删除,而对象的属性是可以删除的

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,它同样指向window

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变量的值,并隐式返回这个新对象作为调用结果。也就是说指向新生成的实例。

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指向。

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。这里还更多的方式来达到同样目的,比如:(@L_777_24@ && 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

脚本宝典总结

以上是脚本宝典为你收集整理的捕捉JavaScript中this的指向全部内容,希望文章能够帮你解决捕捉JavaScript中this的指向所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。