this 是JavaScript中很重要的一个指针,但是往往也是最容易产生bug 的地方,因为稍不留神this的指向就和你以为的指向根本不是一个对象,让多数新手懵逼,部分老手觉得恶心,这是因为this的绑定 [难以捉摸],出错的时候还往往不知道为什么,相当反逻辑。熟悉Java的对this应该很熟悉,在Java中tthis在编码阶段不能确定出它指向谁,而是在运行时确定,指向的是当前调用它的对象。那么在JavaScript中这句话同样适用,记住一句话。在JavaSript中this指向的是最后调用它的那个对象,这句话在大部分场景是对的,但是还有几种特殊情况会改变this的指向。本文对this指向的所有情况做一个总结
默认绑定
分几种情况谈论下
普通函数调用
function foo(){
console.log(this.a);
}
var obj = {
a : 10,
foo : foo
}
foo();
// 结果
undefined
foo()
的这个写法熟悉吗,就是我们刚刚写的默认绑定,代码中直接调用了foo()
其等价于 window.foo()
调用,因此foo方法中的this指向的是window对象,等价于打印window.a
,而代码中没有定义全局变量a,故输出undefined
,如果写成下面这样。就会输出全局变量5
function foo() {
console.log( this.a );
}
var obj = {
a: 10,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = 5; // a是全局对象的属性
bar(); // 5
obj.foo();// 10
//等价的两个函数,输出不一样的结果。为什么?
//其实var bar === window.bar; 它隐式绑定了window 对象,所以它访问到的自然是 window.a
对于普通调用有一种情况会存在this指向丢失,那就是回调,看下面这个例子
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn其实引用的是foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo( obj.foo ); // "oops, global"
注意function有自己的函数作用域,function默认绑定的父级作用域,而在这个例子中传入的是foo()方法
,foo()此时属于window作用域的方法,因此调用它的是window对象
总结:对于普通的函数调用,实际上等价于window.foo()。在浏览器中最外层的对象就只能是window,因此可以简写成foo()调用。对于回调函数则要注意找到回调函数的父级作用域
对象函数调用
情况一:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。如下情况,o.fn()调用时,fn()指向对象o。因此输出对象o中的user变量值
var o = {
user:"追梦子",
fn:function(){
console.log(this.user);
console.log(this);
}
}
o.fn();
// 结果
追梦子
{user: "追梦子", fn: ƒ}
情况二:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,形成调用链,this指向的是最后一个调用它的对象,如下情况,o.b.fn()尽管fn调用链有两个对象o和b,但是fn指向的是最后一个调用它的对象,因此fn指向对象b,而在对象b中没有变量a,因此输出undefined
var o = {
a:10,
b:{
//a:12,
fn:function(){
console.log(this.a); //undefind 有两个对象b和o,所以此this.a指向它的上一级
}
},
fn1:function(){
console.log(this.a); //10
}
}
o.b.fn();
// 结果
undefined
构造函数调用
对于普通调用和对象调用,都可以总结成一句话 this指向的是最后调用它的那个对象
。但在JS中还有一种特殊的调用,学过面向对象的小伙伴对new肯定不陌生,JS的new和传统的面向对象语言的new的作用都是创建一个新的对象,但是他们的机制完全不同。创建一个新对象少不了一个概念,那就是构造函数
,传统的面向对象 构造函数 是类里的一种特殊函数,要创建对象时使用new 类名()
的形式去调用类中的构造函数,而JS中就不一样了。在JS中的只要用new修饰的 函数就是’构造函数’,准确来说是 函数的构造调用,因为在JS中并不存在所谓的 [构造函数]。那么用new 做到函数的构造调用
后,JS做了什么呢
- 创建一个新对象。
- 把这个新对象的
__proto__
属性指向 原函数的prototype
属性。(即继承原函数的原型) - 将这个新对象绑定到 此函数的this上 。
- 返回新对象,如果这个函数没有返回其他对象。
对于函数通过构造调用this的指向问题,分为两种情况。
情况一:这种情况仍然适用于this指向的是最后调用它的那个对象,这种方式也是new方式的常规用法,代码如下
function foo(){
this.a = 10;
console.log(this);
}
foo(); // window对象
console.log(window.a); // 10 默认绑定
var obj = new foo(); // foo{ a : 10 } 创建的新对象的默认名为函数名
// 然后等价于 foo { a : 10 }; var obj = foo;
console.log(obj.a); // 10 new绑定
情况二:如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。下面列举了五种返回对象和返回值的情况
// 1.
function fn()
{
this.user = '追梦子';
return {};
}
var a = new fn;
console.log(a.user); //undefined
// 2.
function fn()
{
this.user = '追梦子';
return function(){};
}
var a = new fn;
console.log(a.user); //undefined
// 3.
function fn()
{
this.user = '追梦子';
return 1;
}
var a = new fn;
console.log(a.user); //追梦子
// 4.
function fn()
{
this.user = '追梦子';
return undefined;
}
var a = new fn;
console.log(a.user); //追梦子
// 5.
function fn()
{
this.user = '追梦子';
return null;
}
var a = new fn;
console.log(a.user); //追梦子
显示绑定
在JS中提供了几个方法可以给函数强制性绑定this。它们就是call
,apply
,bind
三个方法。这里我们就要用到 js 给我们提供的函数 call 和 apply,它们的作用都是改变函数的this指向,第一个参数都是 设置this对象。两个函数的区别:
- call从第二个参数开始所有的参数都是 原函数的参数。
- apply只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。
举个例子
function foo(a,b){
console.log(a+b);
}
foo.call(null,'海洋','饼干'); // 海洋饼干 这里this指向不重要就写null了
foo.apply(null, ['海洋','饼干'] ); // 海洋饼干
bind函数有点特殊,在React中常常用bind方法为组件绑定事件 ,它和call,apply都不同。使用bind函数为对象绑定方法时,不会立刻执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回,例如
function foo(){
console.log(this.a);
}
var obj = { a : 10 };
foo = foo.bind(obj);
foo(); // 10
总结: call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别
箭头函数绑定
ES6 提供了箭头函数,增加了我们的开发效率,但是在箭头函数里面,没有 this
,箭头函数里面的 this
是继承外面的环境,即箭头函数绑定规则不是上面介绍的规则,而是完全根据外部作用域来决定this,且绑定无法被修改。因此箭头函数的this对象判断比较简单也比较特殊,举个例子
let obj={
a:222,
fn:function(){
setTimeout(function(){console.log(this.a)})
}
};
obj.fn();//undefined
不难发现,虽然 fn() 里面的 this是指向 obj ,但是,传给 setTimeout 的是普通函数, this 指向是 window , 但是window下面没有 a ,所以这里输出 undefined。换成箭头函数
let obj={
a:222,
fn:function(){
setTimeout(()=>{console.log(this.a)});
}
};
obj.fn();//222
这次输出 222 是因为,传给 setTimeout 的是箭头函数,然后箭头函数里面没有 this ,所以要向上一层作用域查找,在这个例子上, setTimeout 的上层作用域是 fn。而 fn 里面的 this 指向 obj ,所以 setTimeout 里面的箭头函数的 this ,指向 obj 。所以输出 222
再举个例子,使用普通调用函数和箭头函数对比一下。下面是普通调用
var people = {
Name: "海洋饼干",
getName : function(){
console.log(this.Name);
}
};
var bar = people.getName;
bar(); // undefined
很明显调用bar的是window,而window中没有Name变量,故输出undefined,如果改成箭头函数,如下
var people = {
Name: "海洋饼干",
getName : function(){
return ()=>{
console.log(this.Name);
}
}
};
var bar = people.getName(); //获得一个永远指向people的函数,不用想this了,岂不是美滋滋?
bar(); // 海洋饼干
getName()
方法永远指向外一层对象people
(箭头函数比较特殊,并不需要在运行时判断是谁调用它,而是在代码阶段就能判断出this指向一定是外一层调用对象),可能会有人不解为什么在箭头函数外面再套一层,直接写不就行了吗,搞这么麻烦干嘛,其实这也是箭头函数很多人用不好的地方。改成如下
var obj= {
bar : function(){
return ()=>{
console.log(this);
}
},
baz : ()=>{
console.log(this);
}
}
obj.bar()(); // obj
obj.baz(); // window
为什么obj.baz()
方法输出的是window
- 我们先要搞清楚一点,obj的当前作用域是window
- 如果不用function(function有自己的函数作用域)将其包裹起来,那么
baz()
是箭头函数,寻找外一层调用对象,那就只能是window了。(箭头函数不会绑定函数所在的作用对象,而是向上找一层调用对象) - 用function包裹的目的就是将箭头函数绑定到当前的对象上。函数的作用域是当前这个对象,然后箭头函数会自动绑定函数所在作用域的this,即obj。
最终总结
对于this的指向问题,大部分情况可以用文章开头那句话适用this指向的是最后调用它的那个对象。对于少部分几种情况,就是使用new调用方法时,方法存在返回值需特别注意一下,还有就是箭头函数中不存在this对象,需要继续向外面一层的上下文寻找this指向,最后一点就是函数的回调会改变this的执行。
其实最终所有的this指向的还是最后调用它的那个对象,只不过这个调用对象在有些情况下和我们预期的不一致,让我们摸不着头脑。所以还是把JS的基础打扎实就不会有这些疑惑了,哈哈。