JavaScript中this是一种什么机制?又该如何的避免的二个坑?

这篇文章我们来看看JavaScript中 this 到底是一种什么样的机制。我们知道 this 是在运行时进行绑定的, 并不是在编写时绑定, 它的上下文取决于函数调用时的各种条件。 this 的绑定和函数声明的位置没有任何关系, 只取决于函数的调用方式。当一个函数被调用时, 会创建一个活动记录(有时候也称为执行上下文)。 这个记录会包含函数在哪里被调用(调用栈)、 函数的调用方法、 传入的参数等信息。 this 就是记录的其中一个属性, 会在函数执行的过程中用到。

JavaScript中this是一种什么机制?又该如何的避免的二个坑?

平时我们在使用 this 时,经常会对 this 产生一些误解,主要表现为:

指向自身

人们很容易把 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 ); //

从某种角度来说这个方法确实“解决” 了问题, 但可惜它忽略了真正的问题——无法理解this 的含义和工作原理——而是返回舒适区, 使用了一种更熟悉的技术: 词法作用域。如果要从函数对象内部引用它自身, 那只使用 this 是不够的。 一般来说你需要通过一个指向函数对象的词法标识符(变量) 来引用它。

思考一下下面这两个函数:

function foo() {

foo.count = 4; // foo 指向它自身

}

setTimeout( function(){

// 匿名(没有名字的) 函数无法指向自身

}, 10 );

第一个函数被称为具名函数, 在它内部可以使用 foo 来引用自身。但是在第二个例子中, 传入 setTimeout(..) 的回调函数没有名称标识符(这种函数被称为匿名函数), 因此无法从函数内部引用自身。所以, 对于我们的例子来说, 另一种解决方法是使用 foo 标识符替代 this 来引用函数对象:

function foo(num) {

console.log( "foo: " + num );

// 记录 foo 被调用的次数

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

// foo 被调用了多少次?

console.log( foo.count ); // 4

然而, 这种方法同样回避了 this 的问题, 并且完全依赖于变量 foo 的词法作用域。另一种方法是强制 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, 没有回避它。

this 的作用域

第二种常见的误解是, 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 和词法作用域的查找混合使用时, 一定要提醒自己, 这是无法实现的。

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin
avatar

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: