详解this http://www.cnblogs.com/Wayou/p/all-this.html
2015-11-17 14:36
786 查看
习惯了高级语言的你或许觉得JavaScript中的
以下内容我希望广大同行们能够了解。全是掏箱底的干货,其中大部分占用了我很多时间才掌握。
全局
浏览器宿主的全局环境中,
示例
浏览器中在全局环境下,使用
示例
任何情况下,创建变量时没有使用
示例
Node命令行(REPL)中,
在Node环境里执行的JS脚本中,
当尝试在Node中执行JS脚本时,脚本中全局作用域中的
...但在命令行里进行求值却会赋值到
在Node里执行的脚本中,创建变量时没带
但在Node命令行里,就会赋值给两者了。
译注:简单来说,Node脚本中
函数或方法里的
除了DOM的事件回调或者提供了执行上下文(后面会提到)的情况,函数正常被调用(不带
示例
还有个例外,就是使用了
示例
当用调用函数时使用了
示例
通常我将这个新的上下文称作实例。
原型中的
函数创建后其实以一个函数对象的形式存在着。既然是对象,则自动获得了一个叫做
示例
当通过
示例
在实例中,
示例
...或者不通过实例,直接操作函数的原型。
示例
同一函数创建的所有实例均共享一个原型。如果你给原型赋值了一个数组,那么所有实例都能获取到这个数组。除非你在某个实例中对其进行了重写,实事上是进行了覆盖。
示例
通常上面的做法是不正确的(译注:改变
示例
多个函数可以形成原型链,这样
示例
很多人便是利用这个特性在JS中模拟经典的对象继承。
注意原型链底层函数中对
示例
我习惯将赋值到原型上的函数称作方法。上面某些地方便使用了方法这样的字眼,比如
原型链方法中的
示例
在JavaScript中,函数可以嵌套函数,也就是你可以在函数里面继续定义函数。但内层函数是通过闭包获取外层函数里定义的变量值的,而不是直接继承
示例
上面示例中,
还有更奇葩的。把实例的方法作为参数传递时,实例是不会跟着过去的。也就是说,此时方法中的
示例
所以很多人习惯将
示例
...但上面的方式不是万能的,在将方法做为参数传递时,就不起作用了。
示例
解决方法就是传递的时候使用
示例
同时也可以使用
示例
使用
示例
避免在构造函数中返回作何东西,因为返回的东西可能覆盖本来该返回的实例。
示例
但,如果你在构造函数里返回的是个原始值比如字符串或者数字什么的,上面的错误就不会发生了,返回语句将被忽略。所以最好别在一个将要通过
诚然,你也可以使用
示例
这种方式不会调用该构造函数。
示例
正因为
示例
对象中的
可以在对象的任何方法中使用
示例
注意这里并没有使用
示例
此时使用
示例
也可以不通过
示例
DOM 事件回调中的
在DOM事件的处理函数中,
示例
...除非你通过
示例
HTML中的
HTML标签的属性中是可能写JS的,这种情况下
示例
重写
无法重写
示例
示例
这里存在安全隐患。最好的办法就是避免使用
使用
示例
使用
使用
示例
正如很多人认为的那样,使用
jQuery中的
一如HTML DOM元素的事件回调,jQuery库中大多地方的
示例
传递
如果你用过underscore.js或者lo-dash你便知道,这两个库中很多方法你可以传递一个参数来显示指定执行的上下文。比如
示例
这样可以使得代码简洁些,不用层层嵌套
一些编程语言上手很简单,比如Go语言手册可以被快速读完。然后你差不多就掌握这门语言了,只是在实战时会有些小的问题或陷阱在等着你。
而JavaScript不是这样的。手册难读。非常多缺陷在里面,以至于人们抽离出了
欢迎勘误及讨论,我的推特@bjorntipling。
this跟Java这些面向对象语言相似,保存了实体属性的一些值。其实不然。将它视作幻影魔神比较恰当,手提一个装满未知符文的灵龛。
以下内容我希望广大同行们能够了解。全是掏箱底的干货,其中大部分占用了我很多时间才掌握。
全局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)中,
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。
console.log(this); console.log(this === global);
$ node test.js {} false
当尝试在Node中执行JS脚本时,脚本中全局作用域中的
var并不会将变量赋值给全局
this,这与在浏览器中是不一样的。
var foo = "bar"; console.log(this.foo);
$ node test.js undefined
...但在命令行里进行求值却会赋值到
this身上。
> var foo = "bar"; > this.foo bar > global.foo bar
在Node里执行的脚本中,创建变量时没带
var或
let关键字,会赋值给全局的
global但不是
this(译注:上面已经提到
this和
global不是同一个对象,所以这里就不奇怪了)。
foo = "bar"; console.log(this.foo); console.log(global.foo);
$ node test.js undefined
bar
但在Node命令行里,就会赋值给两者了。
译注:简单来说,Node脚本中
global和
this是区别对待的,而Node命令行中,两者可等效为同一对象。
函数或方法里的this
除了DOM的事件回调或者提供了执行上下文(后面会提到)的情况,函数正常被调用(不带new)时,里面的
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>
示例
foo = "bar"; function testThis () { this.foo = "foo"; } console.log(global.foo); testThis(); console.log(global.foo);
$ node test.js bar foo
还有个例外,就是使用了
"use strict";。此时
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指代一个新的上下文,不再指向全局
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
函数创建后其实以一个函数对象的形式存在着。既然是对象,则自动获得了一个叫做prototype的属性,可以自由地对这个属性进行赋值。当配合
new关键字来调用一个函数创建实例后,此刻便能直接访问到原型身上的值。
function Thing() { console.log(this.foo); } Thing.prototype.foo = "bar"; var thing = new Thing(); //logs "bar" console.log(thing.foo); //logs "bar"
示例
当通过
new的方式创建了多个实例后,他们会共用一个原型。比如,每个实例的
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自身其实只是个关键字。你可以把
this想象成在实例中获取原型值的一种途径,同时对
this赋值又会覆盖原型上的值。完全可以将新增的值从原型中删除从而将原型还原为初始状态。
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";
示例
...或者不通过实例,直接操作函数的原型。
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";
示例
同一函数创建的所有实例均共享一个原型。如果你给原型赋值了一个数组,那么所有实例都能获取到这个数组。除非你在某个实例中对其进行了重写,实事上是进行了覆盖。
function Thing() { } Thing.prototype.things = []; var thing1 = new Thing(); var thing2 = new Thing(); thing1.things.push("foo"); console.log(thing2.things); //logs ["foo"]
示例
通常上面的做法是不正确的(译注:改变
thing1的同时也影响了
thing2)。如果你想每个实例互不影响,那么请在函数里创建这些值,而不是在原型上。
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 []
示例
多个函数可以形成原型链,这样
this便会在原型链上逐步往上找直到找到你想引用的值。
function Thing1() { } Thing1.prototype.foo = "bar"; function Thing2() { } Thing2.prototype = new Thing1(); var thing = new Thing2(); console.log(thing.foo); //logs "bar"
示例
很多人便是利用这个特性在JS中模拟经典的对象继承。
注意原型链底层函数中对
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"
示例
我习惯将赋值到原型上的函数称作方法。上面某些地方便使用了方法这样的字眼,比如
logFoo方法。这些方法中的
this同样具有在原型链上查找引用的魔力。通常将最初用来创建实例的函数称作构造函数。
原型链方法中的
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指代是全局作用域或者是
undefined如果使用了
"use strict";声明的话。对于很多新手来说,理解这点是非常头疼的。
还有更奇葩的。把实例的方法作为参数传递时,实例是不会跟着过去的。也就是说,此时方法中的
this在调用时指向的是全局
this或者是
undefined在声明了
"use strict";时。
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或者其他什么的变量来保存,以将外层与内层的
this区分开来。
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可以任意改变函数或方法的执行上下文,即使它没有被绑定到一个实例的原型上。
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();
示例
对象中的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
示例
也可以不通过
this,直接访问对象的属性。
var obj = { foo: "bar", deeper: { logFoo: function () { console.log(obj.foo); } } }; obj.deeper.logFoo(); //logs "bar"
示例
DOM 事件回调中的this
在DOM事件的处理函数中,this指代的是被绑定该事件的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标签的属性中是可能写JS的,这种情况下this指代该HTML元素。
<div id="foo" onclick="console.log(this);"></div> <script type="text/javascript"> document.getElementById("foo").click(); //logs <div id="foo"... </script>
示例
重写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。
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也是指代的DOM元素。页面上的事件回调和一些便利的静态方法比如
$.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>
示例
传递 this
如果你用过underscore.js或者lo-dash你便知道,这两个库中很多方法你可以传递一个参数来显示指定执行的上下文。比如_.each。自ECMAScript 5 标准后,一些原生的JS方法也允许传递上下文,比如
forEach。事实上,上文提到的
bind,
apply还有
call已经给我们手动指定函数执行上下文的能力了。
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。
一些编程语言上手很简单,比如Go语言手册可以被快速读完。然后你差不多就掌握这门语言了,只是在实战时会有些小的问题或陷阱在等着你。
而JavaScript不是这样的。手册难读。非常多缺陷在里面,以至于人们抽离出了
它好的部分(The Good Parts)。最好的文档可能是MDN上的了。所以我建议你看看他上面关于
this的介绍,并且始终在搜索JS相关问题时加上"mdn" 来获得最好的文档资料。静态代码检查也是个不错的工具,比如jshint。
欢迎勘误及讨论,我的推特@bjorntipling。
相关文章推荐
- Android系列之网络(二)----获取HTTP请求头与响应头
- 交换机知识--网络安全介绍
- 总结几点无线Mesh网络的优点
- Android 判断用户2G/3G/4G移动数据网络
- 网络常用命令集锦
- 在RDO自动安装的OpenStack Liberty单机中搭建能够内外网互访的网络(一)-网络规划
- 内核线程 http://blog.chinaunix.net/uid-24467128-id-3246495.html
- 网络编程-TCP状态图详细说明 没有不懂的
- 《神经网络和深度学习》系列文章之目录
- 信息学奥林匹克竞赛-网络各层设备
- android https正确调用方案(防中间人劫持)
- FastJson和AsyncHttpCLient
- 安卓SDK下载国内镜像网站 http://gmirror.org/#android-sdk-tools-only
- 《神经网络和深度学习》系列文章十三:反向传播背后的四个基本等式
- 《神经网络和深度学习》系列文章十二:Hadamard积,s⊙t
- 《神经网络和深度学习》系列文章十一:关于损失函数的两个假设
- 《神经网络和深度学习》系列文章十:[热身]一个基于矩阵的快速计算神经网络输出的方法
- 《神经网络和深度学习》系列文章九:反向传播算法是如何工作的
- 《神经网络和深度学习》系列文章八:迈向深度学习
- 《神经网络和深度学习》系列文章七:实现我们的神经网络来分类数字(下)