前端面试之this指向


这几天参加了很多面试和笔试,其中有很多的各种各样的this指向的或笔试或面试题,发现自己其实有很多没有搞懂的地方,在学习的一些资料后,现整理一些学习资料.

this 是 javascript 中的一个关键字,但是它又是一个相对比较特殊的关键字,不像var let const 这些关键字一样,可以很清楚的搞明白它到底是如何使用的.

this会在执行上下文中绑定一个对象,但是是根据什么条件绑定的呢?在不同的执行条件下会绑定不同的对象,这也是让人捉摸不定的地方。

1. 理解this

1.1 this到底指向什么

最简单的情况就是this在默认情况下指向全局对象window,

console.log(this); // window

var name = "why";
console.log(this.name); // why
console.log(window.name); // why

但是,在平时使用this的过程中,我们很少会直接在全局作用于中使用this,通常都是在函数中使用.

所以函数在被调用的时候,会创建一个执行上下文,这个上下文中巨鹿中函数的调用栈,调用方法,传入的参数信息等,this也是其中的一个属性;

我们先来看一个让人困惑的问题:
定义一个函数,我们采用三种不同的方式对它进行调用,它产生了三种不同的结果

       function  foo() {
            console.log(this);
        }

        //1. 直接在全局作用域下调用函数
        foo();  //window

        // 2.调用方式二: 将foo放到一个对象中,再调用
        var obj = {name :'why',age : 18,foo :foo};
        obj.foo();  //obj对象

        // 3.调用方式三: 通过call/apply调用
        foo.call("123"); // String {"123"}对象

从上面的代码中我们可以得知什么呢?

  1. 函数在调用实,js会默认的给this绑定一个值
  2. this的绑定的定义的位置没有关系
  3. this的绑定和调用的方式和调用位置有关系
  4. this是在运行时才会被绑定的

下面我们就来具体的学习一下this到底是怎么绑定的.

2. this的绑定规则

2.1 默认绑定

在什么情况下函数的this会进行一个默认的绑定呢??

  • 函数独立调用的时候
  • 独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;

普通函数调用

  • 该函数直接被调用,并没有进行任何的对象关联.
  • 这样的独立函数会使用默认绑定,通常在默认绑定是,函数中的this指向全局对象wndow.
function foo() {
  console.log(this); // window
}

foo();

函数调用链(一个函数又调用另外一个函数)

  • 所有的函数调用都没有被绑定到某个对象上;
function test1() {
  console.log(this); // window
  test2();
}

function test2() {
  console.log(this); // window
  test3()
}

function test3() {
  console.log(this); // window
}
test1();

将函数作为参数传递到另外一个函数中

function foo(func) {
  func()
}

function bar() {
  console.log(this); // window
}

foo(bar);

我们对案例进行一些修改,考虑一下打印结果是否会发生变化:

  • 这里的结果依然是window,为什么呢?
  • 原因非常简单,在真正函数调用的位置,并没有进行任何的对象绑定,只是一个独立函数的调用;
function foo(func) {
  func()
}

var obj = {
  name: "why",
  bar: function() {
    console.log(this); // window
  }
}

foo(obj.bar);

2.2 隐式绑定

隐式绑定指的是在调用的位置是否有上下文关系(是否被某个对象所包含),这种情况下谁调用指向谁

通过对象调用函数

  • foo的调用位置是obj.foo()方式进行调用的
  • 那么foo调用时this会隐式的被绑定到obj对象上
        function  foo() {
            console.log(this);
        }

        var obj = {
            name :'why',
            foo :foo
        };
        obj.foo();  //obj对象

对象的链式调用

  • 我们通过obj2又引用了obj1对象,再通过obj1对象调用foo函数;
  • 那么foo调用的位置上其实还是obj1被绑定了this;
        function  foo() {
            console.log(this);
        }

        var obj1 = {
            name :'why',
            foo :foo
        };

        var obj2 = {
            name: 'gh',
            obj1: obj1,
        }
        obj2.obj1.foo();  //obj1对象

所以说,一个多级对象在调用方法时,this绑定在离他最近的那个对象上

通常会有下面三种情况:

  1. 如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的是undefined;

  2. 如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

  3. 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,

隐式丢失

隐式绑定有一种特殊情况,就是隐式丢失 当并不是直接调用某个函数或方法时,这时候会出现this绑定丢失的情况。

给函数起别名
        function  foo() {
            console.log(this);
        }

        var obj = {
            name : 'why',
            foo : foo,
        };

        var xyz = obj.foo;
        xyz();   //window
  • 结果最终是window,为什么是window呢?
  • 因为foo最终的调用位置是 xyz , 而xyz 在进行调用的时候没有绑定任何的对象,也就没有形成我们所说的隐式绑定,
  • 所以这里相当于调用的是全局的foo,结果为window
函数作为另外一个函数的参数
function foo(){
    console.log(this.a)
}

var obj = {
    a:2,
    foo: foo
}

function doFoo(fn){
    fn()
}
var a = 'baz'
doFoo(obj.foo)  // baz

2.3 显示绑定

通过前面的分析,我们可以得知,隐式绑定是由前提条件的:

  • 必须在调用的对象内部有一个函数的引用(如,foo:foo);
  • 如果没有这样的引用,我们在直接通过对象调用该方法时就会包找不到这个函数的错误.
  • 正是通过这个引用,间接的将this绑定到了这个对象的身上.

有时候我们并不希望在对象内部引用或者定义函数,同时又希望在这个对象的身上能对这个方法进行强制的调用,这个时候我们应该怎么做呢???

  • JavaScript所有的函数都可以使用call和apply方法(这个和Prototype有关)。

    • 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的。
    • 在调用这个函数时,会将this绑定到这个传入的对象上。

因为上面的过程,我们明确的绑定了this指向的对象,所以称之为显示绑定。

call,apply,bind

通过call或者apply绑定this对象

  • 显示绑定后,this就会明确的指向绑定的对象
        function  foo() {
            console.log(this);
        }

        var obj = {
            name : 'why',
        };

        foo.call(obj);  //obj
        foo.apply(obj);  //obj
        foo.bind(obj)(); //obj

内置函数

有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。

  • 这些内置函数会要求我们传入另外一个函数;
  • 我们自己并不会显示的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行;
  • 这些函数中的this又是如何绑定的呢?
setTimeout()
  • setTimeout中会传入一个函数,这个函数中的this通常是window
setTimeout(function() {
  console.log(this); // window
}, 1000);

为什么这里是window呢?

  • 这个和setTimeout源码的内部调用有关;
  • setTimeout内部是通过apply进行绑定的this对象,并且绑定的是全局对象;
数组的forEach

数组有一个高阶函数forEach,用于函数的遍历:

  • 在forEach中传入的函数打印的也是Window对象;
  • 这是因为默认情况下传入的函数是自动调用函数(默认绑定);
var names = ["1", "2", "3"];
names.forEach(function(item) {
  console.log(this); // 三次window
});

2.4 new 绑定

JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。
使用new关键字来调用函数时,会执行如下的操作:

  1. 创建一个全新的对象;
  2. 这个新对象会被执行Prototype连接;
  3. 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);
  4. 如果函数没有返回其他对象,表达式会返回这个新对象;
function Foo(a){
    this.a = a
}

var foo = new Foo(2)
console.log(foo.a) // 2
//这就说明当前的this不是全局对象

2.5 箭头函数中的this

箭头函数没有自己的this,当在北部使用了this时,它会指向最近一层作用域内的this,所以说箭头函数的this在定义时就已经确定了.

看了很多文章,都在说箭头函数指向最近一层作用域内的this,其实它也会分下面的两种情况

没有被函数包裹

  • 没有被函数包裹,那么它里面的this就指向就是全局,window上面的对象
        var name = 'why';
        var obj = {
            name: 'gh',
            sayName :()=> {
                console.log(this);  //window
                console.log(this.name);  //why
            }
        }
        obj.sayName() 

有被函数包裹

		var name = 'why';
        var obj = {
            name: 'gh',
            sayName(){
                (() => {
                    console.log(this);  //obj
                    console.log(this.name);  //gh
                })()
            }
        }
        obj.sayName()

像上面的立即执行函数如果是function的话就输出为全局a,箭头函数同样解决了一些问题,当我们在被函数包裹时就可以采取它然后就可以使用对象自身的方法以及属性值。

当this碰到return时

        function foo()
        {
            this.user = 'haha';
            return {};
        }
        var a = new foo;
        console.log(a.user); //undefined
        function foo()
        {
            this.user = 'haha';
            return function () { };
        }
        var a = new foo;
        console.log(a.user); //undefined
        function foo()
        {
            this.user = 'haha';
            return 1;
        }
        var a = new foo;
        console.log(a.user); //haha
        function foo()
        {
            this.user = 'haha';
            return undefined;
        }
        var a = new foo;
        console.log(a.user); //haha

上面的代码什么意思呢???

如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。

        function foo(){
            this.user = 'haha';
            return undefined;
        }
        var a = new foo;
        console.log(a.user); //haha
        console.log(a);  //foo {user: "haha"}

还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。

        function foo(){
            this.user = 'haha';
            return null;
        }
        var a = new foo;
        console.log(a.user); //haha
        console.log(a);  //foo {user: "haha"}

this面试题分析

例一

function a(){
    var user = "哈哈";
    console.log(this.user); //undefined
    console.log(this); //Window
}
a();

按照我们上面说的this最终指向的是调用它的对象,这里的函数a实际是被Window对象所点出来的.

例二

var o = {
    user:"haha",
    fn:function(){
        console.log(this.user);  //haha
    }
}
o.fn()

这里的this指向的是对象o,因为你调用这个fn是通过o.fn()执行的,那自然指向就是对象o,这里再次强调一点,this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁,一定要搞清楚这个。

例三

var obj = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //undefined
            console.log(this); //window
        }
    }
}
var j = obj.b.fn;
j();

这里this指向的是window,是不是有些蒙了?其实是因为你没有理解一句话,这句话同样至关重要。

this永远指向最后调用它的对象,也就是看他执行的时候是谁调用的,上面的代码中虽然fn是被b对象引用了,按时将fn赋值给变量j的时候并没有直接执行,所以这里的this最终指向了window
 
当我们修改最后一行代码如下的时候:

  var j = o.b.fn();
  // 12 obj

此时的this才会指向obj对象

例四

var length = 100;
function f1() {
	console.log(this.length);
}
var obj = {
length: 10,
f2 : function(f1) {
	f1();
	arguments[0]();  //指向arguments,拿到arguments的长度值
	}
}
obj.f2(f1,1);  
//100
//2

例五

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
function sayName() {
  var sss = person.sayName;
  sss(); 
  person.sayName(); 
  (person.sayName)(); 
  (b = person.sayName)(); 
}
sayName();

//window
//person
//person
//window

例六

var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1(); 
person1.foo1.call(person2); 

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);

解析:

// 隐式绑定,肯定是person1
person1.foo1(); // person1
// 隐式绑定和显示绑定的结合,显示绑定生效,所以是person2
person1.foo1.call(person2); // person2

// foo2()是一个箭头函数,不适用所有的规则
person1.foo2() // window
// foo2依然是箭头函数,不适用于显示绑定的规则
person1.foo2.call(person2) // window

// 获取到foo3,但是调用位置是全局作用于下,所以是默认绑定window
person1.foo3()() // window
// foo3显示绑定到person2中
// 但是拿到的返回函数依然是在全局下调用,所以依然是window
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数,通过显示绑定到person2中,所以是person2
person1.foo3().call(person2) // person2

// foo4()的函数返回的是一个箭头函数
// 箭头函数的执行找上层作用域,是person1
person1.foo4()() // person1
// foo4()显示绑定到person2中,并且返回一个箭头函数
// 箭头函数找上层作用域,是person2
person1.foo4.call(person2)() // person2
// foo4返回的是箭头函数,箭头函数只看上层作用域
person1.foo4().call(person2) // person1

例七:

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)

解析:

// 隐式绑定
person1.foo1() // peron1
// 显示绑定优先级大于隐式绑定
person1.foo1.call(person2) // person2

// foo是一个箭头函数,会找上层作用域中的this,那么就是person1
person1.foo2() // person1
// foo是一个箭头函数,使用call调用不会影响this的绑定,和上面一样向上层查找
person1.foo2.call(person2) // person1

// 调用位置是全局直接调用,所以依然是window(默认绑定)
person1.foo3()() // window
// 最终还是拿到了foo3返回的函数,在全局直接调用(默认绑定)
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数后,通过call绑定到person2中进行了调用
person1.foo3().call(person2) // person2

// foo4返回了箭头函数,和自身绑定没有关系,上层找到person1
person1.foo4()() // person1
// foo4调用时绑定了person2,返回的函数是箭头函数,调用时,找到了上层绑定的person2
person1.foo4.call(person2)() // person2
// foo4调用返回的箭头函数,和call调用没有关系,找到上层的person1
person1.foo4().call(person2) // person1
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值