[译] all this / JAVAScript/NodeJS 中 this 那些事儿

原文 all this

译者注: 我在这之前只写过管理后台类型的 JS 代码, 所以翻译这篇含金量很高的文章会有一些问题, 一是之前没想过会有 this 这种困扰, 二是作者用了很多长句子, 有的问题只好生硬直译或者按照我理解的意思换上我的表达, 建议粗看后去看原文
重点在最后几段, 建议看完

你可能认为 JavaScript 中的 this 和面向对象语言如 JAVA 中的 this 一样, 是对实例属性的引用. 事实不是这样的, JavaScript 中的 this 是一个潘多拉魔盒(译者注: 作者引用了哈利波特中黑魔法防御课卢平教授用来关恶灵的柜子..)

下面是我想让我的工作伙伴都了解的 JavaScript 中 this 的用法, 内容很丰富, 有些知识点我花了几年的时间才能学到

global this

在浏览器的全局作用域中, this === window

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

在浏览器中, 用全局作用域中用 var 和给 this/window 赋值是一样的

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

如果你定义变量时没有用 var 就相当于给 this 增加/改变 属性

<script type="text/javascript">
foo  = "foo";
function testThis () {
    foo = "bar";
}
console.log(this.foo);  // foo
testThis();
console.log(this.foo);  // bar
</script>

在 node 的交互模式下, this 是顶级命名空间, this === global
var 定义变量时, 会赋值给 this/global

> this
{ ArrayBuffer: [Function: ArrayBuffer],
  Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 },
  Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 },
  Uint8ClampedArray: { [Function: Uint8ClampedArray] BYTES_PER_ELEMENT: 1 },
  Int16Array: { [Function: Int16Array] BYTES_PER_ELEMENT: 2 },
  Uint16Array: { [Function: Uint16Array] BYTES_PER_ELEMENT: 2 },
  Int32Array: { [Function: Int32Array] BYTES_PER_ELEMENT: 4 },
  Uint32Array: { [Function: Uint32Array] BYTES_PER_ELEMENT: 4 },
  Float32Array: { [Function: Float32Array] BYTES_PER_ELEMENT: 4 },
  Float64Array: { [Function: Float64Array] BYTES_PER_ELEMENT: 8 },
  DataView: [Function: DataView],
  global: [Circular],
  process: 
   { title: 'node',
     version: 'v0.10.29',
> this === global
true
> var foo = "bar";
> this.foo
bar
> global.foo
bar

在用 node 执行的脚本中, this 是一个空对象, 和 global 不一样
var 赋值时, 不会像在浏览器中一样, 赋值给 this

// test.js
console.log(this);
console.log(this === global);
var foo = "bar";
console.log(this.foo);
console.log(this.foo);
bar = "foo";
console.log(this.bar);
console.log(global.bar);

输出

$ node test.js
{}
false
undefined
undefined
undefined
foo

function this

除了在 DOM 事件处理函数参数中提供的 this 参数外, 在 node 和浏览器的 function 中, 如果不是用 new 引用这个函数, this === global

<script type="text/javascript">
foo = "bar";
function testThis() {
    this.foo = "foo";
}
console.log(this.foo);      // bar
testThis();
console.log(this.foo);      // foo

function testThis2() {
    this.foo = "f2f";
}
new testThis2();
console.log(this.foo);      // foo
console.log(new testThis2().foo)    // f2f     instance

function testThis3() {
    "use strict";
    this.foo = "foo3";
}
testThis3();        // Uncaught TypeError: Cannot set property 'foo' of undefined
console.log(new testThis3().foo);   // foo3
</script>

在 node 中的输出也一样

prototype this

你创建的 function 是 function object, 创建后自动添加一个 prototype 属性, 可以用来赋值, 当你用 new 创建 function object 时, 你可以 通过 this 访问 prototype 属性中的值

function Car() {
    console.log(this.foo);
}
Car.prototype.foo = "bar";
var car = new Car();        // bar
console.log(car.foo);       // bar

如果有多个 function 实例, 这些实例间共享 prototype, 可以重写实例属性

function Car() {
}
Car.prototype.color = "B";
Car.prototype.logColor = function () {
    console.log(this.color);
}
Car.prototype.setColor = function (newColor) {
    this.color = newColor;
}

var car1 = new Car();
var car2 = new Car();
car1.logColor();        // B
car2.logColor();        // B

// share prototype in instances
Car.prototype.color = 'R';
car1.logColor();        // R
car2.logColor();        // R

// override individual instance property
car1.setColor("G");
car1.logColor();        // G
car2.logColor();        // R

实例中的 this 是一种特殊对象, 它实际上是一个关键词. 可以把它认为是访问 prototype 中属性的方式. 如果直接给实例中的 this 赋值, 就访问不到 prototype 中的属性了, 删除赋值后可以恢复访问

function Car() {
}
Car.prototype.color = 'B';
Car.prototype.colors = [];

var car1 = new Car();
var car2 = new Car();
car1.color = 'R';
console.log(car1.color);        // R
console.log(car2.color);        // B

car1.colors.push("O");
console.log(car1.colors);       // "[O]"
console.log(car2.colors);       // "[O]"

car2.colors.push("L");
console.log(car1.colors);       // ["O", "L"]
console.log(car2.colors);       // ["O", "L"]

car1.colors.pop("L");
console.log(car1.colors);       // ["O"]
console.log(car2.colors);       // ["O"]

不能在 prototype 上赋 array 类型的值, 如果你想让实例独享 array 类型的属性, 可以在 function 中创建这些属性

function Car() {
    this.colors = [];
}
var car1 = new Car();
var car2 = new Car();

car1.colors.push("B");
console.log(car1.colors);       // ["B"]
console.log(car2.colors);       // []

car2.colors.push("G");
console.log(car1.colors);       // ["B"]
console.log(car2.colors);       // ["G"]

你可以把很多函数的 prototype 连起来组成 prototype chain, 这样用 this 引用属性时会沿着 prototype chain

function Car() {
  this.seats = 4;
}
Car.prototype.color = 'Blue';
function Driver() {
}
Driver.prototype = new Car();
var driver = new Driver();
console.log(driver.color, driver.seats);    // Blue 4

有人在 JavaScript 中用 this 模拟面向对象中的继承.

Any assignments to this in functions used to create the prototype chain will hide values defined further up the prototype chain.

不知怎么翻译, 但是从代码结果看, 对 this 的赋值符合 OO 的特性

function Hard() {
  this.color = "Black";
}
function Car() {
  this.color = 'Orange';
}
Car.prototype = new Hard();
function Tank() {}
Tank.prototype = new Car();
function AirPlain() {}
AirPlain.prototype = new Hard();

console.log(new Tank().color);    // Orange
console.log(new AirPlain().color);  // Black

我把赋值到 prototype 的函数称作 methods 方法, 我在上面的例子中用了一些 methods(方法) 如 logColor. 这些 methods(方法) 和用来创建它的源函数一样, 有 this 这个魔法.

prototype 上定义的方法中引用的 this 指向当前实例

function VW() {
  this.color = 'Black';
}
VW.prototype.logColor = function() {
  console.log(this.color);
}
function Volvo() {
  this.color = 'Slivery';
}
Volvo.prototype = new VW();

var car = new Volvo();
car.logColor();     // Slivery

在 JavaScript 中你可以嵌套函数, 也就是说可以在函数中定义函数, 当嵌套函数在闭包中引用在父函数中定义的变量时, 并不会继承 this

var foo = "bar";
function Car() {}
Car.prototype.color = "Slivery";
Car.prototype.logColorInClosure = function() {
  var info = "Log color in closure";
  function logIt() {
    console.log(info, this.color);
    console.log(this.foor);
  }
  logIt();
}
var car  = new Car();
car.logColorInClosure();
// Log color in closure undefined
// bar

logColorInClosure 中的 thisglobal 中的对象(例子中的 foo), 在 use strict 情况下是 undefined. 这对于很多不熟悉 JavaScript 中 this 的人来说痛苦的根源.

有更糟糕的情况, 如果把实例方法当作一个函数参数, 方法中的 this 就恢复为对 global 的引用了, 在 use strict 的情况下是 undefined

function Car() {
  this.color = 'Slivery';
  this.logColor = function() {
    console.log(this.color);
  }
}
function logColorInAnother(method) {
  method();
}
var car = new Car();
car.logColor();                 // Slivery
logColorInAnother(car.logColor);    // undefined

有的人倾向于用 self 捕捉 this, 避免同时用 this/that

function Car() {}
Car.prototype.color = 'Slivery';
Car.prototype.logColor = function() {
  var self = this;
  function logColorInClosure() {
    console.log(self.color, this.color);
  }
  logColorInClosure();
}
var car = new Car();
car.logColor();         // Slivery undefined

但是如果你需要把方法当成值到处传递, 上面的方法就有问题了

function Car() {}
Car.prototype.color = 'Slivery';
Car.prototype.logColor = function() {
  var self = this;
  function logColorInClosure() {
    console.log(self.color);
  }
  logColorInClosure();
}
var car = new Car();
car.logColor();           // Slivery

function doIt(method) {
  method();
}
doIt(car.logColor);       // undefined

你可以用函数的 bind 方法绑定实例后传递

function Car() {}
Car.prototype.color = 'Slivery';
Car.prototype.logColor = function() {
  function logColorInClosure() {
    console.log(this.color);
  }
  logColorInClosure();
}
function doIt(method) {
  method();
}
var car = new Car();
doIt(car.logColor.bind(car));       // Slivery

也可以用 apply/call 立即调用函数或方法

function Car() {}
Car.prototype.color = 'Slivery';
Car.prototype.logColor = function() {
  function logColorInClosure() {
    console.log(this.color);
  }
  logColorInClosure.apply(this);
  // logColorInClosure.call(this);
}
function doIt(method) {
  method();
}
var car = new Car();
doIt(car.logColor.bind(car));       // Slivery

对任何函数或方法, 你可以用 bind 代替 this, 即使它没有被赋值到 prototype

function Car() {}
Car.prototype.color = 'Slivery';

function logColor() {
  console.log(this.color);
}
var car = new Car();
logColor.bind(car)();         // Slivery
logColor.apply(car);          // Slivery
logColor.call(car);           // Slivery
logColor();                   // undefined

如果你在 function 中返回了任何东西, 所有的魔法就都消失了

function Car() {
  return {};
}
Car.prototype.color = "Slivery";
Car.prorotype.logColor = function() {
  console.log(this.color);
}
var car = new Car();
car.logColor();           // Uncaught TypeError: Cannot set property 'logColor' of undefined

奇怪的是, 如果你在 function 中返回一个字母/数字类型的值, 上面的现象就不会发生了, return 语句将被忽略. 永远不要在你要 newfunction 中返回任何东西. 如果你想创建一个工厂模式, 用函数而不是 new 来创建实例, 当然这只是个建议.

你可以用 Object.create 代替 new 来创建对象

function Car() {}
Car.prototype.color = 'Slivery';
Car.prototype.logColor = function() {
  console.log(this.color);
}
var car = Object.create(Car.prototype);
car.logColor();           // Slivery

但是这样的话就不会调用 function 中定义的过程了

function Car() {
  this.color = "Black";
}
Car.prototype.color = 'Slivery';
Car.prototype.logColor = function() {
  console.log(this.color);
}
var car = Object.create(Car.prototype);
var car2 = new Car();
car.logColor();           // Slivery
car2.logColor();          // Black

因为 Object.create 不会调用 function 中定义的过程, 在创建继承模式的时候这个特性就有用了

function Car() {
  this.color = "Black";
}
Car.prototype.color = 'Slivery';
Car.prototype.logColor = function() {
  console.log(this.color);
}

function Tank() {
  this.logColor(this.color);        // Slivery
  Car.apply(this);
  this.logColor(this.color);        // Black
}
Tank.prototype = Object.create(Car.prototype);
var tank = new Tank();

object this

你可以在对象(object)上的任意函数里面使用 this 指向对象(object)上的其他属性, 这点和用 new 创建的实例不一样

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

注意这里没有用 new/Object.create 也没有调用函数来创建 obj, 但是你也可以像函数实例一样 bind 到一个函数上

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

当你像下面这样用多级 object 时, this 无法回溯到上一层寻找它要的属性, 只有当它们的上一级 object 是同一个时 this 才有效

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

但是你可以不通过 this 而直接引用你想要的值

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

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

除非你手动绑定了一个 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

在可以加 JavaScript 代码的所有属性中, this 指向当前元素

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

override this

不能重写 this 因为它是个关键词

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

无法通过语法检查

eval this

你可以在 eval 中访问 this

function Car() {
  this.color = "Slivery";
  this.logColor = function() {
    eval("console.log(this.color)");
  }
}
var car = new Car();
car.logColor();         // Slivery

这是个安全问题, 只能通过禁止使用 eval 来避免

通过 Function 创建的函数也可以访问 this

function Car() {
  this.color = "Slivery";
  this.logColor = new Function("console.log(this.color)");
}
var car = new Car();
car.logColor();       // Slivery

with this

你可以用 withthis 加到当前的作用域中来读写 this 中的属性

function Car() {
  this.color = "Black",
  this.logColor = function() {
    with(this) {
      color = "Slivery";
      console.log(color);
    }
  }
}
var car = new Car();
car.logColor();         // Slivery
console.log(car.color); // Slivery

许多人认为这样不好因为 with 有歧义问题

jQuery this

HTML DOM 事件处理中的 this 一样, jQuery 中的 this 在很多情况下也指向一个 DOM 元素, 如 event handler$.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>

thisArg this

如果你用 underscore.js/lo-dash, 你大概会知道可以通过函数参数 thisArg(会被当作 this) 来传递一个实例, 比如 _.each 就是这样. ECMAScript 5 之后 JavaScript 中的原生方法也支持 thisArg 比如 forEach. 前面在演示 bind/apply/call 时, 就是用 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);
   // _.each(arr, this.log, this);
}

var thing = new Thing("fruit");
thing.logThings(["apples", "oranges", "strawberries", "bananas"]);
// fruit apples
// fruit oranges
// fruit strawberries
// fruit bananas

这样就不需要写很多嵌套绑定语句, 不需要再使用 self

End

一些编程语言容易学, 比如 Go 的 SPEC 可以在很短时间内读完. 一旦读完 SPEC 就可以理解这门语言, 就不需要太多的担心技巧和陷阱, 更不用说实现细节了

JavaScript 并不是这样易懂的语言, SPEC 不易读懂, 有许多陷阱, 所以有许多人会讨论 The God Parts, 目前返现的最好的文档在 Mozilla Developer Network, 我推荐阅读那篇文档中关于 this 的部分, 并在用 Google 查找 JavaScript 相关的问题时加上 mdn 的前缀, 这样总能查找到最好的文档. 静态代码分析也非常有帮助, 我一般用 jshint

如果有任何问题请在 Twitter 上联系 我bjorntipling

更新: @hicksyfern 在他的博客上有一篇更简短的[some of this](http://tomhicks.github.io/code/2014/08/11/some-of-this.html)

Published: February 08 2015

blog comments powered by Disqus