关于this的解析(一)

1.1 为什么要用this

如果对于有经验的 JavaScript 开发者来说 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, 我是 KYLE 
speak.call( you ); // Hello, 我是 READER 

看不懂这段代码?不用担心!我们很快就会讲解。现在请暂时抛开这些问题,专注于为 什么。 这段代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。 如果不使用 this,那就需要给 identify() 和 speak() 显式传入一个上下文对象。

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, 我是 KYLE 

然而,this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。 随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。当我们介绍对象和原型时,你就会明白函数可以自动引用合适的上下文对象 有多重要。

1.2 误解

1.2.1 指向自身

人们很容易把 this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。

那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函 数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。

JavaScript 的新手开发者通常会认为,既然函数看作一个对象(JavaScript 中的所有函数都 是对象),那就可以在调用函数时存储状态(属性的值)。这是可行的,有些时候也确实有 用,但是在本书即将介绍的许多模式中你会发现,除了函数对象还有许多更合适存储状态 的地方。

不过现在我们先来分析一下这个模式,让大家看到 this 并不像我们所想的那样指向函数 本身。

我们想要记录一下函数 foo 被调用的次数,思考一下下面的代码:

function foo(num) { 
	console.log( "foo: " + num );
	 // 记录 foo 被调用的次数
	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 
 // foo 被调用了多少次? 
 console.log( foo.count ); // 0 -- WTF?

console.log 语句产生了 4 条输出,证明 foo(…) 确实被调用了 4 次,但是 foo.count 仍然 是 0。显然从字面意思来理解 this 是错误的。

执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相 同,困惑随之产生。

遇到这样的问题时,许多开发者并不会深入思考为什么 this 的行为和预期的不一致,也不 会试图回答那些很难解决但却非常重要的问题。他们只会回避这个问题并使用其他方法来 达到目的,比如创建另一个带有 count 属性的对象。

function foo(num) { 
	console.log( "foo: " + num ); 
	// 记录 foo 被调用的次数 
	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 
// foo 被调用了多少次? 
console.log( data.count ); // 4 

从某种角度来说这个方法确实“解决”了问题,但可惜它忽略了真正的问题——无法理解 this 的含义和工作原理——而是返回舒适区,使用了一种更熟悉的技术:词法作用域

另一种方法是强制 this 指向 foo 函数对象:

function foo(num) { 
	console.log( "foo: " + num ); 
	// 记录 foo 被调用的次数 
	// 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
	this.count++; 
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
	if (i > 5) { 
		// 使用 call(..) 可以确保 this 指向函数对象 foo 本身 
		foo.call( foo, i ); 
	} 
}
// foo: 6 
// foo: 7 
// foo: 8 
// foo: 9 

// foo 被调用了多少次? 
console.log( foo.count ); // 4 

这次我们接受了 this,没有回避它。如果你仍然感到困惑的话,不用担心,之后我们会详 细解释具体的原理。

1.2.2 它的作用域

第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它 是正确的,但是在其他情况下它却是错误的。

需要明确的是,this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用 域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部

思考一下下面的代码,它试图(但是没有成功)跨越边界,使用 this 来隐式引用函数的词 法作用域:

function foo() {
	var a = 2;
	this.bar(); 
}
function bar() { 
	console.log( this.a ); 
}
foo(); // ReferenceError: a is not defined 

这段代码中的错误不止一个。虽然这段代码看起来好像是我们故意写出来的例子,但是实 际上它出自一个公共社区中互助论坛的精华代码。这段代码非常完美(同时也令人伤感) 地展示了 this 多么容易误导人。

首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之 后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。

此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让 bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一 个词法作用域内部的东西。

每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

1.3 this到底是什么

排除了一些错误理解之后,我们来看看 this 到底是一种什么样的机制。

之前我们说过 this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到

1.4 小结

对于那些没有投入时间学习 this 机制的 JavaScript 开发者来说,this 的绑定一直是一件非常令人困惑的事。

this 是非常重要的,但是猜测、尝试并出错和盲目地从 Stack Overflow 上复制和粘贴答案并不能让你真正理解 this 的机制。

学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法作用域,你也许被 这样的解释误导过,但其实它们都是错误的。

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

你可能感兴趣的:(js,前端)