[翻译] All this in Javascript

作者:frank 发表日期:2016-06-21 22:57:39 更新日期:2016-06-23 09:41:50 分类:猿文色

摘要

Javascript 中所有 this 相关的问题!

正文

指向全局对象的 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 (ES6) 关键字,则相当于为全局对象添加了属性或者更新了全局对象的属性。

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    testThis();
    console.log(this.foo); //logs "foo"
</script>

在 NodeJS 终端执行代码,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

同样,在一段脚本文件中,在全局作用域中使用 var 声明的变量并不会像在浏览器中一样将此变量做为 this 的属性。

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

但是在终端中却是可以。

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

再进一步,如果在脚本文件的全局作用下没有使用 var 或者 let 定义变量,而是直接赋值使用,则此变量会被添加到 global 对象的属性,而不是脚本文件的顶级作用域 this 中。

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

在终端中,因为 this === global,不会存在上述情况。

函数相关的 this

除了在 DOM 事件处理函数中或显示为某个函数绑定 this 外(下面会介绍),不管是在 NodeJS 环境下还是在浏览器环境下,没有使用 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>
// NodeJS: 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 就指向函数返回的对象(即此函数的实例)。

<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

众所周知,在 javascript 中函数也是对象。当我们定义一个函数时,函数会拥有一个特殊的对象属性 prototype(原型对象)。当使用 new 操作符通过一个构造函数创建一个实例时,可以通过返回的实例获取到原型对象的属性。

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

Thing.prototype.foo = "bar";

var thing = new Thing(); //logs "bar",返回的实例
console.log(thing.foo);  //logs "bar",通过实例访问 prototype 中的属性

如果创建了多个实例,那所有的实例都会共享原型对象中的属性,除非在构造函数中覆盖了原型对象的属性。可以这样说,原型对象是所有实例共享的属性的集合。

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 设置 foo
thing1.setFoo("foo");
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "bar";

// 为 thing2 设置 foo
thing2.foo = "foobar";
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "foobar";

与构造函数相关的 this 是一个非常特殊的对象。当试图访问一个实例的属性时,会首先查找构造函数中有没有定义此属性,如果没有,则在构造函数的原型对象中继续查找。如果要使用 this 访问原型对象中的同名属性,可以通过 delete 操作符删除构造函数中的属性实现。

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

由于原型对象也是一个对象,所以我们就可以将某个构造函数 A 的实例赋值给另一个构造函数 B 的原型对象以实现原型链。为什么称为原型“链”?假设有一个 B 的实例 b,此时要访问 b 的某个属性,如果 B 中没有定义这个属性,则会继续访问 B 的原型对象(A 的一个实例 a),如果 B 的原型对象也没有这个属性,由于 B 的原型对象是 A 的一个实例,则会继续查找 A 的原型对象。这便是原型链的真正含义。

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

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

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

可以通过原型链实现面向对象中的继承。但是要谨记,构造函数中的属性会覆盖其原型对象中的同名属性,而原型链中同名属性也会被覆盖。

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。对方法查找与对属性的查找类似。通常将使用 new 创造实例的函数称之为构造函数。

实例方法中的 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 同样遵循上述“函数相关的 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。

比较让人费解的是,如果尝试将实例的方法作为参数传递给某个函数,此时并不会传递实例,也就是说,此方法中的 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 的引用。

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"

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

最好避免在构造函数中使用 return 语句,因为 return 语句可能会覆盖构造函数的原始行为(返回实例的引用)。

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

令人费解的是,构造函数中 return 的字符串或者数字会被忽略掉,仍然会返回实例。所以除非你确实知道自己在干啥,否则不要在构造函数中使用 return 语句。如果要使用工厂模式,不要使用 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"

使用 Object.create 不会调用构造函数。

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 创建的实例中使用 this 是不同的。

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

obj.logFoo(); //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"

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

除非显示绑定 this。

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 元素的 on{event} 属性中嵌入 javascript 代码,this 指向这个元素。

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

覆盖 this

由于 this 在 javascript 中是个关键字,所以在函数中不能定义 this。

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

eval 中的 this

在 eval 中也可以访问到 this。如果是直接调用 eval,则 this 指向当前对象,如果是间接调用,this指向全局作用域。尽量不要使用 eval。

function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    // 直接调用
    eval("console.log(this.foo)"); //logs "bar"
}

var thing = new Thing();
thing.logFoo();

(0, eval)('this === widnwo'); // 间接调用

使用 new 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 关键字创建一个临时的作用域,with 会将参数对象添加到作用域链的顶部,可以方便的访问到参数对象中的属性。

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,因为 with 块中的赋值操作可能会不经意的覆盖父作用域中的属性。

var foo = {
    name: 'Jim'
};

with(foo) {
    name = 'Frank';
    age = '20';
};

console.log(foo.name); //'Frank'
console.log(foo.age); //undefiend
console.log(age); //20

jQuery 中的 this

jQuery 中的 this 在大部分 jQuery 事件回调函数中都指向相关的 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>

参数中的 this

在某些库中,比如 underscore.js 或者 lo-dash,其中有一些函数可以通过参数设置函数执行的上下文,即为函数指定特定的 this。比如,ECMAScript 5中的 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"]);

原文

点击访问

后记

Some programming languages can be simple to learn, for example the specification for the go programming language can be read in a short time. Once the specification has been read, one understands the language, and there are few tricks or gotchas to be troubled about, barring implementation details.

JavaScript is not such a language. The specification is not very readable. There are many “gotchas”, so many in fact that people talk about “The Good Parts.” The best documentation is found on the Mozilla Developer Network. I recommend reading the documentation there about this and to always prefix your Google searches for JavaScript with “mdn” to always get the best documentation. Static code analysis is also great help, for that I use jshint.

If I have made any mistakes or if you have any questions, please let me know I am @bjorntipling on Twitter.