为什么需要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")
结果如下:
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
函数则是立即执行函数。
JS中函数的this指向(二)https://developer.aliyun.com/article/1470344