this,你到底是谁?--javascript前端

1,769

this是javascript前端编程中非常常用的一个关键字,也是面试常考题目,但是由于this不是其他面向对象语言的固定指向,而是情况不同的调用代码,会产生不同指向的this。下面我们就系统的分析下我们在编程时,如何理解每种情况下的this指向。

this的定义

首先,我们面对一个编程语言问题的时候,特别是关键字或者原生API的时候,第一反应应该是去查官方文档,因为官方文档才是最正确,最权威,最通俗的解释,这也是我编程这么多年的经验总结。下面是this在MDN Web Docs中的解释

在与其他语言相比,函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。

绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用的。ES2015 引入了箭头函数,箭头函数不提供自身的 this 绑定(this 的值将保持为闭合词法上下文的值)。

其中最重要的一句是,”函数的调用方式决定了 this 的值(运行时绑定)“,我们可以理解为在例如person.run()调用时,run方法中的this指向的是person这个对象。

下一章节中根据具体案例具体分析一下。(下列情况均指在非严格模式下的情况分析)

this的案例分析

我们将使用6个案例来分析this的每种场景下的含义

案例1 函数执行

function run(){
    console.log('this->' + this)
}
​
run()//this->[object Window]console.log('this->' + this)//this->[object Window]var title = 'window'
function doJob(){
    let title = 'doJob'
    function test(){
        console.log('this->' + this.title)//this->window
    }
    test()
}
doJob()

第一个是打印执行run时的this对象,由于未指定run的执行对象,默认this指向window。

第二个是在全局环境下执行打印this,此时this也是默认的window对象。

第三个的重点是看test的执行,test是单独执行,没有任何绑定对象,所以test中的this是window对象

案例2 对象创建

function run(){
    this.sum = 18
    console.log('this->' + this)
}
​
new run()//this->[object Object]let person = {name:'123',prun:run};
person.prun()//this->[object Object]let arr = [1,2,run]
arr[2]() //this->1,2,function run(){...}

注意: new在执行时会做四件事情

  1. 创建一个新的子对象;
  2. 让子对象继承构造函数的原型对象(prototype);
  3. 调用构造函数,将this指向子对象,并给子对象添加对应的属性(例如在run中添加this.sum=18);
  4. 将子对象返回到函数外面。

第一个new run()执行时,可以理解为将new将run变成了一个构造函数,此时this会指向新创建的子对象,打印出来//this[object Object]的结果

第二个person.run()执行时,this指向的是’.‘前面的person对象,所以this指向person。

第三个arr[2]()执行时,this指向arr数组。

案例3 call,apply,bind的应用

function run(){
    console.log('this->' + this)
}
​
let obj = {name:'456'}
run.call(obj)//this->[object Object]
run.apply(obj)//this->[object Object]
let bindRun = run.bind(obj)
bindRun()//this->[object Object]

函数call,apply,bind的效果都是指定this的对象,影响函数执行的作用域。所以上述都是打印的指定的obj对象。

案例4 dom绑定

<button onclick="clickFun()">button</button>
...
function clickFun(){
    console.log('this->' + this)//this->[object Window]
}

dom元素绑定事件中,this指向window对象。

案例5 箭头函数

let arrow = ()=>{
    console.log('this->' + this) //this->[object Window]
}
arrow()
​
​
var title = 'window'
function test(){
    let title = 'test'
    console.log('test this->' + this.title) //test this->p
    let arrow1 = ()=>{
        console.log('arrow1 this->' + this.title) //arrow1 this->p
    }
    arrow1()
​
    setTimeout(() => {
        console.log('setTimeout this->' + this.title)//setTimeout this->p   
    });
}
let p = {title:'p',test}
p.test()
​
var title = 'window'
let obj = {
    title:'obj',
    fun:()=>{
        console.log('this->' + this.title)//this->window
    },
    getTitle(){
        setTimeout(() => {
            console.log('this->' + this.title)   //this->obj
        });
    }
}
obj.fun()
obj.getTitle()

首先,箭头函数没有this指向,它的this指向上一级的作用域中的this。

第一个执行arrow()函数时,由于箭头函数没有this指向,所以this指向上一级的window;

第二个arrow1与setTimeout中的箭头函数执行时,this跟随test的函数的this指向,test执行前方有p对象,所以这里面三个的this都是指向p对象,所以都是打印p

第三个中我们先分析obj.fun()执行,由于fun函数是一个箭头函数,this指向上一级的this,上一级指的是全局window,不是obj,因为对象没有作用域。这里是容易混淆的一个点。然后下面的setTimeout中this指向getTitle函数的this,getTitle因为有'.'前的obj,所以这里的this指向的是obj

案例6 原型对象函数

function Student(name){
    this.name = name
}
​
Student.prototype.logName = function(){
    console.log(this.name);
}
​
let xiaoqiang = new Student('xiaoqiang')
let dawang = new Student('dawang')
xiaoqiang.logName()//xiaoqiang
dawang.logName()//dawang

原型对象方法中,在执行时,this指向调用该方法的子对象,所以xiaoqiang和dawang调用时,会分别打印出各自的名称,也遵循了一个this指向看调用的原则

总结

this的指向不看定义,看调用。

  1. this默认指向window;
  2. 函数调用时,this通常指向调用函数的对象,例如 obj.fun()时,this指向obj;
  3. 构造函数中,this指向新创建的对象,例如new Fun()时,this指向新创建的对象;
  4. 使用call,apply,bind可以自定义this的指向对象
  5. dom事件中的this,指向该文档的window对象
  6. 箭头函数没有独立的this,this指向上一级的this对象;
  7. 原型对象共有方法中的this,这个this指向将来调用此方法的’.‘前子对象