资讯专栏INFORMATION COLUMN

捕捉JavaScript中this的指向

hiyang / 1626人阅读

摘要:构造函数调用构造函数调用将一个全新的对象作为变量的值,并隐式返回这个新对象作为调用结果。调用方式引起的改变函数的调用方式最常见的是方法调用构造函数调用,或者使用调用,也可以是立即执行函数。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89376.html

相关文章

  • JavaScripr常遇到错误和this关键字

    摘要:通过使用提供的异常处理语句,可以用结构化的方式来捕捉发生的错误,让异常处理带啊与核心业务代码实现分离。错误与异常处理在应用中的重要性是毋庸置疑的。包括内置对象函数在内的所有函数都可以用来调用,这种函数调用被称为构造函数调用。 错误与异常 概念 错误与异常是什么错误,指程序中的费正常运行状态,在其他编程语言中称为‘异常’或‘错误’。解释器会为每一个错误创建并抛出一个Error对象,其中包...

    klinson 评论0 收藏0
  • javascript-错误与异常、 this关键字

    摘要:错误与异常错误,指程序中的非正常运行状态,在其他编程语言中称为异常或,错误。定义一个全局变量,并赋值对象的方法绑定在中,构造函数只是一些使用操作符时被调用的函数。包括内置对象函数在内的所有函数都可以用来调用,这种函数调用被称为构造函数调用。 错误与异常 错误,指程序中的非正常运行状态,在其他编程语言中称为‘异常’或,‘错误’。解释器为每个错误情形创建并抛出一个Error对象,其中包含错...

    zhaofeihao 评论0 收藏0
  • 带你入门 JavaScript ES6 (三)

    摘要:上一章我们学习了遍历和扩展字符语法。本章我们主要学习中的箭头函数箭头函数更准确来说叫箭头函数表达式。箭头函数余普通函数功能相同,但语法差别比较大。 带你入门 JavaScript ES6 (三) 本文同步带你入门 JavaScript ES6 (三),转载请注明出处。 上一章我们学习了 for of 遍历和扩展字符语法。本章我们主要学习 ES6 中的箭头函数 箭头函数 更准确来说叫 箭...

    刘福 评论0 收藏0
  • JavaScript】面向对象之错误与异常与this关键字

    摘要:一错误与异常概述错误,指程序中的非正常运行状态,在其它语言中称为异常或错误将每个错误中创建个对象,描述包含的错误信息通过使用提供异常的处理语句,可以用结构化方式捕捉发生错误,异常处理代码与核心代码实现分离语句语句是指中处理异常一种标准方式, JS(JavaScript)一.错误与异常1.概述错误,指程序中的非正常运行状态,在其它语言中称为异常或错误将每个错误中创建个Error对象,描述...

    ASCH 评论0 收藏0
  • 我了解到JavaScript异步编程

    摘要:接下来我们看下三类异步编程的实现。事件监听事件发布订阅事件监听是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。 一、 一道面试题 前段时间面试,考察比较多的是js异步编程方面的相关知识点,如今,正好轮到自己分享技术,所以想把js异步编程学习下,做个总结。下面这个demo 概括了大多数面试过程中遇到的问题: for(var i = 0; i < 3; i++...

    RichardXG 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<