资讯专栏INFORMATION COLUMN

理解 JavaScript this

zombieda / 436人阅读

摘要:回调函数在回调函数中的指向也会发生变化。在闭包回调函数赋值等场景下我们都可以利用来改变的指向,以达到我们的预期。文章参考系列文章理解闭包理解执行栈理解作用域理解数据类型与变量原文发布在我的公众号,点击查看。

这是本系列的第 5 篇文章。

还记得上一篇文章中的闭包吗?点击查看文章 理解 JavaScript 闭包 。

在聊 this 之前,先来复习一下闭包:

var name = "Neil";

var person = {
  name: "Leo",
  sayHi: function() {
    return function () {
      return "Hi! My name is " + this.name;
    }
  }
};

person.sayHi()(); // "Hi! My name is Neil"

上一篇文章说,我们可以把闭包简单地理解为函数返回函数。所以这里的闭包结构是:

// ...
function () {
  return "Hi! My name is " + this.name;
}
// ...

但是你有没有发现,这个函数执行的结果是 “Hi! My name is Neil” 。等等,我不是叫 Leo 吗?怎么给我改了个名字?!

我一分析,原来是 this 在其中作祟,且听我慢慢道来这“改名的由来”。

§ this 从何而来

首先,你得确保你已经清楚执行栈与执行上下文的知识。点击查看文章 理解 JavaScript 执行栈 。

ECMAScript 5.1 中定义 this 的值为执行上下文中的 ThisBinding。而 ThisBinding 简单来说就是由 JS 引擎创建并维护,在执行时被设置为某个对象的引用

在 JS 中有三种情况可以创建上下文:初始化全局环境、eval() 和执行函数。

§ 全局中的 this
var num = 1;

function getName () {
  return "Leo";
}

this.num; // 1
this.getName(); // Leo

this == window; // true

当我们在浏览器中运行这段代码,JS 引擎会将 this 设置为 window 对象。而声明的变量和函数被作为属性挂载到 window 对象上。当然,在严格模式下,全局中 this 的值设置为 undefined。

"use strcit";

var num = 1;
function getName () {
  return "Leo";
}

this.num; // TypeError
this.getName(); // TypeError

this == undefined; // true

开启严格模式后,全局 this 将指向 undefined,所以调用 this.num 会报错。

§ eval() 中的 this

eval() 不被推荐使用,我现在对其也不太熟悉,这里尝试着说一下。初学者可以直接跳到下一节。

结合所查阅的资料,目前我对 eval() 的理解如下:

eval(...) 直接调用,被理解为是一个 lvalue,也有说是 left unchanged,字面理解为余下不变。什么是“余下不变”?我理解为直接调用 eval(...),其中代码的执行环境不变,依旧为当前环境,this 也依旧指向当前环境中的调用对象。

而使用类似 (1, eval)(...) 的代码,被称为间接调用。(1, eval) 是一个表达式,你可以这样认为 (true && eval) 或者 (0 : 0 ? eval)。间接调用的 eval 始终认为其中的代码执行在全局环境,将 this 绑定到全局对象。

var x = "outer";
(function() {
  var x = "inner";
  // "direct call: inner"
  eval("console.log("direct call: " + x)");
  // "indirect call: outer"
  (1, eval)("console.log("indirect call: " + x)");
})();

关于 eval(),现在不敢确定,如有错误,欢迎指正。

§ 函数中的 this ◆ 一般情况

首先,我们需要明确的是,在 JS 中函数也属于对象,它可以拥有属性,this 就是函数在执行时获得的属性。一般情况下,在全局环境中直接调用函数,函数中的 this 会在调用时被 JS 引擎设置为全局对象 window(同样在严格模式下为 undefined)。

var name = "Leo";

function getName() {
  var name = "Neil";
  console.log(this); // [object Window]
  return this.name;
}

getName(); // Leo
◆ 作为对象的方法

函数可以作为对象的方法被该对象调用,那么这种情况 this 会被设置为该对象。

var name = "Neil";

var person = {
  name: "Leo",
  sayHi: function() {
    console.log(this); // person
    return "Hi! My name is " + this.name;
  }
};

person.sayHi(); // "Hi! My name is Leo"

当 person 对象调用 sayHi() 方法时,this 被指向 person。

◆ 特殊的内置函数

JS 还提供了一种供开发者自定义 this 的方式,它提供了 3 种方式。

Function.prototype.call(thisArg, argArray)

Function.prototype.apply(thisArg [, arg1 [, args2, ...]])

Function.prototype.bind(thisArg [, arg1 [, args2, ...]])

我们可以通过设置 thisArg 的值,来自定义函数中 this 的指向。

var leo = {
  name: "Leo",
  sayHi: function () {
    return "Hi! My name is " + this.name;
  }
}

var neil = {
  name: "Neil"
};

leo.sayHi(); // "Hi! My name is Leo"
​leo.sayHi.call(neil); // "Hi! My name is Neil"

这里,我们通过 call() 将 sayHi() 中 this 的指向绑定为 neil 对象,从而取代了默认 的 this 指向 leo 对象。

关于函数的 call(), apply(), bind() 我将在后面另写一篇文章,敬请期待。

§ this 引起的令人费解的现象 ◆ 闭包

通过前面的介绍,我想你对 this 已经有了初步的印象。那么,回到文章开头的问题,this 是怎么改变了我的名字?换句话说,this 在闭包的影响下指向发生了怎样的变动?

再看一下代码:

var name = "Neil";

var person = {
  name: "Leo",
  sayHi: function() {
    return function () {
      return "Hi! My name is " + this.name;
    }
  }
};

person.sayHi()(); // "Hi! My name is Neil"

通过上一篇文章 理解 JavaScript 闭包,函数返回函数会形成闭包。在这种情况下,闭包往往所执行的环境与所定义的环境不一致,而 this 的值却是在执行时决定的。所以,当上面代码中的闭包在执行时,它所在的执行上下文是全局环境,this 将被设置为 window(严格模式下为 undefined)。

怎么解决?我们可以利用 call / apply / bind 来修改 this 的指向。

var name = "Neil";

var person = {
  name: "Leo",
  sayHi: function() {
    return function () {
      return "Hi! My name is " + this.name;
    }
  }
};

person.sayHi().call(person); // "Hi! My name is Leo"

这里利用 call() 将 this 指向 person。OK,我的名字回来了,“Hi! My name is Leo” ^^

当然,我们还有第二种解决方法,闭包的问题就让闭包自己解决。

var name = "Neil";

var person = {
  name: "Leo",
  sayHi: function() {
    var that = this; // 定义一个局部变量 that
    return function () {
      return "Hi! My name is " + that.name; // 在闭包中使用 that
    }
  }
};

person.sayHi()(); // "Hi! My name is Leo"

在 sayHi() 方法中定义一个局部变量,闭包可以将这个局部变量保存在内存中,从而解决问题。

◆ 回调函数

在回调函数中 this 的指向也会发生变化。

var name = "Neil";

var person = {
  name: "Leo",
  sayHi: function() {
    return "Hi! My name is " + this.name;
  }
};

var btn = document.querySelector("#btn");

btn.addEventListener("click", person.sayHi);
// "Hi! My name is undefined"

这里 this 既不指向 person,也不指向 window。那它指向什么?

btn 对象,它是一个 DOM 对象,有一个 onclick 方法,在这里定义为 person.sayHi。

{
  // ...
  onclick: person.sayHi
  // ...
}

所以,当我们执行上面的代码,this.name 的值为 undefined,因为 btn 对象上没有定义 name 属性。我们给 btn 对象自定义一个 name 属性来验证一下。

var btn = document.querySelector("#btn");

btn.name = "Jackson";

btn.addEventListener("click", person.sayHi);
// "Hi! My name is Jackson"

原因说清楚了,解决方案同样可用过 call / apply / bind 来改变 this 的指向,使其绑定到 person 对象。

btn.addEventListener("click", person.sayHi.bind(person));
// "Hi! My name is Leo"
◆ 赋值
var name = "Neil";

var person = {
  name: "Leo",
  sayHi: function() {
    return "Hi! My name is " + this.name;
  }
};

person.sayHi(); // "Hi! My name is Leo"

var foo = person.sayHi;

foo(); // "Hi! My name is Neil"

当把 person.sayHi() 赋值给一个变量,这个时候 this 的指向又发生了变化。因为 foo 执行时是在全局环境中,所以 this 指向 window(严格模式下指向 undefined)。

同样,我们可以通过 call / apply / bind 来解决,这里就不贴代码了。

§ 别忘了 new

在 JS 中,我们声明一个类,然后 new 一个实例。

function Person(name) {
  this.name = name;
}

var her = Person("Angelia");
console.log(her.name); // TypeError

var me = new Person("Leo");
console.log(me.name); // "Leo"

如果我们直接把调用这个函数,this 将指向全局对象,Person 在这里就是一个普通函数,没有返回值,默认 undefined,而尝试访问 undefined 的属性就会报错。

如果我们使用 new 操作符,那么 new 其实会生成一个新的对象,并将 this 指向这个新的对象,然后将其返回,所以 me.name 能打印出 “Leo”。

关于 new 的原理,我会在后面的文章分享,敬请期待。

§ 小结

你看,this 是不是千变万化。但是我们得以不变应万变。

在这么多场景下,this 的指向万变不离其宗:它一定是在执行时决定的,指向调用函数的对象。在闭包、回调函数、赋值等场景下我们都可以利用 call / apply / bind 来改变 this 的指向,以达到我们的预期。

接下来,请期待文章《理解 JavaScript call/apply/bind》。

◆ 文章参考

Understand JavaScript"s "this" With Clarity, and Matser It | Richard Bovell

How does the "this" keyword work | Stack Overflow

ECMAScript Language Specification - $11.1.1 The This Keyword

(1, eval)("this") vs eval("this") in JavaScript | Stack Overflow

§ JavaScript 系列文章

理解 JavaScript 闭包

理解 JavaScript 执行栈

理解 JavaScript 作用域

理解 JavaScript 数据类型与变量

Be Good. Sleep Well. And Enjoy.

原文发布在我的公众号 camerae,点击查看。

前端技术 | 个人成长

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

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

相关文章

  • 我对JavaScriptthis的一些理解

    摘要:匿名函数的执行环境具有全局性,因此它的对象通常指向。如果对此有疑惑,可以看知乎上的答案知乎匿名函数的指向为什么是作为对象方法的调用,指向该对象当函数作为某个对象的方法调用时,就指这个函数所在的对象。 因为日常工作中经常使用到this,而且在JavaScript中this的指向问题也很容易让人混淆一部分知识。 这段时间翻阅了一些书籍也查阅了网上一些资料然后结合自己的经验,为了能让自...

    focusj 评论0 收藏0
  • javascript技术难点(三)之this、new、apply和call详解

    摘要:第四点也要着重讲下,记住构造函数被操作,要让正常作用最好不能在构造函数里 4) this、new、call和apply的相关问题 讲解this指针的原理是个很复杂的问题,如果我们从javascript里this的实现机制来说明this,很多朋友可能会越来越糊涂,因此本篇打算换一个思路从应用的角度来讲解this指针,从这个角度理解this指针更加有现实意义。 下面我们看看在ja...

    ghnor 评论0 收藏0
  • 加深对 JavaScript This理解

    摘要:使用来调用函数,会自动执行下面操作创建一个全新的对象。所以如果是一个函数的话,会是这样子的创建一个新对象连接新对象与函数的原型执行函数,改变指向新的对象所以,在使用来调用函数时候,我们会构造一个新对象并把它绑定到函数调用中的上。 欢迎来我的博客阅读:《加深对 JavaScript This 的理解》 我相信你已经看过很多关于 JavaScript 的 this 的谈论了,既然你点进来...

    PiscesYE 评论0 收藏0
  • 理解 JavaScript call()/apply()/bind()

    摘要:理解文章中已经比较全面的分析了在中的指向问题,用一句话来总结就是的指向一定是在执行时决定的,指向被调用函数的对象。与和直接执行原函数不同的是,返回的是一个新函数。这个新函数包裹了原函数,并且绑定了的指向为传入的。 理解 JavaScript this 文章中已经比较全面的分析了 this 在 JavaScript 中的指向问题,用一句话来总结就是:this 的指向一定是在执行时决定的,...

    duan199226 评论0 收藏0
  • javascriptthis理解

    摘要:的关键字总是让人捉摸不透,关键字代表函数运行时,自动生成的一个内部对象,只能在函数内部使用,因为函数的调用场景不同,的指向也不同。其实只要理解语言的特性就很好理解。个人对中的关键字的理解如上,如有不正,望指正,谢谢。 javascript的this关键字总是让人捉摸不透,this关键字代表函数运行时,自动生成的一个内部对象,只能在函数内部使用,因为函数的调用场景不同,this的指向也不...

    jimhs 评论0 收藏0

发表评论

0条评论

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