JavaScript this详解

2017-02-01  本文已影响732人  姜乐衣

英文原文地址:All this
翻译:姜乐衣

全局作用域下的this

在浏览器中,当this处于全局作用域时,thiswindow是相同的。

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

如果声明变量时(即使未在全局作用域下,比如函数中)没有使用varlet(ES6),你实际上是添加或改变一个全局作用域下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

但在直接执行一个脚本文件的时候,全局作用域下的this一开始是一个空对象。和global并不同。

//test.js
console.log(this);
console.log(this === global);
$ node test.js
{}
false

但如果你使用repl环境来做同样的事情,结果就不同了。

> var foo = "bar";
> this.foo
bar
> global.foo
bar

使用node直接执行脚本文件,创建变量不加varlet会把这个变量添加给global,但不会添加给处于脚本文件最顶层作用域的this

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

但在node的repl环境下,会添加给thisglobal两者。

函数作用域下的this

除了处于DOM事件处理函数和提供了thisArg参数两种情况(请看下面)之外的情况下。如果你在调用函数时没有使用newthis是等同于全局作用域下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); //打印 "bar"
    testThis();  //Uncaught TypeError: Cannot set property 'foo' of undefined 
</script>

如果你使用new调用函数,this则指向了一个新环境,不在指向全局作用域下的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调用函数来创建实例,便可以使用被赋予给prototype的属性。你将会利用this来使用这些值。

    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实际上是一个关键字。你可以认为this是使用定义在prototype上的值的渠道,但是在实例中直接对this赋值的行为却会隐藏本来定义在prototype上的值。当然如果你做了这样的事,想要再次获得本来定义在prototype上的值,可以删除你在实例中对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";

……或直接引用函数对象的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,多个实例会共享这个数组,除非你在实例中重写了它,也就是说你隐藏了它。

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


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

一般来说,赋予prototype数组或对象都是错误的行为。如果你需要每个实例拥有他们自己的数组,那就在函数里创造,而不是在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连成一条原型链(prototype chain),这样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"

我喜欢叫赋给prototype的值为「方法」。我已经使用了一些方法在上面的例子里,比如logFoo。这些方法都会获得一个「有魔力」的this原型,如同最开始那个用来创建实例的函数一样。我一般会叫这个原始函数为构造函数。

prototype上定义的方法里使用的this,无论在继承链的任何位置,都引用着当前的实例。这也就意味着,如果通过在继承链上对this直接复制来隐藏一个值,此实例上的方法都会使用新值,而不会再考虑这个方法实际是赋给哪个prototype的。

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"

"use strict"情况下,在doIt中的thisundefined,而默认情况下是global对象。这对于不熟悉JS的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"

……但是这仍然不会解决把方法当成参数传递不会传递实例的问题。

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,便可以连实例带方法一起传递,

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

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

译者注:这里的代码同时出现了两个内容点,有点难以理解,稍微解释一下,apply``call``bind都是用来改变一个方法调用环境的方法,前两者用处几乎相同,为直接改变函数的调用环境并调用,而bind则是只返回改变了环境的函数。上面的这个代码例子,可见我们在Thing构造函数中定义了logFoo方法,而此方法其中有一个局部函数,由上文可知,此局部函数中的this是指向着globalundefined的。然后,通过doIt.apply(this);改变了局部函数中的this指向,从而使其变为了原型对象this,并执行。而下文中的doItIndirectly(thing.logFoo.bind(thing));,如果没有bind语句,由于上文提到「把一个实例方法当做值,比如把一个方法当做参数传给一个函数,并不会把其所在实例一起传给函数」,故使用bind改变了log.Foo的执行环境为实例this再传给了doItIndirectly

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

奇怪的是,如果构造函数返回一个基本数据类型值,比如一个字符串或一个数值,这种情况就不会发生,同时返回语句也会被忽略。最好的方式是永远不要在一个你可能会用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"

因为Obeject.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"

注意,obj不是通过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事件中的属性

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

……除非你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指向所在元素。

<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的事件处理函数一样,jq库很多地方使用this来引用DOM元素。对于事件处理函数和一些方便的方法(比如$.each),this引用DOM元素。

<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.jslo-dash 你会知道,在库中,很多方法可以传入一个实例,通过thisArg函数参数,这个参数将会成为this环境。举个例子,_.each就是这样的。从ES5开始,原生的方法就允许thisArg,比如forEach。实际上,之前的bindapplycall的实例用法就教你一些使用thisArg的途径。实际上当你使用bind时,是将对象作为thisArg传入了。

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语句,也不用采用self代替this,代码显然更加清晰简洁了。

上一篇下一篇

猜你喜欢

热点阅读