你不知道的JS之 this 和对象原型(一)this 是什么
原文:你不知道的js系列
JavaScript 的 this 机制并没有那么复杂
为什么会有 this?
在如何使用 this 之前,我们要搞清楚一个问题,为什么要使用 this。
下面的代码尝试去说明 this 的使用动机:
function identify() { return this.name.toUpperCase(); } function speak() { var greeting = "Hello, I'm " + identify.call( this ); console.log( greeting ); } var me = { name: "Kyle" }; var you = { name: "Reader" }; identify.call( me ); // KYLE identify.call( you ); // READER speak.call( me ); // Hello, I'm KYLE speak.call( you ); // Hello, I'm READER
这段代码使得函数 identify() 和 speak() 可以在多个上下文(me 和 you)对象中重用,不用给每个对象分别创建函数。
如果不用 this,你也可以将上下文对象直接传入函数:
function identify(context) { return context.name.toUpperCase(); } function speak(context) { var greeting = "Hello, I'm " + identify( context ); console.log( greeting ); } identify( you ); // READER speak( me ); // Hello, I'm KYLE
然而 this 机制可以隐式地传递一个对象引用,使得 API 设计得更简洁和更容易复用。
你的使用模式越复杂,你就能更加明白,显式传递一个参数经常比传递 this 上下文还混乱。
困惑
在解释 this 如何工作之前,必须要先摒弃错误的概念。开发者们总是太过依赖 this 的字面意思。
引用自身 Itself
一种普遍的错误是认为 this 指代这个函数自身。
为什么你会想从一个函数内部引用它自己呢,通常的原因是递归,或者事件回调函数在被调用之后解除绑定。
JS 新手会认为将函数作为对象引用可以在函数调用期间存储状态(属性的值)。这确实是可以的但是用处有限,后面会介绍其它模式,除了函数对象本身还有更好的存储状态的地方。
下面的代码会说明,this并不会像我们以为的那样让函数得到对自身的引用:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called this.count++; } foo.count = 0; var i; for (i=0; i<10; i++) { if (i > 5) { foo( i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // how many times was `foo` called? console.log( foo.count ); // 0 -- WTF?
foo.count 还是 0 ,循环确实执行了 4 次,console.log 也确实被调用了 4 次。
foo.count = 0 执行之后,实际上给函数对象 foo 添加了一个属性 count。
但是在函数内部的 this.count 中,this 实际上并不指向这个函数对象,即使这个属性名字是一样的,但属性所在的对象是不同的。
如果 foo 的属性 count 的值没有改变,那么我们改变的究竟是什么。实际上,如果你再深究一下,就会发现,这段代码意外地创建了一个全局变量 count,而且当时会有一个值 NaN(具体看这个系列的第二节)。
很多开发者就会通过别的方式避免这个问题,比如创建另外一个对象储存这个属性 count:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called data.count++; } var data = { count: 0 }; var i; for (i=0; i<10; i++) { if (i > 5) { foo( i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // how many times was `foo` called? console.log( data.count ); // 4
这确实解决了问题,但是很遗憾这忽略了真正的问题——不理解 this 的含义和用法,只是回到熟悉的词法作用域机制。
如果想在一个函数对象内部引用自身,this 是不够的,你需要一个标识符:
function foo() { foo.count = 4; // `foo` refers to itself } setTimeout( function(){ // anonymous function (no name), cannot // refer to itself }, 10 );
在第一个函数中,函数被命名为 foo,这个标识符 foo 就可以用来指代这个函数对象自身。
但在第二段中,回调函数没有名字,所以没办法引用自己。
注:老派的已经被废弃的 arguments.callee 在函数中可以用来指代正在执行的函数对象。这是在匿名函数内部访问函数对象的唯一方式。
当然最好的方式还是避免匿名函数的使用。
另外一种解决办法就是使用 foo 标识符,不使用 this:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called foo.count++; } foo.count = 0; var i; for (i=0; i<10; i++) { if (i > 5) { foo( i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // how many times was `foo` called? console.log( foo.count ); // 4
然而这种方法同样回避了对 this 的理解。
另外一种解决这个问题的方式是,将 this 强制绑定到 foo 这个函数对象上:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called // Note: `this` IS actually `foo` now, based on // how `foo` is called (see below) this.count++; } foo.count = 0; var i; for (i=0; i<10; i++) { if (i > 5) { // using `call(..)`, we ensure the `this` // points at the function object (`foo`) itself foo.call( foo, i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // how many times was `foo` called? console.log( foo.count ); // 4
作用域的引用 Its Scope
第二个常见的关于 this 的错误理解是,this 指向这个函数的作用域。这是一个有点狡猾的问题,因为在某种意义上这种说法是有些正确的,但在另一种意义上,这又是被误导的。
首先,this 并没有指向函数的词法作用域。作用域确实就像是一个包含所有标识符属性的对象,但是这个作用域 “对象” 是无法被代码直接访问的,这是引擎内部实现的。
所以下面的代码是错误的:
function foo() { var a = 2; this.bar(); } function bar() { console.log( this.a ); } foo(); //undefined
你可能觉得这段代码很做作,但这是摘自一些帮助论坛里的真实代码。
首先,这段代码试图通过 this.bar() 引用函数 bar(),能运行起来也是巧合。调用 bar() 最自然的方式就是直接使用标识符引用,去掉前面的 this。
然而,写这段代码的开发者其实是想让 bar() 访问 foo() 内部的变量 a,但 this 不能被用来查询词法作用域的。
this 到底是什么
前面讲到过,this 是在运行时绑定的,它的上下文环境取决于函数调用的条件。this 的绑定和函数声明的位置没有关系,和函数调用的位置有关。
当一个函数被调用时,一个执行上下文被创建。这个上下文记录包含函数调用的位置,函数调用的方式以及传入的参数这些信息。this 的引用就是在这个时候决定的。
在下一节中,会介绍根据一个函数的调用位置确定它执行过程中将如何绑定this。
小结:
- this 既不指代函数本身,也不指代函数的词法作用域。
- this 是在函数调用的时候绑定的,它引用的内容完全取决于函数调用的位置。