JS中的this

一:关于this?

1.为什么要使用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 设计 得更加简洁并且易于复用。

1.2 对于this的误解

1.2.1 指向自身

我们先来看一下下面这段代码,可以发现 this 并不像我们所想的那样指向函数本身。

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 是错误的。

这一段代码实际上在无意间创建了一个全局变量count,它是值是NaN

1.2.1 指向作用域

需要明确的一点是,this在任何情况下都不会指向函数的词法作用域,
看一下下面的这一段代码:

function foo() {
	var a = 2;
	this.bar(); 
}

function bar() { 
	console.log( this.a ); 
}

foo(); // ReferenceError: a is not defined

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

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

1.3 this到底是什么

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

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

1.4 小结

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

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

二:this全面解析

2.1 调用位置

调用位置就是函数在代码中被调用的位置(不是声明的位置)。只有仔细分析这一点,就能明白this到底引用的是什么。

最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

请看接下来的一段代码:

function baz() { 
	// 当前调用栈是:baz 
	// 因此,当前调用位置是全局作用域 
	
	console.log( "baz" ); 
	bar(); // <-- bar 的调用位置 
}

function bar() {
	// 当前调用栈是 baz -> bar 
	// 因此,当前调用位置在 baz 中

	console.log( "bar" ); 
	foo(); // <-- foo 的调用位置
}

function foo() { 
	// 当前调用栈是 baz -> bar -> foo 
	// 因此,当前调用位置在 bar 中 
	console.log( "foo" ); 
}

baz(); // <-- baz 的调用位置

调用栈:为到达当前执行位置所调用的所有函数
调用位置:在当前正在调用的函数的前一个调用中

2.2 绑定规则

那么在函数的执行过程中调用位置如何决定this的绑定对象。

首先:先找到调用位置。
其次:应用四条规则进行判断,属于哪一条,并解释借条规则时它们的优先级是如何排列的。

2.2.1 默认绑定

最常用的函数调用类型:独立函数调用。

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

var a = 2; 

foo(); // 2

当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?因为在本例中,函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。

通过分析调用位置来看看 foo() 是如何调用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则

注意:如果处于严格模式下,那么全局对象将无法使用默认绑定,this将会绑定到undefined.

2.2.2 隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

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

var obj = { 
	a: 2, 
	foo: foo 
};

obj.foo(); // 2

调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。

当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引 用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调 用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a是一样的。

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined上。

如下代码:

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

var obj = { 
	a: 2, 
	foo: foo 
};

var bar = obj.foo; // 函数别名!

var a = "oops, global"; // a 是全局对象的属性 

bar(); // "oops, global"

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

传入回调函数时:

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

function doFoo(fn) {
	 // fn 其实引用的是 foo 
	 
	 fn(); // <-- 调用位置!
 }
var obj = {
	 a: 2, 
	 foo: foo 
};

var a = "oops, global"; // a 是全局对象的属性 

doFoo( obj.foo ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。

如果把函数传入语言内置的函数而不是传入你自己声明的函数,结果也是一样的:

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

var obj = {
	 a: 2, 
	 foo: foo 
};

var a = "oops, global"; // a 是全局对象的属性 

setTimeout( obj.foo, 100 ); // "oops, global"

2.2.3 显式绑定

可以使用函数的 call(…) 和 apply(…) 方法将对象绑定到this,接着在调用函数时候指定这个this.

代码:

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

var obj = {
	 a:2 
};

foo.call( obj ); // 2

通过 foo.call(…),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。

显式绑定仍然无法解决我们之前提出的丢失绑定的问题。

但是可以利用显式绑定的变种解决这个问题

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

var obj = {
	 a:2 
};

var bar = function() {
	 foo.call( obj ); 
};

bar(); // 2 
setTimeout( bar, 100 ); // 2 

// 硬绑定的 bar 不可能再修改它的 this 
bar.call( window ); // 2

2.2.4 new绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

代码:

function foo(a) {
	this.a = a; 
}

var bar = new foo(2); 
console.log( bar.a ); // 2

使用 new 来调用 foo(…) 时,我们会构造一个新对象并把它绑定到 foo(…) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

2.3 优先级

既然已经了解函数调用中this绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。

首先,默认绑定的优先级是四条规则中最低的。

new绑定>显示绑定>隐士绑定>默认绑定

判断this:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()

  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)

  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()

  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo()

2.4 绑定例外

2.4.1 被忽略的this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则:

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

var a = 2; 

foo.call( null ); // 2

2.4.2 间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则。

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

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 }; 

o.foo(); // 3 
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。

2.4 this词法(箭头函数)

箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使用 this 的四种标准规则,

而是根据外层(函数或者全局)作用域来决定 this。

看下面的这段代码:

function foo() { 
	// 返回一个箭头函数
	return (a) => {
		 //this 继承自 foo() 
		 console.log( this.a ); 
		 }; 
}

var obj1 = { 
	a:2 
};

var obj2 = { 
	a:3
};

var bar = foo.call( obj1 ); 
bar.call( obj2 ); // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() { 
	setTimeout(() => { 
		// 这里的 this 在此法上继承自 foo() 
		console.log( this.a ); 
	},100); 
}
var obj = { 
	a:2 
};

foo.call( obj ); // 2

箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值