前言
看了这篇文章,麻麻再也不用担心我不理解JS中this的指向问题了。本文以文章目录为顺序,层层递进。耐心的看完,会有很大的收获。
一、this的作用?我们为什么要用this,没它不行吗?
this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的JavaScript开发者也很难说清它到底指向什么。 实际上,JavaScript中this的机制并没有那么先进,但是开发者往往会把理解过程复杂化。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。 this都有一个共同点,它总是返回一个对象。简单说,this就是属性或方法“当前”所在的对象。 从某些角度来说,开发中如果没有this,很多问题也有其他的解决办法。 但是没有this,会让我们编写代码变得非常不放便。
话不多说,举个栗子 看看就知道了,示例代码如下:
var obj = { name: '张三', eating () { console.log(obj.name + '在吃饭') }, studying () { console.log(obj.name + '在学习') }, running () { console.log(obj.name + '在跑步') } } obj.eating() obj.studying() obj.running()
上述示例代码,如果我们修改了变量obj名字为object,之后需要修改的代码有以下两处: 首先,需要修改的代码有方法的调用。 其次,就是在eating、studying、running三个方法中打印语句中的变量名 如果我们的obj对象中不止三个方法呢?如果是一百个方法呢?一个一个改,杀了我吧(裂开)。
如下示例代码,展示了使用this进行优化的效果:
var obj = { name: '张三', eating () { console.log(this.name + '在吃饭') }, studying () { console.log(this.name + '在学习') }, running () { console.log(this.name + '在跑步') } } obj.eating()
优化后的代码不管我们怎么改变变量名称 ,我们需要修改的地方只有一个,那就是方法调用时的对象名称。是不是简便了许多,为编程的效率提升的不止一点哦。
二、this在全局作用域中指向哪里?
其实在大多数情况下,this都是出现在函数中。但也有出现在全局作用域下的情况,我们就来举个栗子说一说。
示例代码如下:
var name = 'code' console.log(this) console.log(this.name) // 输出 code console.log(Window.name) // 输出 code
浏览器环境下输出结果如下图所示:
由图可知,全局作用域下,浏览器环境中:this指向的是全局对象Window。
这里需要补充一下的是,在全局作用域下,node环境中,this指向的是一个空对象{}。
三、同一个函数中this的不同指向
先看代码,再做结论。如下示例代码展示了不同的调用方法,得到的this的不同指向。示例代码如下:
// 定义一个函数 function foo () { console.log(this) } // 调用方式一:直接调用 foo() // this指向:Window // 调用方式二:间接调用 var obj = { name: '张三', fn: foo } obj.fn() // this指向:obj对象 // 调用方式三:通过call/apply调用 foo.call('123') //this指向: String {‘123’}对象
浏览器输出结果如下所示:
三个不同的调用方法,得到了三个不同的this指向。
结论:
1.函数在调用时,JavaScript会默认给this绑定一个值;
2.this的绑定值与定义的位置没有关系;
3.this的绑定和调用方式以及调用的位置没有关系;
4.this是在运行时被绑定的,而不是在定义时。
四、this的四种绑定规则
1、默认绑定
在一个函数体中使用this,当该函数被独立调用时,就会被默认绑定一个对象。
在默认绑定情况下,只要是独立函数调用就是指向Window。
如下展示五个案例进行深入学习,层层深入。示例代码如下:
// 案例一: function foo1 () { console.log(this) } foo1() // 案例二: 三个方法被调用的时候都是独立的,所以this的指向都是Window function one () { console.log(this) } function two () { console.log(this) one() } function three () { console.log(this) two() } three() // 案例三: var obj = { name: '张三', foo3 () { console.log(this) } } var bar3 = obj.foo3 bar3() // Window // 案例四: function foo4 () { console.log(this) } var obj = { name: '张三', fn4: foo4 } var bar4 = obj.fn4 bar4() // Window // 案例五: function foo5 () { function bar () { console.log(this) } return bar } var fn = foo5() fn()
上述代码中五个案例,全部都是独立调用,所以显而易见,输出结果this都是指向Window对象
2、隐式绑定
通过某个对象进行调用,也就是它的调用位置中,是通过某个对象发起的函数调用。
隐式绑定有一个前提条件:
必须在调用的对象内部有一个对函数的引用(比如一个属性)
如果没有这样的引用,在进行调用时,会报找不到该函数的错误
正式通过这个引用,间接的讲this绑定到了这个对象上
如下通过三个案例,进行隐式绑定的详细了解,示例代码如下:
// 案例一:通过隐式绑定将fn函数中的this指向obj对象 function fn () { console.log(this) } var obj = { name: '张三', studying: fn } obj.studying() // {name: '张三', studying: ƒ} // 案例二: var obj2 = { name: '张三', eating () { console.log(this.name + '正在吃饭') }, running () { console.log(obj.name + '正在跑步') } } // 隐式绑定进行调用 obj2.eating() // 张三正在吃饭 obj2.running() // 张三正在跑步 var fn = obj.eating fn() // 独立函数调用 指向window,name为window中的name的值为空 输出结果为: 正在吃饭 // 案例三 var obj3 = { name: 'foo3', foo () { console.log(this) } } var obj4 = { name: 'foo4', bar: obj3.foo } obj4.bar() // {name: 'foo4', bar: ƒ}
上述三个案例中,三个this都被隐式绑定到了调用此方法的对象上。
3、显示绑定 (call/apply/bind )
显示绑定就是指使用js中的原型方法call()、apply()、bind(),对this进行显示绑定。
首先我们先对三个方法的概念下手,如下是参考的相关文档整理的:
①、call()
定义:
call()方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
语法:
fun.call(thisArg, arg1, arg2, ...)
参数:
thisArg:在 fun 函数运行时指定的 this 值。if(thisArg == undefined|null) this = window,if(thisArg == number|boolean|string) this == new Number()|new Boolean()| new String()
arg1, arg2, …: 指定的参数列表
返回值: 使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
②、apply()
定义:
apply()方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。
语法:
func.apply(thisArg, [argsArray])
参数:
thisArg:可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
argsArray: 可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。
返回值: 调用有指定this值和参数的函数的结果
③、bind()
定义:
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。
语法:
function.bind(thisArg[,arg1[,arg2[, ...]]])
参数:
thisArg:调用绑定函数时作为this参数传递给目标函数的值。如果bind函数的参数列表为空,执行作用域的this将被视为新函数的thisArg。
arg1, arg2, …: 当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。
返回值: 返回一个原函数的拷贝,并拥有指定的this值和初始参数。
概念介绍完了,上代码:
// call 与 apply function foo () { console.log(this + 'foo函数被调用了') } var obj = { name: 'obj' } // 调用方法一:直接调用 指向的是全局对象(window) foo() // 调用方法二: call和apply与直接调用不同的在于this的绑定不同 ,可以直接指定this的绑定对象为obj foo.call(obj) //{name: 'obj'} foo.apply(obj) //{name: 'obj'} // bind function bar () { console.log(this) } var newBar = bar.bind('aaa') newBar() // String {'aaa'} 解释一下 newBar()句话 很明显是直接调用应该指向全局对象window 但是bind显示绑定的优先级更高 (优先级会在下一篇文章更新)
4、new绑定
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。包括内置对象函数在内的所有函数都可以用new来调用,这种函数调用称为构造函数调用。
我们通过一个new关键字调用一个函数时(构造器),这个时候this是在调用这个构造器时创建出来的对象。 this 等于 创建出来的对象 ,这个过程就是 new 绑定。
示例代码如下:
function Person (name, sex) { this.name = name this.sex = sex } var p1 = new Person('张三', '男') console.log(p1.name, p1.sex) // 张三 男 // 每次都会创建新对象赋值给this var p2 = new Person('李红', '女') console.log(p2.name, p2.sex) // 李红 女
5、call和apply的区别
根据上面的概念我们可以很明显的看出来,两个方法接收的参数不一样,其实作用基本一致。
// call 与 apply的区别是什么? function sum (num1, num2, num3) { console.log(num1 + num2 + num3) } sum.call('call', 10, 20, 30) // 60 sum.apply('apply', [10, 20, 30]) // 60 // call参数是单个的值,而apply的参数是一个数组,就这点差别