ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JS之 this

2022-06-20 09:03:30  阅读:107  来源: 互联网

标签:调用 console 函数 绑定 JS var foo


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

标签:调用,console,函数,绑定,JS,var,foo
来源: https://www.cnblogs.com/codexlx/p/16392084.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有