当有人问起你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> 在浏览器里,在全局范围内,用var声明一个变量和给this或者window添加属性是等价的。 <script type="text/javascript"> var foo = "bar"; console.log(this.foo); //logs "bar" console.log(window.foo); //logs "bar" </script> 若是你在声明一个变量的时候没有使用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> 在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 在node环境里,若是执行一个js脚本,在全局范围内,this以一个空对象开始做为最高级的命名空间,这个时候,它和global不是等价的。 test.js脚本内容: console.log(this); console.log(this === global); REPL运行脚本: $ node test.js {} false 在node环境里,在全局范围内,若是你用REPL执行一个脚本文件,用var声明一个变量并不会和在浏览器里面同样将这个变量添加给this。 test.js: var foo = "bar"; console.log(this.foo); $ node test.js undefined 可是若是你不是用REPL执行脚本文件,而是直接执行代码,结果和在浏览器里面是同样的(神坑) > var foo = "bar"; > this.foo bar > global.foo bar 在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 上面的八种状况可能你们已经绕晕了,总结起来就是:在浏览器里面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> test.js foo = "bar"; function testThis () { this.foo = "foo"; } console.log(global.foo); testThis(); console.log(global.foo); $ node test.js bar foo 除非你使用严格模式,这时候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> 若是你在调用函数的时候在前面使用了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> 我更喜欢把新的值称做一个实例。 函数里面的this其实相对比较好理解,若是咱们在一个函数里面使用this,须要注意的就是咱们调用函数的方式,若是是正常的方式调用函数,this指代全局的this,若是咱们加一个new,这个函数就变成了一个构造函数,咱们就建立了一个实例,this指代这个实例,这个和其余面向对象的语言很像。另外,写JavaScript很常作的一件事就是绑定事件处理程序,也就是诸如button.addEventListener(‘click’, fn,false)之类的,若是在fn里面须要使用this,this指代事件处理程序对应的对象,也就是button。 prototype this 你建立的每个函数都是函数对象。它们会自动得到一个特殊的属性prototype,你能够给这个属性赋值。当你用new的方式调用一个函数的时候,你就能经过this访问你给prototype赋的值了。 function Thing() { console.log(this.foo); } Thing.prototype.foo = "bar"; var thing = new Thing(); //logs "bar" console.log(thing.foo); //logs "bar" 当你使用new为你的函数建立多个实例的时候,这些实例会共享你给prototype设定的值。对于下面的例子,当你调用this.foo的时候,都会返回相同的值,除非你在某个实例里面重写了本身的this.foo 复制代码 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } Thing.prototype.setFoo = function (newFoo) { this.foo = newFoo; } var thing1 = new Thing(); var thing2 = new Thing(); thing1.logFoo(); //logs "bar" thing2.logFoo(); //logs "bar" thing1.setFoo("foo"); thing1.logFoo(); //logs "foo"; thing2.logFoo(); //logs "bar"; thing2.foo = "foobar"; thing1.logFoo(); //logs "foo"; thing2.logFoo(); //logs "foobar"; 实例里面的this是一个特殊的对象。你能够把this想成一种获取prototype的值的一种方式。当你在一个实例里面直接给this添加属性的时候,会隐藏prototype中与之同名的属性。若是你想访问prototype中的这个属性值而不是你本身设定的属性值,你能够经过在实例里面删除你本身添加的属性的方式来实现。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } Thing.prototype.setFoo = function (newFoo) { this.foo = newFoo; } Thing.prototype.deleteFoo = function () { delete this.foo; } var thing = new Thing(); thing.setFoo("foo"); thing.logFoo(); //logs "foo"; thing.deleteFoo(); thing.logFoo(); //logs "bar"; thing.foo = "foobar"; thing.logFoo(); //logs "foobar"; delete thing.foo; thing.logFoo(); //logs "bar"; 或者你也能直接经过引用函数对象的prototype 来得到你须要的值。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo, Thing.prototype.foo); } var thing = new Thing(); thing.foo = "foo"; thing.logFoo(); //logs "foo bar"; 经过一个函数建立的实例会共享这个函数的prototype属性的值,若是你给这个函数的prototype赋值一个Array,那么全部的实例都会共享这个Array,除非你在实例里面重写了这个Array,这种状况下,函数的prototype的Array就会被隐藏掉。 function Thing() { } Thing.prototype.things = []; var thing1 = new Thing(); var thing2 = new Thing(); thing1.things.push("foo"); console.log(thing2.things); //logs ["foo"] 给一个函数的prototype赋值一个Array一般是一个错误的作法。若是你想每个实例有他们专属的Array,你应该在函数里面建立而不是在prototype里面建立。 function Thing() { this.things = []; } var thing1 = new Thing(); var thing2 = new Thing(); thing1.things.push("foo"); console.log(thing1.things); //logs ["foo"] console.log(thing2.things); //logs [] 实际上你能够经过把多个函数的prototype连接起来的从而造成一个原型链,所以this就会魔法般地沿着这条原型链往上查找直到找你你须要引用的值。 function Thing1() { } Thing1.prototype.foo = "bar"; function Thing2() { } Thing2.prototype = new Thing1(); var thing = new Thing2(); console.log(thing.foo); //logs "bar" 一些人利用原型链的特性来在JavaScript模仿经典的面向对象的继承方式。任何给用于构建原型链的函数的this的赋值的语句都会隐藏原型链上游的相同的属性。 function Thing1() { } Thing1.prototype.foo = "bar"; function Thing2() { this.foo = "foo"; } Thing2.prototype = new Thing1(); function Thing3() { } Thing3.prototype = new Thing2(); var thing = new Thing3(); console.log(thing.foo); //logs "foo" 我喜欢把被赋值给prototype的函数叫作方法。在上面的例子中,我已经使用过方法了,如logFoo。这些方法有着相同的prototype,即建立这些实力的原始函数。我一般把这些原始函数叫作构造函数。在prototype里面定义的方法里面使用this会影响到当前实例的原型链的上游的this。这意味着你直接给this赋值的时候,隐藏了原型链上游的相同的属性值。这个实例的任何方法都会使用这个最新的值而不是原型里面定义的这个相同的值。 function Thing1() { } Thing1.prototype.foo = "bar"; Thing1.prototype.logFoo = function () { console.log(this.foo); } function Thing2() { this.foo = "foo"; } Thing2.prototype = new Thing1(); var thing = new Thing2(); thing.logFoo(); //logs "foo"; 在JavaScript里面你能够嵌套函数,也就是你能够在函数里面定义函数。嵌套函数能够经过闭包捕获父函数的变量,可是这个函数没有继承this function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { var info = "attempting to log this.foo:"; function doIt() { console.log(info, this.foo); } doIt(); } var thing = new Thing(); thing.logFoo(); //logs "attempting to log this.foo: undefined" 在doIt里面的this是global对象或者在严格模式下面是undefined。这是形成不少不熟悉JavaScript的人深陷 this陷阱的根源。在这种状况下事情变得很是糟糕,就像你把一个实例的方法看成一个值,把这个值看成函数参数传递给另一个函数可是却不把这个实例传递给这个函数同样。在这种状况下,一个方法里面的环境变成了全局范围,或者在严格模式下面的undefined。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } function doIt(method) { method(); } var thing = new Thing(); thing.logFoo(); //logs "bar" doIt(thing.logFoo); //logs undefined 一些人喜欢先把this捕获到一个变量里面,一般这个变量叫作self,来避免上面这种状况的发生。 博主很是喜欢用这种方式 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { var self = this; var info = "attempting to log this.foo:"; function doIt() { console.log(info, self.foo); } doIt(); } var thing = new Thing(); thing.logFoo(); //logs "attempting to log this.foo: bar" 可是当你须要把一个方法做为一个值传递给一个函数的时候并无论用。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { var self = this; function doIt() { console.log(self.foo); } doIt(); } function doItIndirectly(method) { method(); } var thing = new Thing(); thing.logFoo(); //logs "bar" doItIndirectly(thing.logFoo); //logs undefined 你能够经过bind将实例和方法一切传递给函数来解决这个问题,bind是一个函数定义在全部函数和方法的函数对象上面。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } function doIt(method) { method(); } var thing = new Thing(); doIt(thing.logFoo.bind(thing)); //logs bar 你一样可使用apply和call来在新的上下文中调用方法或函数。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { function doIt() { console.log(this.foo); } doIt.apply(this); } function doItIndirectly(method) { method(); } var thing = new Thing(); doItIndirectly(thing.logFoo.bind(thing)); //logs bar 你能够用bind来代替任何一个函数或者方法的this,即使它没有赋值给实例的初始prototype。 function Thing() { } Thing.prototype.foo = "bar"; function logFoo(aStr) { console.log(aStr, this.foo); } var thing = new Thing(); logFoo.bind(thing)("using bind"); //logs "using bind bar" logFoo.apply(thing, ["using apply"]); //logs "using apply bar" logFoo.call(thing, "using call"); //logs "using call bar" logFoo("using nothing"); //logs "using nothing undefined" 你应该避免在构造函数里面返回任何东西,由于这可能代替原本应该返回的实例。 function Thing() { return {}; } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } var thing = new Thing(); thing.logFoo(); //Uncaught TypeError: undefined is not a function 奇怪的是,若是你在构造函数里面返回了一个原始值,上面所述的状况并不会发生而且返回语句被忽略了。最好不要在你将经过new调用的构造函数里面返回任何类型的数据,即使你知道本身正在作什么。若是你想建立一个工厂模式,经过一个函数来建立一个实例,这个时候不要使用new来调用函数。固然这个建议是可选的。 你能够经过使用Object.create来避免使用new,这样一样可以建立一个实例。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } var thing = Object.create(Thing.prototype); thing.logFoo(); //logs "bar" 在这种状况下并不会调用构造函数 function Thing() { this.foo = "foo"; } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } var thing = Object.create(Thing.prototype); thing.logFoo(); //logs "bar" 由于Object.create不会调用构造函数的特性在你继承模式下你想经过原型链重写构造函数的时候很是有用。 function Thing1() { this.foo = "foo"; } Thing1.prototype.foo = "bar"; function Thing2() { this.logFoo(); //logs "bar" Thing1.apply(this); this.logFoo(); //logs "foo" } Thing2.prototype = Object.create(Thing1.prototype); Thing2.prototype.logFoo = function () { console.log(this.foo); } var thing = new Thing2(); object this 在一个对象的一个函数里,你能够经过this来引用这个对象的其余属性。这个用new来新建一个实例是不同的。 var obj = { foo: "bar", logFoo: function () { console.log(this.foo); } }; obj.logFoo(); //logs "bar" 注意,没有使用new,没有使用Object.create,也没有使用函数调用建立一个对象。你也能够将对象看成一个实例将函数绑定到上面。 var obj = { foo: "bar" }; function logFoo() { console.log(this.foo); } logFoo.apply(obj); //logs "bar" 当你用这种方式使用this的时候,并不会越出当前的对象。只有有相同直接父元素的属性才能经过this共享变量。 var obj = { foo: "bar", deeper: { logFoo: function () { console.log(this.foo); } } }; obj.deeper.logFoo(); //logs undefined 你能够直接经过对象引用你须要的属性。 var obj = { foo: "bar", deeper: { logFoo: function () { console.log(obj.foo); } } }; obj.deeper.logFoo(); //logs "bar" DOM event this 在一个HTML DOM事件处理程序里面,this始终指向这个处理程序被所绑定到的HTML DOM节点 function Listener() { document.getElementById("foo").addEventListener("click", this.handleClick); } Listener.prototype.handleClick = function (event) { console.log(this); //logs "<div id="foo"></div>" } var listener = new Listener(); document.getElementById("foo").click(); 除非你本身经过bind切换了上下文。 function Listener() { document.getElementById("foo").addEventListener("click", this.handleClick.bind(this)); } Listener.prototype.handleClick = function (event) { console.log(this); //logs Listener {handleClick: function} } var listener = new Listener(); document.getElementById("foo").click(); HTML this 在HTML节点的属性里面,你能够放置JavaScript代码,this指向了这个元素 <div id="foo" onclick="console.log(this);"></div> <script type="text/javascript"> document.getElementById("foo").click(); //logs <div id="foo"... </script> override this 你不能重写this,由于它是保留字。 function test () { var this = {}; // Uncaught SyntaxError: Unexpected token this } eval this 你能够经过eval来访问this function Thing () { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { eval("console.log(this.foo)"); //logs "bar" } var thing = new Thing(); thing.logFoo(); 这会形成一个安全问题,除非不用eval,没有其余方式来避免这个问题。 在经过Function来建立一个函数的时候,一样可以访问this function Thing () { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = new Function("console.log(this.foo);"); var thing = new Thing(); thing.logFoo(); //logs "bar" with this 你能够经过with来将this添加到当前的执行环境,而且读写this的属性的时候不须要经过this function Thing () { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { with (this) { console.log(foo); foo = "foo"; } } var thing = new Thing(); thing.logFoo(); // logs "bar" console.log(thing.foo); // logs "foo" 许多人认为这样使用是很差的由于with自己就饱受争议。 jQuery this 和HTML DOM元素节点的事件处理程序同样,在许多状况下JQuery的this都指向HTML元素节点。这在事件处理程序和一些方便的方法中都是管用的,好比$.each <div class="foo bar1"></div> <div class="foo bar2"></div> <script type="text/javascript"> $(".foo").each(function () { console.log(this); //logs <div class="foo... }); $(".foo").on("click", function () { console.log(this); //logs <div class="foo... }); $(".foo").each(function () { this.click(); }); </script> thisArg this 若是你用过underscore.js 或者lo-dash 你可能知道许多类库的方法能够经过一个叫作thisArg 的函数参数来传递实例,这个函数参数会做为this的上下文。举个例子,这适用于_.each。原生的JavaScript在ECMAScript 5的时候也容许函数传递一个thisArg参数了,好比forEach。事实上,以前阐述的bind,apply和call的使用已经给你创造了传递thisArg参数给函数的机会。这个参数将this绑定为你所传递的对象。 function Thing(type) { this.type = type; } Thing.prototype.log = function (thing) { console.log(this.type, thing); } Thing.prototype.logThings = function (arr) { arr.forEach(this.log, this); // logs "fruit apples..." _.each(arr, this.log, this); //logs "fruit apples..." } var thing = new Thing("fruit"); thing.logThings(["apples", "oranges", "strawberries", "bananas"]); 这使得代码变得更加简介,由于避免了一大堆bind语句、函数嵌套和this暂存的使用。 做者:yuanzm 文章源自:http://segmentfault.com/a/1190000002640298