22 Sep 2014

this-你所需要知道的一切!

源自于一种智能语言, 你或许认为JavaScript中的this类似于面向对象语言(比如:Java)中的this, 会引用一些存储在实例属性内的值。 但其实不是这样, JavaScript中, 最好把它当成一个被施了无形伸展咒的背着一个装满数据背包的幻形怪。

接下来的内容, 我希望我的同事了解如何在JavaScript中使用this. 此处内容很多并且大部分都使我花了几年去学习。

全局下的this

在浏览器内, 在全局作用域上, this指向window对象。

<script type="text/javascript">
console.log(this === window); //true
</script>

jsfiddle

浏览器中, 在全局作用域中使用var关键字为thiswindow分配属性.

<script type="text/javascript">
    var foo = "bar";
    console.log(this.foo); //logs "bar"
    console.log(window.foo); //logs "bar"
</script>

jsfiddle

如果你创建了一个新的变量却并没有使用varlet(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>

jsfiddle

在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中, 使用脚本, 创建一个没有varlet声明的变量会添加此变量到global对象, 但是不会分配给脚本顶层作用域的this

test.js:

foo = "bar";
console.log(this.foo);
console.log(global.foo);
$ node test.js
undefined
bar

在Node.js REPL中, 则会一起分配给两者。

函数中的this

除去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>

jsfiddle

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>

jsfiddle

如果使用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>

jsfiddle

我倾向于称呼这个新的上下文环境为实例.

原型内的this

被创建的函数会成为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"

jsfiddle

如果使用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";

jsfiddle

实例中的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";

jsfiddle

…或者直接引用函数对象的原型属性.

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";

jsfiddle

被同一个函数创建的实例会共享原型属性上的相同的属性值. 如果给原型分配一个数组, 那么所有实例都会共享同一个数据的访问权限, 除非你在实例内部覆写它, 这会隐藏原型属性上的值.

function Thing() {
}
Thing.prototype.things = [];


var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo");
console.log(thing2.things); //logs ["foo"]

jsfiddle

为原型属性分配数组与对象通常是一个错误. 如果你想每个实例拥有自己的属性, 那么请在函数中创建它们, 而不是原生属性上.

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 []

jsfiddle

实际上, 你可以链接起很多函数的原型从而形成一个原型链, 这样this的特殊魔力就会沿着原型链寻找直到找到引用的值.

function Thing1() {
}
Thing1.prototype.foo = "bar";

function Thing2() {
}
Thing2.prototype = new Thing1();


var thing = new Thing2();
console.log(thing.foo); //logs "bar"

jsfiddle

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"

jsfiddle

我喜欢调用被分配原型”方法”的函数。 我在上面的一些例子中使用了方法, 比如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";

jsfiddle

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"

jsfiddle

doIt函数中的thisglobal对象或者是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

jsfiddle

一些人喜欢在变量内捕获住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"

jsfiddle

…但是方法作为属性值进行传递的情况下, 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

jsfiddle

你可以使用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

jsfiddle

也可以使用applycall方法来用一个新的上下文环境调用方法或者函数.

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

jsfiddle

你可以使用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"

jsfiddle

需要避免在构造器中有返回值, 因为返回值会替换掉结果的实例.

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

jsfiddle

奇怪的是, 如果你在构造器中返回一个原始类型的值, 例如字符串或者数字, 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"

jsfiddle

这不会调用构造器.

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"

jsfiddle

因为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();

jsfiddle

对象中的this

你可以在任意函数中使用this以引用对象中其他的属性. 这种方式与通过new关键字创建的实例并不相同.

var obj = {
    foo: "bar",
    logFoo: function () {
        console.log(this.foo);
    }
};

obj.logFoo(); //logs "bar"

jsfiddle

注意, 此处并没有使用new, Object.create以及创建对象的函数. 你也可以绑定到多个对象上如果它们是实例.

var obj = {
    foo: "bar"
};

function logFoo() {
    console.log(this.foo);
}

logFoo.apply(obj); //logs "bar"

jsfiddle

当你使用这种方式时, 就不会遍历对象的层次结构. 只有直接父对象上共享的属性是对this可用的.

var obj = {
    foo: "bar",
    deeper: {
        logFoo: function () {
            console.log(this.foo);
        }
    }
};

obj.deeper.logFoo(); //logs undefined

jsfiddle

你可以直接引用想要使用的属性:

var obj = {
    foo: "bar",
    deeper: {
        logFoo: function () {
            console.log(obj.foo);
        }
    }
};

obj.deeper.logFoo(); //logs "bar"

jsfiddle

DOM事件中的this

在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();

jsfiddle

…除非你绑定了上下文环境.

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();

jsfiddle

HTML中的this

JavaScript所在的HTML属性内部, this是指向元素的引用.

<div id="foo" onclick="console.log(this);"></div>
<script type="text/javascript">
document.getElementById("foo").click(); //logs <div id="foo"...
</script>

jsfiddle

重写this

this是一个关键字, 因此它不可以被重写.

function test () {
    var this = {};  // Uncaught SyntaxError: Unexpected token this
}

jsfiddle

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();

jsfiddle

这种方式可能存在安全隐患. 而且除了不使用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"

jsfiddle

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"

jsfiddle

很多人认为这是很糟糕的做法, 因为使用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>

jsfiddle

thisArg this

如果你使用了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"]);

jsfiddle

这令代码变得更加整洁, 因为没有过多的嵌套绑定代码段以及无需使用”self”变量.

某些编程语言可能很易于学习, 例如:编程语言的规范很易读. 一旦读过了规范, 就可以理解语言, 并且没有什么可以感到忧虑的技巧与陷阱, 只需要关注实现的细节.

但是JavaScript并不是这样的一门语言. 它的规范并不是很易读. 它存在很多的”陷阱”, “The Good Parts“一书中就有很多大家谈论的”陷阱”. 所见的最好的文档在Mozilla Developer Network上. 我推荐阅读这个文档, 并且在Google搜索JavaScript问题时, 最好总是添加前缀”mdn”以获得最好的文档. 静态代码分析也有很大的帮助, 比如我使用的jshint.

原文地址: all this
原文作者: @bjorntipling on Twitter


Tags:
0 comments