js中的this,你真的理解了吗?——《你不知道的js 上》读书笔记(五)

1 了解this

1.1 this是什么?

this是js 中的一个关键字,也叫调用上下文,会自动定义在所有函数的作用域中,他不是一个变量,所以this不能通过赋值语句自定义,即this不能被左查询,只能通过this获取绑定的对象。

this和哪个对象进行绑定完全属于内部操作,绑定的结果取决于this的调用位置或者说调用方式,而不是函数声明所处的位置。this在运行时绑定,这一点this有点像动态作用域,而之前探讨的函数作用域属于静态作用域。

当一个函数被调用的时候,会创建一个活动记录,有时候也称为执行上下文。这个记录会包含函数在哪里调用(调用栈)、函数的调用方法、传入参数等信息。

可以这么说,this指向的是当前对象,谁调用就指向谁。

一句话,我们通过this,可以获取一个调用者的对象!

1.2 为啥要用this获取?

this可以给我们提供一个调用者的对象,那么问题来了,我们想用这个对象,为啥要通过this来获取,别的方式拿不到吗?this获取有啥好处呢?

首先第一个问题:别的方式拿不到吗?我们想想别的获取方式:通过参数传递进来或者通过作用域链从外层获取!是,的确有很多方式!但是这种方式必须我们显式的把对象的引用传递进来,而this,可以隐式的,悄悄地传递!不用通过参数、不用通过变量维护就可以传递进来!!

第二个问题,好处,选用理由!用官方的语言,好几本书上都看到这样的描述:如果使用显示传递调用上下文,会让代码变得混乱且难以维护,this提供了一种优雅的方式避免了这种问题,使得API的设计更加的简洁和易于复用。

如果你了解对象和原型,你会知道自动的引用合适的上下文有多重要。

一句话,this可以自动(隐式)引用到合适的上下文,其他方式需要手动(显示)传递。

看看下面的代码!虽然代码看起来好像改动不是很大,但这只是一个小小的数据返回,如果我们的函数封装了大量的业务逻辑呢?显示传递的参数是不是每次也要维护呢?

//使用this隐式传递对象引用
function getName(){
return this.name
}
//使用参数显示传递对象引用
function getName(context){
return context.name
}

1.3 关于this的一些误解

误以为this指向自身
function foo(){
this.count++;
}
foo.count = 0
foo();
alert(foo.count)//0

上面这段代码如果按照this指向自身的说法,应该输出1,但是事实上却输出了0。

什么原因呢?

由于this并不是指向了自身的foo,因此this.count并不是这种想法所预期的foo.count,所以foo.count并没有改变!

那this指向了哪里了呢,这就是我们这篇文章的重点!接着往下看,小小的透露一下,这里的this指向的的window,所以window.count 实际上创建了一个全局的变量,并且执行完后加加操作之后,变成了NAN。当然啦,这个并不是这篇文章的重点,想知道为什么可以看看之前的文章。

如果你觉得和你的预期不符,然后开始采用上文的两种显示传递(参数,外层变量),这实际上是跳过了this的问题,采用了词法作用域的方式,忽略了真正的问题所在!

所以那些想要通过this调用函数自身的想法,是不正确的!如果你想调用函数自身,可以通过指向函数自身的词法标识符来引用他,也就是函数名!当然匿名函数就没有咯。也可以用arguments.callee来指向函数自身,但是这种方式几乎要废弃了呢!所以我们使用foo标识符代替this,也可以解决上边的问题呢。

当然,了解this的人可能会知道,可以强制绑定this达到效果。

一句话,this并不是指向函数本身的。

误以为this指向函数作用域

第二种误解是this指向函数的作用域,我们应该明确一点,this 在任何情况下都不指向函数的词法作用域。词法作用域对象在js引擎的内部,尽管可见的标识符是他的属性,但我们依然无法通过代码访问作用域对象本身。

上代码!

function fun1 (){
	var a =1
	this.fun2();
}
function fun2 (){
	alert(this.a)
}
fun1()

上面这段代码的错误不止一个!

1、试图通过this.fun2 调用fun2函数,这是不会成功的!可以直接省略!之后解释原因。
2、试图通过this.a访问fun1 的词法作用域,相当于联通1和2 的词法作用域。无法实现

一句话,你不能使用this来引用一个词法作用域内部的东西。

2 调用位置

上文中我们提到,this和哪个对象进行绑定完全属于内部操作,绑定的结果取决于this的调用位置或者说调用方式,那么我们首先理解一下调用位置或者说函数的调用方式。

寻找调用位置就是寻找函数被调用的位置,但是做起来有时候没那么简单,因为有些编程模式可能会隐藏真正的调用位置。

分析调用位置的关键在于分析函数的调用栈,我们关心的调用位置就在当前函数的前一个调用栈中。

上代码,告诉你什么是调用位置和调用栈!

function fun1 (){
	//当前调用栈fun1
	alert('fun1');
	fun2();//2、fun2的调用位置:fun1中,调用栈:fun1
	
}
function fun2 (){
	//当前调用栈fun1>fun2
	alert('fun2');
	fun3();//3、fun3的调用位置:fun2中,调用栈:fun1>fun2
}
function fun3 (){
	//当前调用栈fun1>fun2>fun3
	alert('fun3');
	fun4();//4、fun4的调用位置:fun3中,调用栈:fun1>fun2>fun3 
}
function fun4 (){
	//当前调用栈fun1>fun2>fun3>fun4
	alert('fun4');
}
fun1();  //1、fun1的调用位置:全局作用域 调用栈:window

看到调用位置了吗,接下来我们来看看调用位置如何决定this的绑定对象。

3 绑定规则

我们要首先根据上文找到调用位置,然后再根据规则判断是四种绑定的哪一种,从而判定它绑定的是哪个对象!

3.1 默认绑定

这条规则指的是函数调用类型:独立调用函数!

this 指向全局变量window!

我们也可以把它看作是无法应用其他规则时的默认规则。

其实在我们介绍调用位置时,里边的所有函数调用的this都是默认绑定,他们的this都会指向全局的window。

注意!这个规则只是适用在非严格模式下,严格模式下,因为全局对象无法使用默认绑定,this指向undefined。严格模式下的与函数的调用位置无关。

通常,严格与非严格模式不能混用的哈。

3.2 隐式绑定

隐式绑定的规则是使用了上下文对象调用。

this 指向这个上下文对象!

上代码!

function fun1(){
	alert(this.a)
}
var obj = {a:1,foo:fun1};
obj.foo() //foo的调用位置  输出1

我们发现foo的调用方式是通过obj对象的属性引用来调用的!他不是普通的调用方式。也就是说该函数使用obj上下文来引用的。

此时隐式绑定规则将this绑定给了obj对象,因此this.a 就是obj.a 。

这里也有一个注意点,就是对象属性引用链中,只有最后一层会影响调用位置。

再次上代码!

function fun1(){
	alert(this.a)
}
var obj1 = {a:1,obj2:obj2};
var obj2 = {a:2,foo:fun1}
obj1.obj2.foo() //foo的调用位置 输出2

此时this是绑定到了obj2而不会绑定到obj1哦。

隐式绑定还会存在一个问题!就是this绑定可能会在传递的过程中丢失!!其实这不算是一个问题,可以说是另一个需要注意的地方。他是符合逻辑的。

那就是我们把隐式绑定的函数引用赋值给另一个变量或者说作为参数传递时,this的隐式绑定会消失,执行时按照新的变量的调用方式来确定。

又来上代码!

function fun1(){
	alert(this.a)
}
var a = 'out'
var obj = {a:2,foo:fun1}
var fun2 = obj.foo;
fun2()  //输出out
function fun3(fun){
	fun()
} 
fun3(obj.foo)//输出out

在上边的代码中,fun2以一种别名的形式来执行,尽管赋值时传给他的是一个对象的函数引用,是有上下文对象的,但是它执行的时候,依然是一个不带任何修饰的独立函数的运行方式,因此执行的是默认绑定!

其实从赋值的角度来说,我拿到的是函数的地址,这个函数本身和obj一点关系都没有。只不过通过obj来获得的地址。我拿到地址就可以执行。这一部分已经和obj没有关系了。

作为函数参数其实也是赋值。所以结果一样的。

这时this绑定到了window。输出了out。

无论哪种方式,this的改变都是意想不到的,实际上我们无法控制回调函数的执行方式。但是我们可以通过固定this来解决这个问题。

其实有一些工具在监听事件中会把回调函数的this强制绑定到dom元素上,从而控制this绑定的对象。

3.3 显式绑定

隐式绑定需要一个对象的属性作为函数的引用,然后通过对象的属性访问函数。如果我们就是不想在对象中添加属性还要实现绑定怎么办呢?

js给我们提供了新的方法!call()和apply()

this 绑定在你手动传入的对象上,也就是他们的参数来控制!

这种方式我们可以通过参数直接指定this的绑定对象,所以称之为显示绑定!

上代码!

function arr (){
	console.log(this.a)
}
var obj = {a:2}
arr.call(obj);//输出2

又有注意点啦,如果这里传入了一个原始值,会绑定他的包装对象!

apply 和 call 只是参数不同。对this而言处理一样!!

可惜,显示绑定仍然没有解决this丢失的问题!

但是显示绑定的一个变种可以解决!

上代码!

function arr (){
	console.log(this.a)
}
var obj = {a:2}
function fun (){
arr.call(obj);
}

fun();//输出2
setTimeout(fun,100)//试图测试通过传参丢失this,但依然输出2

fun.call(window)//试图修改this,但依然输出2

我们通过给this包裹了一层函数,达到了无法改变this绑定。

因为无论如何调用函数,最后都是会走函数arr的显示绑定调用,也算是利用了this机制。

这种绑定是一种显式的强制绑定,所以我们称之为硬绑定!

硬绑定的典型场景就是创建一个包裹函数!

其实js提供了这样的方式!bind函数

他就是把this设置成参数的对象,然后调用原始函数!

用法:

function arr (b){
	console.log(this.a,b)
}
var obj = {a:2}
arr.bind(obj,2);//硬绑定,第一个参数为this,其他参数跟在后边就好

其实有很多第三方库包括js都提供了可选的参数context来指定this。

其实这些函数包括硬绑定都是实现显示绑定来实现的!!

3.4 new绑定

new绑定的规则是使用new 关键字调用函数!

new绑定的this会绑定在一个新的对象上!

如果熟悉一些面向对象的语言,会发现new这个关键字通常用来调用类中的构造函数。创建一个对象的实例。

但是在js 中,用new关键字调用的函数他不属于任何一个类,也不会进行实例化。他甚至可以说不是特殊的函数类型,他只是被new调用了的普通函数而已!

可以了解下使用new来调用函数时具体会发生的事情:js中new调用函数

我们现在只要知道,使用new来调用函数会构建一个新对象绑定到函数的this上。

上代码:

function foo(a){
	this.a = a;
}
var bar = new foo(2)
alert(bar.a);//输出2 

使用new来调用foo时,会构造一个新对象并把它绑定到foo调用中的this上。

4 优先级

规则出来后,其实还有个问题。

同时运用了多种规则,以谁的为准!!

这就涉及到了优先级的问题。

4.1 优先级顺序

上干货:

new绑定>显示绑定>隐式绑定>默认绑定

我们先来分析下,默认绑定之所以叫默认绑定,自然优先级最低!所以我们先考虑其他三种情况!

4.2 优先级验证与分析

显示绑定和隐式绑定

显然在下面的代码中,最后两行:显式+隐式=>输出显式绑定的结果,不再是隐式绑定的结果,即显式绑定优先级高于隐式绑定

function fun1(){
	alert(this.a)
}
var obj1 = {a:1,foo:fun1};
var obj2 = {a:2,foo:fun1}
//隐式
obj1.foo() //输出1
obj2.foo() //输出2

//显式+隐式=>输出显式,即显式绑定优先级高
obj1.foo.call(obj2); //输出2
obj2.foo.call(obj1); //输出1
隐式绑定和new绑定

上代码:

function fun1(a){
	this.a= a;
}
var obj1 = {foo:fun1};

//隐式
obj1.foo(1); 
alert(obj1.a); //输出1


//new绑定+隐式=>new
var obj2 = new obj1.foo(2);
alert(obj2.a)//输出2
alert(obj1.a)//输出1

在上面的代码中我们总结new绑定比隐式绑定更高!

那new和显式绑定那个优先级更高呢??

继续往下看!

new绑定和显式绑定

new和call/apply无法一起使用,因此不能直接测试,但是我们可以用bind硬绑定和new结合来测试他们的优先级!

硬绑定其实是给他一个包裹函数,这个函数会忽略你当前this的绑定,并把this绑定到我们提供的对象上。

上代码!

function fun1(a){
	this.a= a;
}
var obj1 ={}

//硬绑定
var fun2 = fun1.bind(obj1,1); 
alert(obj1.a); //输出1


//new绑定+隐式=>new
var obj2 = new fun2(2);
alert(obj2.a)//输出2
alert(obj1.a)//输出1

以上代码中,new绑定修改了已经绑定在fun2上的obj1,因此new绑定比硬绑定优先级更高。

4.3 教你判断this的绑定

step1:看是不是new绑定,如果是,this绑定到新的对象,如果不是,走step2

step2:看函数是否通过apply call bind调用,如果是,this绑定到指定的对象。如果不是,走step3

step3:看函数是否存在隐式绑定,如果是,this绑定到上下文对象。如果不是,走step4

step4:使用默认绑定,非严格模式下将this绑定到全局window,严格模式下将this绑定到undefined

5 绑定例外

规则,总有例外!

在一些场景下,this的绑定也会出人意料,你以为是其他绑定,但是实际上应用了默认绑定。

5.1 例外1:需要被忽略的this

如果把null undefined 没有包装类的值传入显示绑定的函数(call、bind、apply),实际上会指向默认的绑定。

如果不是异常的话,还有什么情况我们需要手动传入一个null值呢!

其实我们通常用apply来展开一个数组,或者说用bind函数实现柯里化。这个时候,我们不关心this绑定到什么地方,于是我们传一个null。

尽管现在已经有了…操作符可以代替apply展开数组,但是依然没有柯里化的实现。仍然需要bind。

当然如果总是这样用,也会造成难以分析和追踪的bug。

这时候,又出现了另一种解决方式:更安全的this!

这种方式是我们创建一个特殊的对象,使得这个对象不会对程序产生任何的副作用。

可以创建一个空的非委托的对象!可以用var nullobj = Object.create(null)生成,他比{}更加空,因为没有Object.prototype这个委托。

我们在不关心this的指向时就把这个对象传进去。

这时,this的操作就不会影响其他!

5.2 例外2:无意识的间接引用

其实严格来说这不算是例外,就是我们应该注意的点。

我们可能有意无意的创建创建一个函数的间接引用,从而改变了函数的this,使用了默认绑定的规则。

这个在上文我们其实讲到了。

5.3 例外3:软绑定

我们发现硬绑定的灵活度太低了,应绑定之后就不能改变this的指向了。

隐式和显式绑定都无法修改this。

于是我们想到了软绑定的方式!!

他的实现方式是给默认绑定实现一个全局变量或者undefined之外的值,

可以保留隐式和显式绑定修改this的能力。

他的大体逻辑是:

先检查this是否是undefined 或者 window

如果是,就把this绑定到指定的对象

如果不是,不修改this的绑定。

这样就可以再次用其他规则来修改this的绑定。

5.4 例外4:箭头函数

之前介绍的四条规则适用于正常的函数,在ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数!

箭头函数的根据外层的作用域来决定this。外层作用域中的this绑定在哪里,箭头函数内部的this就绑定在哪里。

箭头函数的this无法被修改。以上哪种绑定方式都不行!

箭头函数可以像bind一样确保函数的this绑定到了指定的对象而不被修改。

此外,他的重要性还体现在他用更常见的词法作用域取代了this机制。

词法作用域风格和this机制风格的代码我们可以根据自己的喜好选择一种。

终于写完了呢~

下边真的可以不看 -----

想学习一些前端的书籍吗,我都帮你整理好啦!评论打出你想读的书,给你最全的笔记干货

超级全的前端知识,面试必备、系统复习必备哟哟哟

有想法评论提出哈,欢迎交流,小编也是渣渣一枚呢~一起进步呗

这次真的可以不看 -----

点个收藏呗,要不赞一个呗,小编手都敲累了,但还是持续加更呢~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值