深入浅出 JavaScript 关键词 -- this
深入浅出 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
?
在我开始讲解前,如果你学过一门基于类的面向对象编程语言(比如 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
对象的值。引自: Stackoverflow.com
记住,现在起,我们专注于查明 this
关键词到底指向哪。因此,我们现在要思考的就一个问题:
是什么调用函数?是哪个对象调用了函数?
为了理解这个关键概念,我们来测一下下面的代码。
var person = {
name: "Jay",
greet: function() {
console.log("hello, " + this.name);
}
};
person.greet();
谁调用了 greet 函数?是 person
这个对象对吧?在 greet()
调用的左边是一个 person 对象,那么 this 关键词就指向 person
,this.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.name
。this
现在指向 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 的词法作用域就像楼里的一架只能向上走的诡异电梯
建筑的顶层就是全局 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
又指向哪里呢?
上一张图已经给了一个大大的提示。如果你还不确定,那请花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
对吧?那它就是原来的上下文
深入浅出 JavaScript 关键词 -- this的更多相关文章
- 深入浅出 JavaScript 对象 v0.5
JavaScript 没有类的概念,因此它的对象与基于类的语言中的对象有所不同.笔者主要参考<JS 高级程序设计>.<JS 权威指南>和<JS 精粹> 本文由浅入深 ...
- 深入浅出Javascript的正则表达式
深入浅出的javascript的正则表达式学习教程 阅读目录 了解正则表达式的方法 了解正则中的普通字符 了解正则中的方括号[]的含义 理解javascript中的元字符 RegExp特殊字符中的需要 ...
- 深入浅出JavaScript之原型链&继承
Javascript语言的继承机制,它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instanc ...
- 深入浅出JavaScript之闭包(Closure)
闭包(closure)是掌握Javascript从人门到深入一个非常重要的门槛,它是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面写下我的学习笔记~ 闭包-无处不 ...
- 深入浅出JavaScript之this
JavaScript中的this比较灵活,根据在不同环境下,或者同一个函数在不同方式调用下,this都有可能是不同的.但是有一个总的原则,那就是this指的是,调用函数的那个对象. 下面是我的学习笔记 ...
- 【转】深入浅出JavaScript之this
JavaScript中的this比较灵活,根据在不同环境下,或者同一个函数在不同方式调用下,this都有可能是不同的.但是有一个总的原则,那就是this指的是,调用函数的那个对象. 下面是我的学习笔记 ...
- 【转】深入浅出JavaScript之闭包(Closure)
闭包(closure)是掌握Javascript从人门到深入一个非常重要的门槛,它是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面写下我的学习笔记~ 闭包-无处不 ...
- 深入浅出 JavaScript 中的 this
在 Java 等面向对象的语言中,this 关键字的含义是明确且具体的,即指代当前对象.一般在编译期确定下来,或称为编译期绑定.而在 JavaScript 中,this 是动态绑定,或称为运行期绑定的 ...
- 【转】深入浅出 JavaScript 中的 this
Java 等面向对象的语言中,this 关键字的含义是明确且具体的,即指代当前对象.一般在编译期确定下来,或称为编译期绑定.而在 JavaScript 中,this 是动态绑定,或称为运行期绑定的,这 ...
随机推荐
- gcc-linaro-arm-linux-gnueabihf交叉编译器配置
系统Ubuntu14.04 版本:gcc 版本 4.7.3 20130328 (prerelease) (crosstool-NG linaro-1.13.1-4.7-2013.04-20130415 ...
- 计算机网络原理和OSI模型与TCP模型
计算机网络原理和OSI模型与TCP模型 一.计算机网络的概述 1.计算机网络的定义 计算机网络是一组自治计算机的互连的集合 2.计算机网络的基本功能 a.资源共享 b.分布式处理与负载均衡 c.综合信 ...
- EXISTS 与 NOT EXISTS 的用法及返回结果
(1)SELECT * FROM `datatower-all`.TMP_DWD_POI t1 WHERE EXISTS ( SELECT 1 FROM `datatower-all`.DWD_POI ...
- Hibernate SQL查询 addScalar()或addEntity()【转】
本文完全引用自: http://www.cnblogs.com/chenyixue/p/5601285.html Hibernate除了支持HQL查询外,还支持原生SQL查询. 对原 ...
- 服务器上的XML
若想让浏览器能访问Web项目,需要配置服务器里的XML文件,XML文件是类似于HtML文件的纯文本文件,可以通过Web服务器轻松的存储和生成. XML可以通过ASP,PHP,数据库生成XML
- form表单提交onclick和onsubmit
onsubmit只能表单上使用,提交表单前会触发, onclick是按钮等控件使用, 用来触发点击事件. 在提交表单前,一般都会进行数据验证,可以选择在submit按钮上的onclick中验证,也可以 ...
- JAVA迭代器学习--在JAVA中实现线性表的迭代器
1,迭代器是能够对数据结构如集合(ADT的实现)进行遍历的对象.在遍历过程中,可以查看.修改.添加以及删除元素,这是它与一般的采用循环来遍历集合中的元素不同的地方.因为,通常用循环进行的遍历操作一般是 ...
- 10 SpringBoot全面接管SpringMVC
Spring Boot官方文档描述 If you want to keep Spring Boot MVC features and you want to add additional MVC co ...
- 八、IIC 接口
8.1 IIC接口介绍 8.1.1 IIC 总线的概念 I2C总线是由Philips公司开发的一种简单.双向二线制同步串行总线.它只需要两根线即可在连接于总线上的器件之间传送信息. 主器件用于启动总线 ...
- SQL——将表中的最大ID+1插入新的ID中------Insert into 表 MAX(表id) +1
表结构:group表(groupid int,groupname varchar) 表中数据:id name 分组1 分组2 分组3 分组4 ----------------------------- ...