当有人问起你JavaScript有什么特点的时候,你可能立马就想到了单线程、事件驱动、面向对象等一堆词语,但是如果真的让你解释一下这些概念,可能真解释不清楚。有句话这么说:如果你不能向一个6岁小孩解释清楚一个东西,那么你自己也不懂这个东西。这句话或许有点夸张,但是极其有道理。个人觉得,如果需要掌握一门语言,掌握它的API只是学了皮毛,理解这门语言的精髓才是重点。提及JavaScript的精髓,this、闭包、作用域链、函数是当之无愧的。这门语言正式因为这几个东西而变得魅力无穷。
博客的标题是《JavaScript中的this陷阱的最全收集--没有之一》,很显然这篇博客阐述的是this。相信做过JavaScript开发的人都遇到过不少this的陷阱,我自己本身也遇到过不少坑,但是如果非要给出一个系统的总结的话,还没有足够的底蕴。非常幸运的是,今天早上起来看《Hacker News》的时候,恰巧看到了一篇有关于JavaScript this的解析:all this。于是,本着学习和共享的精神,决定将它翻译成中文。翻译的目的绝对不是为了当大自然的搬运工,在这个过程中会完全弄明白别人的著作,加深认识,同时将好东西分享给别人,才能让更多的学习者站在巨人的肩膀上前进。按照我自己的习惯,会翻译的过程中加上一些自己解释(引用部分),毕竟中西方人的思考方式是有差异的。当然文章标题所述的最全也不是吹的,文章非常长。
JavaScript来自一门健全的语言,所以你可能觉得JavaScript中的this和其他面向对象的语言如java的this一样,是指存储在实例属性中的值。事实并非如此,在JavaScript中,最好把this当成哈利波特中的博格特的背包,有着深不可测的魔力。
下面的部分是我希望我的同事在使用JavaScript的this的时候应当知道的。内容很多,是我学习好几年总结出来的。
JavaScript中很多时候会用到this,下面详细介绍每一种情况。在这里我想首先介绍一下宿主环境这个概念。一门语言在运行的时候,需要一个环境,叫做宿主环境。对于JavaScript,宿主环境最常见的是web浏览器,浏览器提供了一个JavaScript运行的环境,这个环境里面,需要提供一些接口,好让JavaScript引擎能够和宿主环境对接。JavaScript引擎才是真正执行JavaScript代码的地方,常见的引擎有V8(目前最快JavaScript引擎、Google生产)、JavaScript core。JavaScript引擎主要做了下面几件事情:
- 一套与宿主环境相联系的规则;
- JavaScript引擎内核(基本语法规范、逻辑、命令和算法);
- 一组内置对象和API;
- 其他约定。
但是环境不是唯一的,也就是JavaScript不仅仅能够在浏览器里面跑,也能在其他提供了宿主环境的程序里面跑,最常见的就是nodejs。同样作为一个宿主环境,nodejs也有自己的JavaScript引擎--V8。根据官方的定义:
Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications
global this
在浏览器里,在全局范围内,this等价于window对象。
<script type="text/javascript"> console.log(this === window); //true </script></div>
在浏览器里,在全局范围内,用var声明一个变量和给this或者window添加属性是等价的。
<script type="text/javascript"> var foo = "bar"; console.log(this.foo); //logs "bar" console.log(window.foo); //logs "bar" </script></div>
如果你在声明一个变量的时候没有使用var或者let(ECMAScript 6),你就是在给全局的this添加或者改变属性值。
<script type="text/javascript"> foo = "bar"; function testThis() { foo = "foo"; } console.log(this.foo); //logs "bar" testThis(); console.log(this.foo); //logs "foo" </script></div>
在node环境里,如果使用REPL(Read-Eval-Print Loop,简称REPL:读取-求值-输出,是一个简单的,交互式的编程环境)来执行程序,this并不是最高级的命名空间,最高级的是global.
> this { ArrayBuffer: [Function: ArrayBuffer], Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 }, Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 }, ... > global === this true</div>
在node环境里,如果执行一个js脚本,在全局范围内,this以一个空对象开始作为最高级的命名空间,这个时候,它和global不是等价的。
test.js脚本内容:
console.log(this); console.log(this === global);</div>
REPL运行脚本:
$ node test.js {} false</div>
在node环境里,在全局范围内,如果你用REPL执行一个脚本文件,用var声明一个变量并不会和在浏览器里面一样将这个变量添加给this。
test.js: var foo = "bar"; console.log(this.foo); $ node test.js undefined</div>
但是如果你不是用REPL执行脚本文件,而是直接执行代码,结果和在浏览器里面是一样的(神坑)
> var foo = "bar"; > this.foo bar > global.foo bar</div>
在node环境里,用REPL运行脚本文件的时候,如果在声明变量的时候没有使用var或者let,这个变量会自动添加到global对象,但是不会自动添加给this对象。如果是直接执行代码,则会同时添加给global和this
test.js foo = "bar"; console.log(this.foo); console.log(global.foo); $ node test.js undefined bar</div>
上面的八种情况可能大家已经绕晕了,总结起来就是:在浏览器里面this是老大,它等价于window对象,如果你声明一些全局变量(不管在任何地方),这些变量都会作为this的属性。在node里面,有两种执行JavaScript代码的方式,一种是直接执行写好的JavaScript文件,另外一种是直接在里面执行一行行代码。对于直接运行一行行JavaScript代码的方式,global才是老大,this和它是等价的。在这种情况下,和浏览器比较相似,也就是声明一些全局变量会自动添加给老大global,顺带也会添加给this。但是在node里面直接脚本文件就不一样了,你声明的全局变量不会自动添加到this,但是会添加到global对象。所以相同点是,在全局范围内,全局变量终究是属于老大的。
function this
无论是在浏览器环境还是node环境, 除了在DOM事件处理程序里或者给出了thisArg(接下来会讲到)外,如果不是用new调用,在函数里面使用this都是指代全局范围的this。
<script type="text/javascript"> foo = "bar"; function testThis() { this.foo = "foo"; } console.log(this.foo); //logs "bar" testThis(); console.log(this.foo); //logs "foo" </script></div>
test.js foo = "bar"; function testThis () { this.foo = "foo"; } console.log(global.foo); testThis(); console.log(global.foo); $ node test.js bar foo</div>
除非你使用严格模式,这时候this就会变成undefined。
<script type="text/javascript"> foo = "bar"; function testThis() { "use strict"; this.foo = "foo"; } console.log(this.foo); //logs "bar" testThis(); //Uncaught TypeError: Cannot set property 'foo' of undefined </script></div>
如果你在调用函数的时候在前面使用了new,this就会变成一个新的值,和global的this脱离干系。
<script type="text/javascript"> foo = "bar"; function testThis() { this.foo = "foo"; } console.log(this.foo); //logs "bar" new testThis(); console.log(this.foo); //logs "bar" console.log(new testThis().foo); //logs "foo" </script></div>
我更喜欢把新的值称作一个实例。
函数里面的this其实相对比较好理解,如果我们在一个函数里面使用this,需要注意的就是我们调用函数的方式,如果是正常的方式调用函数,this指代全局的this,如果我们加一个new,这个函数就变成了一个构造函数,我们就创建了一个实例,this指代这个实例,这个和其他面向对象的语言很像。另外,写JavaScript很常做的一件事就是绑定事件处理程序,也就是诸如button.addEventListener(‘click', fn, false)之类的,如果在fn里面需要使用this,this