你不知道的javaScript——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

  • 这段代码可以在不同的上下文对象(me和you)中重复使用函数identify()和speak(),无需针对每个对象编写不同的版本的函数。this提供了一种优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简介并且易于使用。

对this的错误认知

  • 常见的对this的错误解释有两种。

1. 指向自身

  • 将this理解成指向函数自身。新手开发者认为,既然把函数看作一个对象(JavaScript中的所有函数都是对象),那就可以在调用函数时存储状态(属性的值)。
  • 例如我们想要记录函数foo被调用的次数,代码如下
function foo(num) {
   
	console.log("foo: " + num);

	this.count++;
}

foo.count = 0;

var i;
for(i = 0; i < 10; i++) {
   
	if(i > 5) {
   
		foo(i);
	}
}

// foo被调用了多少次?
console.log(foo.count); // 0 -- 震惊吧
  • console.log产生了4条输出,但foo.count仍然是0,显然从字面意思来理解this是错误的。
  • 执行foo.count = 0时,的确向函数对象foo添加了一个属性count。但是函数内部代码this.count中的this并不是指向那个函数对象,虽然属性名相同,根对象却并不相同。实际上,这段代码在无意中创建了一个全局变量count,它的值为NaN。

2. 它的作用域

  • 第二种常见的误解时,this指向函数的作用域。这种情况下,它有时是正确的,有时是错误的。
  • 需要明确的是,this在任何情况下都不指向函数的词法作用域。
function foo() {
   
	var a = 2;
	this.bar();
}

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

foo(); // ReferenceError: a is not defined
  • 首先,这段代码试图通过this.bar()来引用bar()函数。这样调用成功纯属意外
  • 此外,这段代码还试图使用this联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里的变量a。每当你想要把this和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

this到底是什么

  • this是在运行时进行绑定的,但并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
  • 当一个函数被调用时,会创建一个活动记录(有时候也被称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。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的调用位置
  • 因此,如果你想要分析this的绑定,使用开发者工具得到调用栈,然后找到栈中第二个元素,那就是真正的调用位置。

小结

  • 如何判断一个运行中的函数的this绑定。首先找到这个函数的直接调用位置。之后根据下面四条规则判断this的绑定对象。

由new调用?绑定到新创建的对象。

function foo(a) {
   
	this.a = a;
}
var bar = new foo(2);
console.log(bar.a); //2

由call或者apply(或者bind)调用?绑定到指定的对象。

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

var obj = {
   
	a:2
};
foo.call(obj); // 2

由上下文对象调用?绑定到那个上下文对象

  • 第三方库的许多函数,以及javaScript语言的宿主环境中许多的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind()一样,确保你的回调函数使用指定的this。
function foo(el) {
   
	console.log(el, this.id);
}
var obj = {
   
	id: "awesome"
};

// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome

默认:严格模式下绑定到undefined,否则绑定到全局对象

  • 严格模式如下所示:
function foo() {
   
	"use strict";
	console.log(this.a);
}
var a = 2;
foo(); // TypeError:this is undefined
  • 注意:一些调用可能无意中使用默认绑定规则。如果想“更安全”地忽略this绑定,可以使用DMZ对象,例如var +o = Object.create(null),以保护全局对象。

  • 摘取自《你不知道的JavaScript》