一网打尽this,对执行上下文说Yes

151 阅读5分钟

this到底指向谁

  • 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的this会被绑定到undefined上,在非严格模式下则会被绑定到全局对象window/global上。
  • 一般使用new方法调用构造函数时,构造函数内的this会被绑定到新创建的对象上。
  • 一般通过call/apply/bind方法调用函数时,函数体内的this会被绑定到指定参数的对象上。
  • 一般通过上下文对象调用函数时,函数体内的this会被绑定到该对象上。
  • 在箭头函数中,this的指向是由外层(函数或全局)作用域来决定的。

实战例题

例题组合1:全局环境中的this

function f1(){
    console.log(this)
}
function f2(){
    'use strict'
    console.log(this)
}
f1()//window
f2()//undefined

函数在浏览器全局环境中被简单调用,在非严格模式下this指向window,在通过use strict指明严格模式的情况下指向undefined。

const foo = {
    bar:10,
    fn:function(){
        console.log(this);
        console.log(this.bar);
        }
}
var fn1 = foo.fn;
fn1();//window,undefined

这里的this仍然指向window。虽然fn函数在foo对象中用来作为对象的方法,但是在赋值给fn1之后,fn1仍然在window的全局环境中执行的。因此,以上代码会输出window和undefined,其输出结果与以下语句等价

console.log(window);//window
console.log(window.bar);//undefined
const foo = {
    bar:10,
    fn:function(){
        console.log(this);
        console.log(this.bar);
        }
}
foo.fn()//{bar:10,fn:f},10

这时,this指向的是最后调用它的对象,在foo.fn()语句中,this指向foo对象。

例题组合2:上下文对象调用中的this

const student = {
    name:'jieye',
    fn:function(){
        return this;
    }
 }
 console.log(student.fn() === student);//true

当存在更复杂的调用关系时,如以下代码中的嵌套关系,this会指向最后调用它的对象,因此输出将会是Mike。

const person = {
    name:'jieye',
    brother:{
        name:'Mike',
        fn:function(){
            return this.name;
        }
   }
}
console.log(person.brother.fn());//Mike
const o1 = {
    text:'o1',
    fn:function(){
        return this.text;
    }
}
const o2 = {
    text:'o2',
    fn:function(){
        return o1.fn();
    }
}
const o3 = {
    text:'o3',
    fn:function(){
        var fn = o1.fn;
        return fn();
    }
}
console.log(o1.fn());//o1
console.log(o2.fn());//o1
console.log(o3.fn());//undefined

如果我们需要让console.log(o2.fn())语句输出o2,该怎么做?(不使用bind、call、apply)

const o1 = {
    text:'o1',
    fn:function(){
        return this.text;
    }
}
const o2 = {
    text:'o2',
    fn:o1.fn
}
console.log(o2.fn())

例题组合3:通过bind、call、apply改变this指向

bind、call、apply它们都是用来改变相关函数this指向的,但是call、apply是直接进行相关函数调用的;bind不会执行相关函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的this指向,开发者可以手动调用它。而call和apply的区别主要体现在参数的设定上。
以下3段代码是等价的:

//1
const target = {};
fn.call(target,'arg1','arg2');
//2
const target = {};
fn.apply(target,['arg1','arg2']);
//3
const target = {};
fn.bind(target,'arg1','arg2'){}
const foo = {
    name:'jieye',
    logName:function(){
        console.log(this.name);
    }
}
const bar = {
    name:'Mike'
}
console.log(foo.logName.call(bar));//Mike

例题组合4:构造函数和this

先看一道最直接的例题:

function Foo(){
    this.bar = 'jieye',
}
const instance = new Foo();
console.log(instance.bar);//jieye

new操作符调用构造函数时具体做了什么?

  • 创建一个新的对象;
  • 将构造函数的this指向这个新的对象;
  • 为这个对象添加属性、方法等;
  • 最终返回新的对象。
    上述过程也可以用如下代码表述
var obj = {};
obj._proto_ = Foo.prototype;
Foo.call(obj);

如果在构造函数中出现了显式return的情况,那么需要注意,其可以细分为两种场景:
场景1:执行以下代码将输出undefined,此时instance返回的是空对象o。

function Foo(){
    this.user = 'jieye';
    const o = {};
    return o;
}
const instance = new Foo();
console.log(instance.user);//undefined

场景2:执行以下代码将输出jieye,也就是说,instance此时返回的是目标对象实例this。

function Foo(){
    this.user = 'jieye';
    return 1;
}
const instance  = new Foo();
console.log(instance.user)

所以,如果构造函数中显式返回一个值,且返回的是一个对象(返回复杂类型),那么this就指向这个返回的对象;如果返回的不是一个对象(返回基本类型),那么this仍然指向实例。

例题组合5:箭头函数中的this

下面来看一段示例代码。在这段代码中,this出现在setTimeout()的匿名函数中,因此this指向window对象。

const foo = {
    fn:function(){
        setTimeout(function(){
            console.log(this)
        })
    }
}
console.log(foo.fn())

如果需要让this指向foo这个对象,则可以巧用箭头函数来解决,代码如下:

const foo = {
    fn:function(){
        setTimeout(()=>{
            console.log(this)
        })
    }
}
console.log(foo.fn())//{fn:f}

例题组合6:this优先级

我们常常把通过call、apply、bind、new对this进行绑定的情况称为显示绑定,而把根据调用关系确定this指向的情况称为隐式绑定。

function foo(){
    console.log(this.a);
}
const obj1 = {
    a:1,
    foo:foo
}
const obj2 = {
    a:2,
    foo:foo
}
obj1.foo.call(obj2);//2
obj2.foo.call(obj1);//1

输出结果为2、1,也就是说,call、apply的显示绑定一般来说优先级更高。

function(){
    this.a = a;
}
const obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);

上述代码通过bind将bar函数中的this绑定为obj1对象。执行bar(2)后,obj1.a的值为2,即执行bar(2)后,obj1对象为{a:2};
当再使用bar作为构造函数时,例如执行以下代码,则会输出3

var baz = new bar(3);
console.log(baz.a);

bar函数本身是通过bind方法构造的函数,其内部已经将this绑定为obj1,当它再次作为构造函数通过new被调用时,返回的实例就已经与obj1解绑了。也就是说,new绑定了bind绑定中的this指向。因此new绑定的优先级比显示bind绑定的更高。

function foo(){
    return a =>{
        console.log(this.a)
    }
}
const obj1 = {
    a:2
}
const obj2 = {
    a:3
}
const bar =  foo.call(obj1);
console.log(bar.call(obj2));

以上代码的输出结果为2,由于foo中的this绑定到了obj上,所以bar(引用箭头函数)中的this也会绑定到obj1上,箭头函数的绑定无法被修改。
如果将foo完全写成如下所示的箭头函数的形式,则会输出123

var a = 123;
const foo = ()=> a=> {
    console.log(this.a);
}
const obj1 = {
    a:2
}
const obj2 = {
    a:3
}
var bar = foo.call(obj1);
console.log(bar.call(obj2));

如果在把上述代码中的第一处变量a的声明修改一下:

const a = 123;
const foo = ()=> a=> {
    console.log(this.a);
}
const obj1 = {
    a:2
}
const obj2 = {
    a:3
}
var bar = foo.call(obj1);
console.log(bar.call(obj2));

答案为undefined,原因是使用const声明的变量不会挂载到window全局对象上。因此,this指向window时,自然也找不到a变量了。

开放例题分析

实现一个bind函数

Function.prototypr.bind = Function.prototype.bind || function(context){
    var me = this;
    var args = Array.prototype.slice.call(arguments,1);
    return function bound(){
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(context,finalArgs);
    }
}