JS之 this

1. this存在哪里?

this在日常开发中给人一种它好像用的不多,但是又好像无处不在的错觉。但是它确实无处不在。

它是一个特殊的关键字,被自动定义在所有函数的作用域中。

2. 为什么要用this?

先说结论:希望在函数可以自动引用合适的上下文对象。

先放不用this的代码:

	function upper(context) {
		return context.name.toUpperCase()
	}

	function speak(context) {
		var greet = "你好,我是" + upper(context)
		console.log(greet);
	}

	var me = {
		name: 'Bob'
	}

	var you = {
		name: "Kyle"
	}

	console.log(upper(me)); // BOB
	console.log(upper(you)); // KYLE
	speak(me) // 你好,我是BOB
	speak(you) // 你好,我是KYLE

上面代码中通过将对象由形参的方式传递给函数,然后在函数内手动获取到此对象形参的属性进行操作。

下面放上使用this的代码:

	function upper() {
		return this.name.toUpperCase()
	}

	function speak() {
		var greet = "你好,我是" + upper.call(this)
		console.log(greet);
	}

	var me = {
		name: 'Bob'
	}

	var you = {
		name: "Kyle"
	}

	console.log(upper.call(me)); // BOB
	console.log(upper.call(you)); // KYLE
	speak.call(me) // 你好,我是BOB
	speak.call(you) // 你好,我是KYLE

两种方法实现的功能一样,但是随着使用模式越来越复杂,显式的传递上下文对象会让代码越来越混乱。但是this提供了一种更为优雅的方式来隐式“传递”一个独享引用,因此可以将API设计的更加简洁且易于复用。

3.对于this的常见误解

3.1 指向自身

我们很容易把this理解成指向函数自身。先看一段示例代码:

	function foo(n) {
		console.log("foo---" + n);
		this.count++
	}

	foo.count = 0

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

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

最终输出结果:

image.png

foo()里的console语句执行了4此,说明foo()确实被调用了4次,但是foo.count仍然是0,这只能说明一个问题:foo()函数里的this.count !== foo.count

那么this.count++到底加到哪里去了?先说结论:它加到了window全局对象上了,且window.count打印出来是NaN

  • 为什么是NaN
    • 因为window.count初始没有这个值,所以是undefined,undefined++就是NaN。
  • 为什么加到了window上?
    • 因为直接调用foo()时,此时的foo()函数所处的上下文是window。(这里先放这不用理解)。

3.2 this指向函数的作用域

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

4. this是什么?

this是在运行时绑定的,并不是在编写时绑定。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(这句话很重要)。

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

5. 调用位置。

上面说了,this的绑定完全取决于函数的调用位置(即函数的调用方法)。

在理解this的绑定过程之前,要先理解调用位置。这里还需要理解另外一个东西:调用栈

调用栈

什么是调用栈:

由于JS只有一个单线程,因此只有一个调用栈,它同一时间只能做一件事,当运行到一个函数,这个函数就会被放到栈顶的位置,当函数return时,就会将这个函数从栈顶弹出。这就是调用栈。可以理解为这样一个模型:

image.png

现在给一个示例来说明调用栈和调用位置:

	function baz() {
		// 当前调用栈:baz
		// 当前调用位置时全局作用域

		console.log('baz')
		bar()
	}

	function bar() {
		// 当前调用栈:baz - bar
		// 当前调用位置在baz中

		console.log('bar')
		foo()
	}

	function foo() {
		// 当前调用栈:baz - bar - foo
		// 当前调用位置在bar中

		console.log('foo')
	}

	baz()

当执行baz()时,baz被压入栈底,此时调用栈中有一个baz;执行到bar()时,bar又被压入栈,此时调用栈由下往上是baz - bar;执行到foo(),foo被压入栈底,此时调用栈是baz - bar - foo。如下图所示:

image.png

后续的话。当foo()执行结束,foo被弹出栈;bar()执行结束,bar被弹出栈;baz()执行结束,baz也被弹出栈。到此调用栈清空。

接下来可以看调用位置是如何决定this的绑定对象的。

6.this的4种绑定规则

6.1 默认绑定

默认绑定的前提是:独立函数调用。当其他规则无法应用时,可以把这个规则当作默认规则。

先见一段代码:

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

	var a = 2

	foo() // 2

声明在全局作用域的变量就是全局对象的一个同名属性。就比如var a = 2,就好像全局对象多了一个值为2的属性a全局对象 : { a: 2 }

这里当调用foo()时,this.a被解析成了全局变量a。因为这里函数调用时应用了this的默认绑定因此this指向了全局对象。且这里foo()函数是直接调用的,并没有像xxx.foo()这样通过xxx.foo()调用,因此只能使用默认绑定,无法应用其他规则。

严格模式下的特殊情况

如果使用了严格模式,那么全局对象将无法使用默认绑定,这时候的this会被绑定到undefined。

6.2 隐式绑定

先看如下代码:

	function foo() {
		console.log(this.a);
	}
	var obj = {
		a: 2,
		foo: foo
	};
	obj.foo(); // 2

上面代码中,foo作为obj对象的引用,当foo()被调用(obj.foo())时,它的落脚点指向obj对象。当函数引用有上下文对象时,隐式绑定规则就会把函数调用中的this绑定到这个上下文对象。这时候的this.aobj.a是一样的。

需要注意的是:对象属性引用链中只有最后一层会影响调用位置。示例:

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

	var obj1 = {
		foo: foo,
		a: 3
	}

	var obj2 = {
		obj1: obj1,
		a: 4
	}

	obj2.obj1.foo() // 3

需要注意一件事:函数也属于引用类型,要防止因引用的是函数地址问题而导致的隐式丢失

小结:隐式绑定时,必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。

6.3 显式绑定(使用call、apply、bind)

通过callapplybind方法将某个对象绑定到this上,接着在调用函数时指定这个this。因为可以直接指定this的绑定对象,因此称为显式绑定。

先看代码:

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

	var obj1 = {
		a: 3
	}


	foo.call(obj1) // 3

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

这两个函数的功能完全一样,在没有参数时的使用方法完全一样,第一个参数就是需要绑定this的对象。但是如果需要向foo()传递参数时,就有点些许的差别:call()后面的参数是一个参数列表:foo.call(obj, param1, param2, ..., paramN);apply()的第二个参数是一个参数数组:foo.apply(obj, [param1, param2, ..., paramN])。

注意:如果传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..)).。

但是这里如果要调用foo()方法,需要每次都调用foo.call()方法。这时候就可以通过bind()实现重复使用:

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

	var obj1 = {
		a: 3
	}

	var b = foo.bind(obj1)

	b() // 3

通过bind(),创建了一个新函数b,它会把参数设置为this的上下文并调用原始函数。

6.4 new绑定

JS中的构造函数,本质上就是一个普通函数,只不过它是被new操作符调用的。

所有的内置对象函数(Number(..)等)都可以用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上。

7. 判断this

看完了上面4种this绑定规则,可以按照下面顺序来判断函数在某个调用位置应用的是哪条规则。

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

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

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

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

8. 箭头函数中的this。

箭头函数不适用于上面的this的4种绑定规则,而是根据外层(函数或全局)作用域来决定的this。

先看代码:

	function foo() {
		return (a) => {
			console.log(this.a); // this继承自foo()
		}
	}

	var obj1 = {
		a: 2
	}

	var obj2 = {
		a: 3
	}

	var bar = foo.call(obj1)

	bar.call(obj2) // 2

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

ES6之前,通过self = this的方法一样确保函数的this绑定到指定对象。

	function foo() {
		var self = this;
		setTimeout(function () {
			console.log(self.a);
		}, 100);
	}
	var obj = { a: 2 }; 
	
	foo.call(obj); // 2
posted @ 2022-06-20 08:46  俄罗斯方块  阅读(22)  评论(0编辑  收藏  举报