深入浅出 JavaScript 关键词 -- this

时间:2024-01-17 09:57:26

深入浅出 JavaScript 关键词 -- this

要说 JavaScript 这门语言最容易让人困惑的知识点,this 关键词肯定算一个。JavaScript 语言面世多年,一直在进化完善,现在在服务器上还可以通过 node.js 来跑 JavaScript。显然,这门语言还会活很久。

所以说,我一直相信,如果你是一个 JavaScript 开发者或者说 Web 开发者,学好 JavaScript 的运作原理以及语言特点肯定对你以后大有好处。

开始之前

在开始正文之前,我强烈推荐你先掌握好下面的知识:

  • 变量作用域和作用域提升

  • JavaScript 的函数

  • 闭包

如果没有对这些基础知识掌握踏实,直接讨论 JavaScript 的 this 关键词只会让你感到更加地困惑和挫败。

我为什么要学 this

如果上面的简单介绍没有说服你来深入探索 this 关键词,那我用这节来讲讲为什么要学。

考虑这样一个重要问题,假设开发者,比如 Douglas Crockford (译者注:JavaScript 领域必知牛人),不再使用 new 和 this,转而使用完完全全的函数式写法来做代码复用,会怎样?

事实上,基于 JavaScript 内置的现成的原型继承 功能,我们已经使用并且将继续广泛使用 new 和 this 关键词来实现代码复用。

理由一,如果只能使用自己写过的代码,你是没法工作的。现有的代码以及你读到这句话时别人正在写的代码都很有可能包含 this 关键词。那么学习怎么用好它是不是很有用呢?

因此,即使你不打算在你的代码库中使用它,深入掌握 this 的原理也能让你在接手别人的代码理解其逻辑时事半功倍。

理由二,拓展你的编码视野和技能。使用不同的设计模式会加深你对代码的理解,怎么去看、怎么去读、怎么去写、怎么去理解。我们写代码不仅是给机器去解析,还是写给我们自己看的。这不仅适用于 JavaScript,对其他编程语言亦是如此。

随着对编程理念的逐步深入理解,它会逐渐塑造你的编码风格,不管你用的是什么语言什么框架。

就像毕加索会为了获得灵感而涉足那些他并不是很赞同很感兴趣的领域,学习 this 会拓展你的知识,加深对代码的理解。

什么是 this ?

JavaScript this 指向

在我开始讲解前,如果你学过一门基于类的面向对象编程语言(比如 C#,Java,C++),那请将你对 this 这个关键词应该是做什么用的先入为主的概念扔到垃圾桶里。JavaScript 的 this 关键词是很不一样,因为 JavaScript 本来就不是一门基于类的面向对象编程语言。

虽说 ES6 里面 JavaScript 提供了类这个特性给我们用,但它只是一个语法糖 ,一个基于原型继承的语法糖。

this 就是一个指针,指向我们调用函数的对象。

我难以强调上一句话有多重要。请记住,在 Class 添加到 ES6 之前,JavaScript 中没有 Class 这种东西。Class只不过是一个将对象串在一起表现得像类继承一样的语法糖,以一种我们已经习惯的写法。所有的魔法背后都是用原型链编织起来的。

如果上面的话不好理解,那你可以这样想,this 的上下文跟英语句子的表达很相似。比如下面的例子

Bob.callPerson(John);

就可以用英语写成 “Bob called a person named John”。由于 callPerson() 是 Bob 发起的,那 this 就指向 Bob。我们将在下面的章节深入更多的细节。到了这篇文章结束时,你会对 this 关键词有更好的理解(和信心)。

执行上下文

执行上下文 是语言规范中的一个概念,用通俗的话讲,大致等同于函数的执行“环境”。具体的有:变量作用域(和 作用域链条,闭包里面来自外部作用域的变量),函数参数,以及 this 对象的值。

引自: *.com

记住,现在起,我们专注于查明 this 关键词到底指向哪。因此,我们现在要思考的就一个问题:

  • 是什么调用函数?是哪个对象调用了函数?

为了理解这个关键概念,我们来测一下下面的代码。

var person = {
   name: "Jay",
   greet: function() {
       console.log("hello, " + this.name);
   }
};
person.greet();

谁调用了 greet 函数?是 person 这个对象对吧?在 greet() 调用的左边是一个 person 对象,那么 this 关键词就指向 personthis.name 就等于 "Jay"。现在,还是用上面的例子,我加点料:

var greet = person.greet; // 将函数引用存起来;
greet(); // 调用函数

你觉得在这种情况下控制台会输出什么?“Jay”?undefined?还是别的?

正确答案是 undefined。如果你对这个结果感到惊讶,不必惭愧。你即将学习的东西将帮助你在 JavaScript 旅程中打开关键的大门。

this 的值并不是由函数定义放在哪个对象里面决定,而是函数执行时由谁来唤起决定。

对于这个意外的结果我们暂且压下,继续看下去。(感觉前后衔接得不够流畅)

带着这个困惑,我们接着测试下 this三种不同的定义方式。

找出 this 的指向

上一节我们已经对 this 做了测试。但是这块知识实在重要,我们需要再好好琢磨一下。在此之前,我想用下面的代码给你出个题:

var name = "Jay Global";
var person = {
   name: 'Jay Person',
   details: {
       name: 'Jay Details',
       print: function() {
           return this.name;
       }
   },
   print: function() {
       return this.name;
   }
};
console.log(person.details.print());  // ?
console.log(person.print());          // ?
var name1 = person.print;
var name2 = person.details;
console.log(name1()); // ?
console.log(name2.print()) // ?

console.log() 将会输出什么,把你的答案写下来。如果你还想不清楚,复习下上一节。

准备好了吗?放松心情,我们来看下面的答案。

答案和解析

person.details.print()

首先,谁调用了 print 函数?在 JavaScript 中我们都是从左读到右。于是 this 指向 details 而不是 person。这是一个很重要的区别,如果你对这个感到陌生,那赶紧把它记下。

print 作为 details 对象的一个 key,指向一个返回 this.name 的函数。既然我们已经找出 this 指向 details ,那函数的输出就应该是 'Jay Details'

person.print()

再来一次,找出 this 的指向。print() 是被 person 对象调用的,没错吧?

在这种情况,person 里的 print 函数返回 this.namethis 现在指向 person 了,那 'Jay Person' 就是返回值。

console.log(name1)

这一题就有点狡猾了。在上一行有这样一句代码:

var name1 = person.print;

如果你是通过这句来思考的,我不会怪你。很遗憾,这样去想是错的。要记住,this 关键词是在函数调用时才做绑定的。name1() 前面是什么?什么都没有。因此 this 关键词就将指向全局的 window 对象去。

因此,答案是 'Jay Global'

name2.print()

看一下 name2 指向哪个对象,是 details 对象没错吧?

所以下面这句会打印出什么呢?如果到目前为止的所有小点你都理解了,那这里稍微思考下你就自然有答案了。

console.log(name2.print()) // ??

答案是 'Jay Details',因为 print 是 name2 调起的,而 name2 指向 details

词法作用域

你可能会问:“什么是词法作用域?

逗我呢,我们不是在探讨 this 关键词吗,这个又是哪里冒出来的?好吧,当我们用起 ES6 的箭头函数,这个就要考虑了。如果你已经写了不止一年的 JavaScript,那你很可能已经碰到箭头函数。随着 ES6 逐渐成为现实标准,箭头函数也变得越来越常用。

JavaScript 的词法作用域 并不好懂。如果你 理解闭包,那要理解这个概念就容易多了。来看下下面的小段代码。

// outerFn 的词法作用域
var outerFn = function() {
   var n = 5;
   console.log(innerItem);
   // innerFn 的词法作用域
   var innerFn = function() {  
       var innerItem = "inner";    // 错了。只能坐着电梯向上,不能向下。
       console.log(n);
   };
   return innerFn;
};
outerFn()();    

想象一下一栋楼里面有一架只能向上走的诡异电梯。

深入浅出 JavaScript 关键词 -- thisJavaScript 的词法作用域就像楼里的一架只能向上走的诡异电梯

建筑的顶层就是全局 windows 对象。如果你现在在一楼,你就可以看到并访问那些放在楼上的东西,比如放在二楼的 outerFn 和放在三楼的 window 对象。

这就是为什么我们执行代码 outerFn()(),它在控制台打出了 5 而不是 undefined

然而,当我们试着在 outerFn 词法作用域下打出日志 innerItem,我们遇到了下面的报错。请记住,JavaScript 的词法作用域就好像建筑里面那个只能向上走的诡异电梯。由于 outerFn 的词法作用域在 innerFn 上面,所以它不能向下走到 innerFn 的词法作用域里面并拿到里面的值。这就是触发下面报错的原因:

test.html:304 Uncaught ReferenceError: innerItem is not defined
at outerFn (test.html:304)
at test.html:313

this 和箭头函数

在 ES6 里面,不管你喜欢与否,箭头函数被引入了进来。对于那些还没用惯箭头函数或者新学 JavaScript 的人来说,当箭头函数和 this 关键词混合使用时会发生什么,这个点可能会给你带来小小的困惑和淡淡的忧伤。那这个小节就是为你们准备的!

当涉及到 this 关键词,箭头函数 和 普通函数 主要的不同是什么?

答案:

箭头函数按词法作用域来绑定它的上下文,所以 this 实际上会引用到原来的上下文。

引自:hackernoon.com

我实在没法给出比这个更好的总结。

箭头函数保持它当前执行上下文的词法作用域不变,而普通函数则不会。换句话说,箭头函数从包含它的词法作用域中继承到了 this 的值。

我们不妨来测试一些代码片段,确保你真的理解了。想清楚这块知识点未来会让你少点头痛,因为你会发现 this 关键词和箭头函数太经常一起用了。

示例

仔细阅读下面的代码片段。

var object = {
   data: [1,2,3],
   dataDouble: [1,2,3],
   double: function() {
       console.log("this inside of outerFn double()");
       console.log(this);
       return this.data.map(function(item) {
           console.log(this);      // 这里的 this 是什么??
           return item * 2;
       });
   },
   doubleArrow: function() {
       console.log("this inside of outerFn doubleArrow()");
       console.log(this);
       return this.dataDouble.map(item => {
           console.log(this);      // 这里的 this 是什么??
           return item * 2;
       });
   }
};
object.double();
object.doubleArrow();

如果我们看执行上下文,那这两个函数都是被 object 调用的。所以,就此断定这两个函数里面的 this 都指向 object 不为过吧?是的,但我建议你拷贝这段代码然后自己测一下。

这里有个大问题:

arrow() 和 doubleArrow() 里面的 map 函数里面的 this 又指向哪里呢?

深入浅出 JavaScript 关键词 -- this

this 和箭头函数

上一张图已经给了一个大大的提示。如果你还不确定,那请花5分钟将我们上一节讨论的内容再好好想想。然后,根据你的理解,在实际执行代码前把你认为的 this 应该指向哪里写下来。在下一节我们将会回答这个问题。

回顾执行上下文

这个标题已经把答案泄露出来了。在你看不到的地方,map 函数对调用它的数组进行遍历,将数组的每一项传到回调函数里面并把执行结果返回。如果你对 JavaScript 的 map 函数不太了解或有所好奇,可以读读这个了解更多。

总之,由于 map() 是被 this.data 调起的,于是 this 将指向那个存储在 data 这个 key 里面的数组,即 [1,2,3]。同样的逻辑,this.dataDouble 应该指向另一个数组,值为 [1,2,3]

现在,如果函数是 object 调用的,我们已经确定 this 指向 object 对吧?好,那来看看下面的代码片段。

double: function() {
   return this.data.map(function(item) {
       console.log(this);      // 这里的 this 是什么??
       return item * 2;
   });
}

这里有个很有迷惑性的问题:传给 map() 的那个匿名函数是谁调用的?答案是:这里没有一个对象是。为了看得更明白,这里给出一个 map 函数的基本实现。

// Array.map polyfill
if (Array.prototype.map === undefined) {
   Array.prototype.map = function(fn) {
       var rv = [];
       for(var i=0, l=this.length; i<l; i++)
           rv.push(fn(this[i]));
       return rv;
   };
}

fn(this[i])); 前面有什么对象吗?没。因此,this 关键词指向全局的 windows 对象。那,为什么 this.dataDouble.map 使用了箭头函数会使得 this 指向 object 呢?

我想再说一遍这句话,因为它实在很重要:

箭头函数按词法作用域将它的上下文绑定到 原来的上下文

现在,你可能会问:原来的上下文是什么?问得好!

谁是 doubleArrow() 的初始调用者?就是 object 对吧?那它就是原来的上下文