JavaScript函数的this指向
1、为什么需要this
在常见的编程语言中,几乎都有this这个关键字,但是JavaScript中的this和常见的面向对象语言中的this不太一样:
常见面向对象的编程语言中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在类的方法中。
也就是你需要有一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象。
但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义。
2、this指向是什么
首先说一个最简单的,this在全局作用域下指向什么?
这个问题非常容易回答,在浏览器中测试的指向的是window
// 在大多数情况下,this都是出现在函数中
// 在全局作用域下
// 浏览器:window(globalObject),Node环境:{}
console.log(this);
// console.log(window);
但是,在开发中很少直接在全局作用域下去使用this,通常都是在函数中使用
所有的函数在被调用时,都会创建一个执行上下文
这个上下文中记录着函数的调用栈,AO对象等
this也是其中的一条记录
我们先来看一个让人困惑的问题:
定义一个函数,我们采用三种不同的方式对它进行调用,它产生了三种不同的结果
// this指向什么,跟函数所处的位置是没有关系的,跟函数被调用的方式是有关系的
function foo(){
console.log(this);
}
// 1.直接调用这个函数
foo() //window
// 2.创建一个对象,对象中的函数指向foo
var obj = {
name:'yushuai',
foo:foo
}
obj.foo() //obj
// 3.apply调用
foo.apply('abc') //abc
这个案例可以给我们什么样的启示呢?
1、函数在调用时,JavaScript会默认给this绑定一个值
2、this的绑定和定义的位置(编写的位置)没有关系
3、this的绑定和调用方式以及调用的位置有关系
4、this是在运行时被绑定的
那么this到底是怎样的绑定规则呢
绑定一:默认绑定;
绑定二:隐式绑定;
绑定三:显示绑定;
绑定四:new绑定
3、this的绑定规则
(1)规则一:默认绑定
什么情况下使用默认绑定呢?独立函数调用.
独立的函数调用我们可以理解成函数没有被绑定都某个对象上进行调用
下面通过几个案例看一下,常见的默认绑定
// 默认绑定:独立函数调用
// 1.案例一:
function foo(){
console.log(this);
}
foo()//window
// 2.案例二
function foo1(){
console.log(this);//window
}
function foo2(){
console.log(this);//window
foo1()
}
function foo3(){
console.log(this);//window
foo2()
}
foo3()
// 3.案例三
var obj = {
name:'zs',
foo:function(){
console.log(this);
}
}
var bar = obj.foo;
bar()//window
// 4.案例四:
function foo(){
console.log(this);
}
var obj = {
name:'zs',
foo:foo
}
var bar = obj.foo;
bar() //window
// 5.案例五:
function foo(){
function bar(){
console.log(this);
}
return bar
}
var fn = foo();
fn()//window
var obj = {
name:'zs',
eating:fn
}
obj.eating();//obj隐式绑定
(2)规则二:隐式绑定
另外一种比较常见的调用方式是通过某个对象进行调用的,也就是说它的调用位置中,是通过某个对象发起的函数调用的。
同样通过几个案例来看一下,常见的默认绑定:
// 隐式绑定:object.fn()
// Object对象会被js引擎绑定到fn函数中的this里面
function foo(){
console.log(this);
}
// 独立函数调用
foo() //window
// 1.案例一
var obj = {
name:'zs',
foo:foo
}
obj.foo() //obj对象
// 2.案例二:
var obj = {
tName:'zs',
eating:function(){
console.log(this.tName + '在吃东西');
},
running:function(){
console.log(obj.tName + '在跑步');
}
}
obj.eating(); //zs在吃东西,这里的this指向obj对象
obj.running()
var fn = obj.eating
//这里就就不用name进行测试了,因为window上有name属性,会产生一定的误导
fn() //undefined在吃东西,这里的this指向window
// 3.案例三:
var obj1 = {
name:'objs',
foo:function(){
console.log(this);
}
}
var obj2 = {
name:'obj2',
bar:obj1.foo
}
obj2.bar() //obj2
(3)规则三:显式绑定
首先说一下,隐式绑定有一个前提条件
必须在调用的对象内部有一个对函数的引用(比如一个属性);
如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
正是通过这个引用,间接的将this绑定到了这个对象上;
如果我们不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?
JavaScript所有的函数都可以使用call和apply方法(这个和Prototype有关)。
它们两个的区别这里不再展开;
其实非常简单,第一个参数是相同的,后面的参数,apply为数组,call为参数列表;
这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的。
在调用这个函数时,会将this绑定到这个传入的对象上。
因为上面的过程,我们明确的绑定了this指向对象,所以称之为显式绑定
那么就看下,call、apply、bind的绑定
function foo(){
console.log("函数被调用了",this);
}
// 1.foo直接调用和call/apply调用的不同在于this绑定的不同
// foo直接调用指向的是全局对象(window)
var obj = {
name:'obj'
}
// call/apply是可以指定this的绑定对象
foo.call(obj) //obj
foo.apply(obj) //obj
foo.apply('aaaa') //aaaa
// 2.call和apply有什么区别?
function sum(num1, num2, num3) {
console.log(num1 + num2 + num3, this)
}
sum.call("call", 20, 30, 40)
sum.apply("apply", [20, 30, 40])
// 3.call和apply在执行函数时,是可以明确的绑定this, 这个绑定规则称之为显示绑定
function foo(){
console.log(this);
}
// 默认绑定和显式绑定的冲突:优先级(显式绑定)
var newFoo = foo.bind('aaa')
newFoo() //aaa
newFoo()
newFoo()
var bar = foo
console.log(bar === foo); //true
console.log(newFoo === foo);//false
(4)规则四:new绑定
JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。
使用new关键字来调用函数是,会执行如下的操作:
1.创建一个全新的对象;
2.这个新对象会被执行prototype连接;
3.这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);
4.如果函数没有返回其他对象,表达式会返回这个新对象;
// 通过一个new关键字调用一个函数时(构造器),这个时候this时在调用这个构造器时创建出来的对象
// this = 创建出来的对象
// 这个绑定过程就是new绑定
function Person(name,age){
this.name = name
this.age = age
}
var p1 = new Person('zs',18)
console.log(p1.name,p1.age);
var p2 = new Person("ls", 30)
console.log(p2.name, p2.age)
var obj = {
foo: function() {
console.log(this)
}
}
(5)内置函数的绑定思考
有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。
这些内置函数会要求我们传入另外一个函数;
我们自己并不会显示的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行;
这些函数中的this又是如何绑定的呢?
// 1.setTimeout
function xsSetTimeout(fn,duration){
fn.call('abc')
}
xsSetTimeout(function(){
console.log(this); //abc
},3000)
setTimeout(function(){
console.log(this); //window
},2000)
// 2.监听点击 this指向的是当前dom
const boxDiv = document.querySelector('.box')
boxDiv.onclick = function(){
console.log(this); //当前的dom
}
boxDiv.addEventListener('click',function(){
console.log(this);
})
boxDiv.addEventListener('click',function(){
console.log(this);
})
// 3.数组forEach/map/filter/find
var names = ['abc','cvb','nbd']
names.forEach(function (item) {
console.log(item, this); //这里的this向window
})
names.forEach(function(item){
console.log(item,this);
},'abc') //this指向abc
names.map(function(item){
console.log(item,this);
},'cde') //this指向cde
4、this绑定规则的优先级
(1)默认规则的优先级最低
毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this
(2)显式绑定优先级高于隐式绑定
代码测试
var obj = {
name:'obj',
foo:function(){
console.log(this);
}
}
obj.foo(); //obj
// 1.call/apply的显式绑定高于隐式绑定
obj.foo.apply('abc') //abc
obj.foo.call('abc')
// 2.bind的优先级高于隐式绑定
var bar = obj.foo.bind('cba')
bar() //cba
// 3.更明显的绑定
function foo(){
console.log(this);
}
var obj = {
name:'obj',
foo:foo.bind('aaaa')
}
obj.foo()//aaaa
(3)new绑定优先级高于隐式绑定
代码测试
var obj = {
name:'obj',
foo:function(){
console.log(this);
}
}
// new的优先级高于隐式绑定
var f = new obj.foo() //foo{}
(4)new绑定优先级高于bind
new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
new绑定可以和bind一起使用,new绑定优先级更高
代码测试:
// 结论:new关键字不能和apply/call一起来使用
// new的优先级高于bind
function foo(){
console.log(this);
}
var bar = foo.bind('aaa')
bar() //aaa
var obj = new bar()//bar{}
// new绑定>显式绑定(apply/call/bind)>隐式绑定(obj.foo())>默认绑定(独立函数调用)
5、this规则之外
(1)this规则之外–忽略显式绑定
我们讲到的规则已经足以应付平时的开发,但是总有一些语法,超出我们的规则之外.
如果在显式绑定中,我们传入一个null或者undefined,那么这个显式绑定会被忽略,使用默认规则:
function foo(){
console.log(this);
}
foo.apply('abc')
foo.apply({})
// apply/call/bind:当传入null/undefined时,自动将this绑定成全局对象
foo.apply(null) //window
foo.apply(undefined) //window
var bar = foo.bind(null) //window
bar()
(2)this规则之外–间接函引用
另外一种情况,创建一个函数的间接引用,这种情况使用默认绑定规则
赋值(obj1.foo = obj1.foo)的结果是foo函数
foo函数被直接调用,那么是默认绑定
var obj1 = {
name:'obj1',
foo:function(){
console.log(this);
}
}
var obj2 = {
name:'obj2'
};
// obj2.bar = obj1.foo;
// obj2.bar(); //obj2
(obj2.bar = obj1.foo)() //window
(3)this规则之外–ES6箭头函数
在ES6中新增一个非常好用的函数类型:箭头函数
这里不再具体介绍箭头函数的用法,可以自行学习。
箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。
我们来看一个模拟网络请求的案例:
这里我使用setTimeout来模拟网络请求,请求到数据后如何可以存放到data中呢?
我们需要拿到obj对象,设置data;
但是直接拿到的this是window,我们需要在外层定义:var _this = this
在setTimeout的回调函数中使用_this就代表了obj对象
// 1.测试箭头函数中this的指向
var name = 'why';
var foo = ()=>{
console.log(this);
}
foo() //window
var obj = {foo:foo}
obj.foo() //window
foo.call('abc') //window
// 2.应用场景
var obj = {
data:[],
getData:function(){
// 发送网络请求,将结果放到上面data属性中
// 在箭头函数之前的解决方案
var _this = this
console.log(_this);
setTimeout(function(){
var result = ['abc','dfd','dsf'];
_this.data = result
// console.log(_this.data);
},2000)
// console.log(this.data);
}
}
obj.getData()
console.log(obj.data);
setTimeout(function(){
console.log(obj.data);
},2500)
之前的代码在ES6之前是我们最常用的方式,从ES6开始,我们会使用箭头函数:
为什么在setTimeout的回调函数中可以直接使用this呢?
因为箭头函数并不绑定this对象,那么this引用就会从上层作用于中找到对应的this
var obj = {
data:[]
getData:function(){
setTimeout(() =>{
//模拟获取到的数据
var res = ['avc','avd','sfd'];
this.data.push(...res)
},1000);
}
}