JavaScript高级笔记-coderwhy版本(二)https://developer.aliyun.com/article/1469638
JS中函数的this指向
为什么需要this?
- 在常见的编程语言中,几乎都有this这个关键字(Objective-C中使用的是self),但是JavaScript中的this和常见的面向对象语言中的this不太一样:
- 常见面向对象的编程中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在类的方法中
- 也就是你需要一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象
- 但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义
- 我们来编写一个obj的对象,看有this跟没有this的区别
this的作用
使用this
var obj = { name:"小余", eacting:function(){ console.log(this.name + "在吃东西"); }, runing:function(){ console.log(this.name + "在跑步"); }, studying:function(){ console.log(this.name + "在学习"); } } obj.eacting() obj.runing() obj.studying() //小余在吃东西 //小余在跑步 //小余在学习
不使用this的弊端
不使用this,一样是可以打印出来的,从某些角度来说,开发中没有this,很多问题我们也是有解决方案的
但是如果使用this的话,我们就不需要修改对象内部的代码了
var obj = { name:"小余", eacting:function(){ console.log(obj.name + "在吃东西"); }, runing:function(){ console.log(obj.name + "在跑步"); }, studying:function(){ console.log(obj.name + "在学习"); } } obj.eacting() obj.runing() obj.studying() //小余在吃东西 //小余在跑步 //小余在学习
但是这种解决方案是有弊端的,当我们要使用多个对象的时候,对象里面使用到的名字还得一个个替换掉,例如obj.name就得替换成xiaoman.name
所以没有this会让我们编写代码非常不方便
var obj = { name:"小余", eacting:function(){ console.log(obj.name + "在吃东西"); }, runing:function(){ console.log(obj.name + "在跑步"); }, studying:function(){ console.log(obj.name + "在学习"); } } var xiaoman = { name:"小满", eacting:function(){ console.log(xiaoman.name + "在吃东西"); }, runing:function(){ console.log(xiaoman.name + "在跑步"); }, studying:function(){ console.log(xiaoman.name + "在学习"); } } obj.eacting() obj.runing() obj.studying()
this在全局作用域指向什么
全局中的this是非常特殊的,因为大多数情况下this都是出现在函数中的
- this在全局作用域下
- 浏览器:window
- Node环境:{}空对象
- 但是,开发中很少直接在全局作用域下去使用this,通常都是在函数中使用
- 所有的函数在被调用时,都会创建一个执行上下文
- 这个上下文中记录着函数的调用栈、AO对象等
- this也是其中一条记录
- this是动态绑定的。动态绑定就是等到我们函数即将执行的时候才会确定绑定上去,而不是解析的时候确定的
- 有着比较多的绑定规则,在不同规则下的绑定情况都不大一样
//打印出来的是两个一模一样的window console.log(this); console.log(window);
node环境下为什么是空对象
文件在要被node执行的时候,我们的文件会被node当作一个模块module -> 加载 ->编译 -> 将所有代码放在一个函数里面 -> 执行这个函数,执行了一个apply
- function foo(){xxx},执行的时候我们不使用foo(),而是foo.apply("小余"),则"小余"会替代掉xxx的内容
同一个函数的this的不同
- this指向什么,跟函数所处的位置是没有关系的
- 跟函数被调用的方式有关系
我们先来看一个让人困惑的问题:
- 定义一个函数,我们采用三种不同的方式对他进行调用,它产生了三种不同的结果
这个的案例可以给我们什么样的启示:
- 函数在调用时,JavaScript会默认给this绑定一个值
- this的绑定和定义的位置(编写的位置)没有关系
- this的绑定和调用方式以及调用的位置有关系
- this是在运行时被绑定的
function foo(){ console.log(this); } //1.直接调用这个函数 foo() //2.创建一个对象,对象中的函数指向foo var obj = { name:"小余", foo:foo } obj.foo() //3.apply调用 foo.apply("XiaoYu")
结果如下:
05_this的绑定规则、优先级和面试
this到底是怎么样的绑定规则
- 绑定一:默认绑定
- 绑定二:隐式绑定
- 绑定三:显示绑定
- 绑定四:new绑定
规则1:默认绑定
- 什么情况下使用默认绑定呢?独立函数调用
- 独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用
- 这种情况下,this指向的就是window
//案例1 //函数在被调用的时候,没有被绑定在任何的对象上面,也没有使用apply等方式调用 function foo(){ console.log(this); } foo()
另外的一种情况,这种情况下,函数也是独立调用的。不是XXX.foo()之类的调用也是独立调用
//案例2 function foo1(){ console.log("foo1",this); } function foo2(){ console.log("foo2",this); foo1() } function foo3(){ console.log("foo3",this); foo2() } foo3()
结果如下:
要注意,案例3是定义的时候有绑定到对象上面,但是当他在执行的时候,我们执行的是fn,根据之前学的内存里的执行过程,我们知道fn此时执行的就是obj里面的function本身,那fn是独立调用的其实就证明了函数也是独立调用的,那答案就应该指向window
this指向什么,跟函数所处的位置是没有关系的,跟调用的位置才有关系
//案例3 var obj = { name:"小余", foo:function(){ console.log(this); } } var fn = obj.foo fn() //答案是指向window,而不是obj
跟案例3只有一点点的小变化,只是将foo移到了obj外面,然后在obj内部进行引用了,本质上没有变化
//案例四 function foo(){ console.log(this); } var obj = { name:"小余", foo:foo } var bar = obj.foo bar() //window
之前用过的非常熟悉的案例
//案例5 function foo(){ function bar(){ console.log(this); } return bar } var fn = foo() fn() //这个时候fn函数调用时返回window,非常熟悉的,fn调用的时foo函数里面的bar函数,并没有扯到foo函数上,属于独立调用。然后就是闭包调用必然指向window这种结论是错误的,换种调用方式,就会发生改变了 var obj = { name:"大余", age:fn } obj.age()//隐式绑定 //这种调用方式,js引擎会将obj绑定到我们age的函数内部 //看,此时外面又创建了一个obj对象,里面的age指向就是我们刚刚认定的闭包的fn或者说bar函数,调用顺序是age -> fn -> bar,但此时我们再调用age的时候,返回的结果不再是window了,因为我们输出前的那一刻的调用方式已经发生了变化,此时不再是函数独立调用
规则2:隐式绑定
- 另外一种比较常见的调用方式是通过某个对象进行调用的
- 也就是它的调用位置中,是通过某个对象发起的函数调用
//案例1 function foo(){ console.log(this); } var obj = { name:"狗洛", foo:foo } obj.foo()
返回结果:
我们通过这种在一开始就演示过的代码中可以看到,obj里面的eacting跟running函数的this是可以指向函数的父级作用域的,也就是obj函数
- 因为我们在调用的时候,是通过obj.eating的方式,将obj绑定到eating里面,所以this指向会指到obj上面
- object对象会被js引擎绑定到fn函数中的this里面
//案例2 var obj = { name:"小余", eating:function(){ console.log(this.name + "在吃东西"); }, running:function(){ console.log(this.name + "在跑步"); } } obj.eating() obj.running()
如果我们将obj跟eating的绑定关系解除掉,再调用eating函数的时候,他的this的指向就会出现问题
var fn = obj.eating fn() obj.running()
吃东西的this指向出现了问题,this.name的结果出不来了,因为我们在调用的时候已经将obj跟eating函数的关系给去除掉了,obj没有绑定到eating里面了,所以就指向不到了obj里面的内容了
通过案例3,我们调用obj2中的bar属性,obj2.bar属性调用obj1中的foo函数。
//案例3 var obj1 = { name:"obj1", foo:function(){ console.log(this); } } var obj2 = { name:"obj2", bar:obj1.foo } obj2.bar() //node环境返回结果:{ name: 'obj2', bar: [Function: foo] }
控制台打印结果:
那此时这个this是绑定到了谁身上,我们通过结果可以看到是obj2的身上,首先我们bar是调用到了obj1中的foo函数身上,但是最后我们执行的时候,是通过obj2来进行执行的,所以obj2就被绑定到了foo函数里面去了,所以此时foo函数控制台打印this的结果才会是obj2里的内容
规则3:显示绑定
- 隐式绑定有一个前提条件:
- 必须在调用的对象内部有一个对函数的引用(比如一个属性);
- 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
- 正是这个引用,间接的将this绑定到了这个对象上
通俗的说就是:以上面obj1、obj2的例子来说,我们obj2如果想要调用obj1里的函数的话,我们就得想办法把obj1里的这个函数放到obj2里的属性里面,然后使用obj2对bar进行一个引用,然后我们才能用obj2.bar进行一个调用
- 如果我们不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?
- JavaScript所有的函数都可以使用call和apply方法(这个和Prototype有关)
- 它们的区别是:第一个参数是相同的,后面的参数apply为数组,call为参数列表
- 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的
- 在调用这个函数时,会将this绑定到这个传入的对象上
apply、call、bind的使用
call函数
函数上面有call方法,所以当我们使用foo.call()的时候,他也会去帮我们调用函数。JavaScript内部已经帮我们实现了一个call函数了
function foo(){ console.log("函数被调用了"); } foo() foo.call() //调用的结果是一样的 // 函数被调用了 // 函数被调用了
apply函数
跟call函数同理的,函数上同样有apply函数,一样是JavaScript内部替我们实现的
function foo(){ console.log("函数被调用了"); } foo() foo.apply() // 函数被调用了 // 函数被调用了
call函数与apply函数的区别
- 它们两者传参的方式不大一样
- call是依次传的
- apply是以数组的形式传的
- call和apply在执行函数时,是可以明确的绑定this,这个绑定规则称之为显示绑定
function sum(num1,num2){ console.log(num1+num2,this) } sum.call("call",20,30)//后面的参数是以逗号来做一个分割的,依次挨个传就行 sum.apply("apply",[20,30])//后面的参数是以数组存在的 //50 [String: 'call'] //50 [String: 'apply']
直接调用与call、apply的不同
- 如果说
foo()
直接调用跟call、apply的结果是一样的,为什么不全部使用直接调用呢?
- 首先,它们的this绑定是不一样的
- foo()直接调用,指向的是全局对象(window)
- call、apply可以手动指定我们所指向的this是谁,很多时候我们使用这两个的目的也就是这个
function foo(){ console.log("函数被调用了",this); } var obj = { name:"班花姐姐" } foo() foo.apply("小余") foo.call(obj)
返回结果如下:
显示绑定 -- bind
当我们要重复使用多次绑定的时候,反复调用call或者apply,往里面填写重复的参数的时候,就会显得比较累赘,这个时候我们就可以使用bind函数来替代,bind函数是会返回一个值的,这个时候我们就可以声明一个函数(这个函数也会重新开辟一个堆空间来进行存放的)来接收他,然后直接调用这个声明的函数就可以了
- 然后我们可以看到,我们调用newFoo函数的时候是独立调用的,这个时候应该是指向window才对,但是我们已经用bind将newFoo的指向明确固化到"小余"上面了,这个时候规则就会冲突,显示绑定bind函数的优先级高于默认绑定
function foo(){ console.log(this) } // foo.call("小余") // foo.call("小余") // foo.call("小余") // foo.call("小余") // foo.call("小余") //默认绑定和现实绑定bind冲突 var newFoo = foo.bind("小余") newFoo()//是不是比每次都写foo.call("小余")方便一些?
我们将foo.bind赋值给了newFoo,又声明了bar来接收了foo函数,我们的目的是为了对比它们的this指向问题
- 第一个对比的是bar函数跟foo函数,很明显,是直接赋值的关系,它们是一样的(包括了this也指向一样的地方),返回true
- 第二个对比的是newFoo函数和foo函数,这里它们的不同在于newFoo接收的并不是foo函数本身,唯一的变量是被bind修改了this指向的foo函数。经过对比,它们是不相等的,返回的是false,也证明了一点:bind函数会返回新的内容,但不会修改原本函数的this(他们指向的不是同一块内存空间,而是不相干的两处地方,不然此时foo与newFoo的对比就该返回true了
function foo(){ console.log(this) } // foo.call("小余") // foo.call("小余") // foo.call("小余") // foo.call("小余") // foo.call("小余") //默认绑定和现实绑定bind冲突 var newFoo = foo.bind("小余") var bar = foo console.log(bar === foo);//true console.log(newFoo === foo);//false
bind
函数和call
和apply
函数都可以用来改变函数的调用对象。但是它们之间有一些微妙的差别。
下面是这三个函数的一些基本区别:
bind
函数会创建一个新函数,其中调用对象被固定为指定的值。而call
和apply
函数则是立即调用函数,并改变调用对象。bind
函数可以在调用时指定函数的参数,而call
和apply
函数则需要在调用时传入所有的参数。bind
函数返回的是一个新的函数,而call
和apply
函数则是立即执行函数。
规则4:new绑定
- JavaScript中的函数可以当作一个类的构造函数来使用,也就是使用new关键字
- 使用new关键字来调用函数是,会执行如下的操作:
- 创建一个全新的对象
- 这个新对象会被执行prototype连接
- 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
- 如果函数没有返回其他对象,表达式会返回这个新对象
new来调用,会把我们生成的新的对象赋值给这个Person里面内部的的this
准确来说,js 中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。
那么new一个函数究竟发生了什么呢,大致分为三步:
- 以构造器的prototype属性为原型,创建新对象;
- 将this(可以理解为上句创建的新对象)和调用参数传给构造器,执行;
- 如果构造器没有手动返回对象,则返回第一步创建的对象
function Person(){ console.log(this); } Person()//正常调用 new Person()//new调用
调用区别:
我们通过一个new关键字调用一个函数时(构造器),这个时候this是在调用这个构造器创建出来的对象
- this = 创建出来的对象
- 这个绑定过程就是new 绑定
//案例2 function Person(name,age){ this.name = name this.age = age } var p1 = new Person("小余",20) console.log(p1.name,p1.age); var p2 = new Person("小满",23) console.log(p2.name,p2.age); //小余 20 //小满 23
一些函数的this分析
内置函数的绑定思考
- 有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库的内置函数
- 这些内置函数会要求我们传入另外一个函数;
- 我们自己并不会显示的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行;
- 这些函数中的this又是如何绑定的呢?
- setTimeout、数组的forEach、div的点击
setTimeout定时器
setTimeout(function(){ console.log("正常的this",this);//window }) setTimeout(()=>{ console.log("箭头函数的this",this);// },2000)
node环境下的结果:
监听点击
css样式不写,自己写一个宽高加背景颜色出来方便点击
我们监听点击中的this给到我们的是监听的对象,也就是如下的东西(div的元素对象):
this指向了div的元素对象,这说明了这个boxDiv会拿到内部的函数的,然后进行调用,相当于
也就是隐式绑定了(只不过内部进行了,没有显示出来),将boxDiv绑定到了onclick上面,所以this会绑定到div元素对象上
boxDiv.onclick()
<div class="box"></div> <script src="./闭包.js"></script>
//1.只能添加一个,如果重复添加,下面那个会把前面的给覆盖掉 const boxDiv = document.querySelector(".box") boxDiv.onclick = function(){ console.log(this); } //2.能够添加多个,实现原理:将所有的函数收集到数组里面,一旦发生点击的时候,我们就遍历数组,对这些函数进行调用, //然后内部会进行fn.call(boxDiv),实现将this绑定到boxDiv身上 boxDiv.addEventListener('onclick',function(){ console.log(this); }) boxDiv.addEventListener('onclick',function(){ console.log(this); }) boxDiv.addEventListener('onclick',function(){ console.log(this); })
数组中的绑定
正常情况下是返回window,但是forEach是接收第二个参数,第二个参数可以帮我们绑定对象,也就包括了this的指向位置
//3.数组forEach map filter find var names = ["ABC",'小余','小满'] names.forEach(function(){ console.log("item",this); }) //返回连续3个window //如果forEach加上了第二个参数,则this指向就会发生改变,因为绑定的对象已经被我们手动设置了,同理的map filter find 这些数组的高阶函数都差不多 names.forEach(function(){ console.log("item",this); },"小余")
forEach不加第二个参数:
forEach加第二个参数:
其他函数的效果(不一定就这些):
names.forEach(function(){ console.log("forEach",this); },"小余") names.map(function(){ console.log("map",this); },"小余") names.filter(function(){ console.log("filter",this); },"小余") names.find(function(){ console.log("find",this); },"小余")
this规则优先级
- 学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了很多条规则,优先级谁更高呢?
- 默认规则的优先度是最低的
- 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定ths
- 显示绑定优先级高于隐式绑定
- 代码测试
var obj = { name:"小余", foo:function(){ console.log(this); } } obj.foo()//单纯隐式绑定 //{ name: '小余', foo: [Function: foo] } //1.call、apply的显示绑定高于隐式绑定 obj.foo.call("我是小满")//显示绑定跟隐式绑定的冲突 //[String: '我是小满'] //2.bind与隐式绑定的优先度比较 var bar = obj.foo.bind("小余666") bar()
bind更明显的比较
//更明显的比较 function foo(){ console.log(this) } var obj1 = { name:"这是bind更明显的比较", foo:foo.bind("喜多川") } obj1.foo()//此时的foo属性才是被绑定到bind上面,前面刚开始的优先度比较更像是直接调用bind传入bar的,不够公平 //[String: '喜多川'] //答案返回的是喜多川,所以bind的显示绑定优先度也更高
- new绑定优先级高于隐式绑定
- 代码测试
如果this打印出来的是obj对象,则证明隐式绑定的优先度更高,如果是foo创建出来的函数对象,则证明new的优先度更高
var obj = { name:"小满Vue3视频讲得不错", foo:function(){ console.log(this); } } var f = new obj.foo() //foo {} 是foo创建出来的函数对象,证明了new的优先度更高 obj.foo()//这是隐式绑定的写法,可以方便进行对比
隐式绑定的结果应该是下面这样的:
- 结论:new关键字是不能够跟call和apply一起来使用的
因为call、apply跟new一样都是主动的去调用函数的,是不能够放在一起来使用。所以我们只能够将bind跟new来进行比较,这证明了一件事,那就是bind不是主动去调用函数的(他虽然也改变了this指向,但是会返回新的内容且需要我们去调用,并且不影响之前的内容),下方的案例也说明了这点
但是来了:new关键字内部在去执行的时候,会找到原函数的,将原来的函数当作一个构造器(构造器的概念直接跳到后面面向对象的部分看),这就是为什么new出来的bar函数最终还是调用foo函数的原因
function foo(){ console.log(this); } var bar = foo.bind("测试一下") //new出来的bar函数最终还是调用foo函数的 var obj = new bar()//foo{} bar()//[String: '测试一下']
优先度总结
new绑定>显示绑定(apply、call、bind)>隐式绑定>默认绑定(独立函数调用 )
bind高于call(一般情况下也不会同时用这两个,当作一个了解即可)
JavaScript高级笔记-coderwhy版本(四)https://developer.aliyun.com/article/1469640