源自于一种智能语言, 你或许认为JavaScript中的this类似于面向对象语言(比如:Java)中的this
, 会引用一些存储在实例属性内的值。 但其实不是这样, JavaScript中, 最好把它当成一个被施了无形伸展咒的背着一个装满数据背包的幻形怪。
接下来的内容, 我希望我的同事了解如何在JavaScript中使用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>
在REPL Node.js中, 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);
$ node test.js
{}
false
Node.js中, 当执行一个脚本时, 全局作用域上的var
关键字不会像浏览器那样为this
分配属性…
test.js:
var foo = "bar";
console.log(this.foo);
$ node test.js
undefined
…但是如果在node repl中同样操作, 就会与浏览器内行为一样。
> var foo = "bar";
> this.foo
bar
> global.foo
bar
Node.js中, 使用脚本, 创建一个没有var
或let
声明的变量会添加此变量到global
对象, 但是不会分配给脚本顶层作用域的this
test.js:
foo = "bar";
console.log(this.foo);
console.log(global.foo);
$ node test.js
undefined
bar
在Node.js REPL中, 则会一起分配给两者。
除去DOM事件处理器或存在thisArg
(详见下文)的情况, Node.js与浏览器环境下,在非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>
test.js:
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>
我倾向于称呼这个新的上下文环境为实例.
被创建的函数会成为function的对象. 它们会自动获得一个特殊的原型属性, 你可以在这个属性上赋值。 当通过new
关键字来创建函数实例时, 就可以访问到之前赋值的属性. 属性的访问通过this
实现.
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"]
为原型属性分配数组与对象通常是一个错误. 如果你想每个实例拥有自己的属性, 那么请在函数中创建它们, 而不是原生属性上.
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"
JavaScript中, 一些人使用this
来模拟传统的面向对象的继承.
函数中, 任何在函数中对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
. 也就是说如果你通过直接分配属性来隐藏原型链上的一个值, 此实例的任意可用方法都会使用这个新值而不管那个方法被分配的是什么原型属性.
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
(使用严格模式"use strict"
的况下); 这是一个给大量不熟悉JavaScript中this
的人带来很多苦恼的源码.
更加糟糕的是. 分配一个实例的方法作为属性值, 好比说你传递给函数一个方法作为参数, 并没有传递它的实例. 方法中this
的上下文环境会回退到指向global
对象, 或者是使用严格模式下"use strict";
的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”并避免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"
…但是方法作为属性值进行传递的情况下, this
不会保存.
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对象上供所有函数和方法使用的函数.
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
, 即使它没有被分配到实例最初的原型属性.
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
奇怪的是, 如果你在构造器中返回一个原始类型的值, 例如字符串或者数字, this
就不会出现并且返回代码会被忽略掉. 最好在要用new
关键字调用的构造器中不返回任何值, 即使你知道你在做什么. 如果你像创造一个工程模式, 使用一个函数去创建实例并且不要使用new
调用. 这个建议只是一个观点, 你可以避免使用new
而以Object.create
代替. 这个方法同样可以创建实例.
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
以引用对象中其他的属性. 这种方式与通过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
可用的.
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"
在HTML 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();
…除非你绑定了上下文环境.
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();
JavaScript所在的HTML属性内部, this
是指向元素的引用.
<div id="foo" onclick="console.log(this);"></div>
<script type="text/javascript">
document.getElementById("foo").click(); //logs <div id="foo"...
</script>
this
是一个关键字, 因此它不可以被重写.
function test () {
var this = {}; // Uncaught SyntaxError: Unexpected token 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
添加到当前作用域, 以便对于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
的语义模糊问题.
同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>
如果你使用了underscore.js或者lo-dash, 你或许了解在很多的类库方法中都可以传递实例, 并将函数参数thisArg
作为this
的上下文使用, 例如: 对于_.each
是适用的. 自ECMAScript 5起, JavaScript中的本地方法也支持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"]);
这令代码变得更加整洁, 因为没有过多的嵌套绑定代码段以及无需使用”self”变量.
某些编程语言可能很易于学习, 例如:编程语言的规范很易读. 一旦读过了规范, 就可以理解语言, 并且没有什么可以感到忧虑的技巧与陷阱, 只需要关注实现的细节.
但是JavaScript并不是这样的一门语言. 它的规范并不是很易读. 它存在很多的”陷阱”, “The Good Parts“一书中就有很多大家谈论的”陷阱”. 所见的最好的文档在Mozilla Developer Network上. 我推荐阅读这个文档, 并且在Google搜索JavaScript问题时, 最好总是添加前缀”mdn”以获得最好的文档. 静态代码分析也有很大的帮助, 比如我使用的jshint
.
原文地址: all this
原文作者: @bjorntipling on Twitter