• 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
    
  • 相关阅读:
    在CentOS 8上安装Jitsi Meet
    centos8 安装docker
    [git]error: pack-objects died of signal
    Andorid 11调用系统裁剪
    XCode修改工程名(完美版)
    java分割后台日志
    五分钟搞定WebRTC视频录制
    一分钟教你构建属于你的视频会议SDK
    史上最全的WebRTC服务器技术选型分析
    数据库设计之思考
  • 原文地址:https://www.cnblogs.com/codexlx/p/16392084.html
Copyright © 2020-2023  润新知