JavaScript(1)之——this指针

229 阅读5分钟

  读了《你不知道的JavaScript(上)》之后总结一下this指针,this到底是什么?它到底指向什么?

什么是this指针?

  想知道this指针是什么。首先需要明白执行上下文的概念以及结构。我先简单介绍一下执行上下文,具体的介绍会另写一章。
  执行上下文是当前所执行的代码的运行环境,而Js的运行环境包括:全局环境,函数环境和eval(已经不推荐使用)。在执行不同的代码时,会进入当前代码的执行上下文。因此在执行一段Js程序时必然会产生多个执行上下文,Js引擎会用栈(执行上下文栈)这个数据结构来装载它们。
  浏览器首次加载脚本,全局环境对应的执行上下文入栈,且它永远都在栈底,每进入一个不同的代码片段(不同的作用域),它的执行上下文就会入栈,当该段代码执行完之后就会将它的执行上下文出栈。而执行代码的顺序就又是另一个知识点了,涉及到event loop事件循环。
  一个执行上下文可以将它抽象的看作一个对象object,它包含一些属性,VO(变量对象),作用域链以及this。
  现在我们可以知道this就是执行上下文的一个属性,会在函数执行的过程中用到。

this的指向

  首先抛出一个重要的结论:this既不指向函数自身也不指向函数的词法作用域。this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
  确定this的指向,也可以称为决定this的绑定对象。需要找到函数的调用位置之后,判断需要应用以下哪四条绑定规则:

1. 默认规则

  独立函数调用(函数调用时没有任何修饰),该函数的this指针指向全局环境。

  ES5环境下的非严格模式:

var a = 1;

function thisTo() {
    var a = 2;
    console.log(this.a);
}

thisTo();

  在chrome控制台下测试结果输出1。

  ES6环境下自动采用严格模式:

const a = 1;

function thisTo() {
    const a = 2;
    console.log(this.a);
}

thisTo();

  在chrome控制台下测试结果为undefined。即严格模式下this会绑定到undefined。

2. 隐式绑定

  函数被调用时,某一个对象拥有该函数引用,则该函数的this指向该对象。

  ES5和ES6的结果相同,不再附上ES6的代码:

var a = 1;

var obj = {
    a: 2,
    thisTo: thisTo
};

function thisTo() {
    var a = 3;
    console.log(this.a);
}

obj.thisTo();

  在chrome控制台下测试结果输出2。
  隐式绑定规则又下个问题,被隐式绑定的函数可能会丢失它的绑定对象,这时它会使用默认规则,将this指向全局对象或者undefined。

var a = 1;

var obj = {
    a: 2,
    thisTo: thisTo
};

function thisTo() {
    var a = 3;
    console.log(this.a);
}

var loseThis = obj.thisTo;

loseThis();

  在chrome控制台下测试结果输出1。
  虽然loseThis是obj.thisTo的一个引用,但实际上,它引用的是thisTo,相当于一次独立函数调用,应用了默认规则。
  类似于function(obj.thisTo)或者setTimeout(obj.thisTo,100)都是实际上引用了thisTo,应用了默认规则,因此我们应该尽可能的明确this的指向,让它固定绑定的在某个上下文对象上,不会因为某个意外而改变指向。

3. 显式绑定

  调用函数的call和apply方法,让某个对象强制调用该函数,使得函数的this指向该对象。call和apply的第一个参数是一个对象,在调用函数时会将this绑定到该对象上。

var a = 1;

var obj = {
    a: 2,
};

function thisTo() {
    var a = 3;
    console.log(this.a);
}

thisTo.call(obj);

  在chrome控制台下测试结果输出2。
  显式绑定仍然无法解决丢失绑定的问题:

var a = 1;

var obj1 = {
    a: 2,
};
var obj2 = {
    a: 4,
};

function thisTo() {
    var a = 3;
    console.log(this.a);
}

thisTo.call(obj1);
thisTo.call(obj2);
thisTo();

  在chrome控制台下测试结果输出2,4,1。this绑定的对象会变化,可能在某些回调函数时不经意修改了this的绑定对象,造成this指向丢失。显式绑定的一个变种可以解决这个问题,它的this指向一经绑定就不能再修改。

var a = 1;

var obj1 = {
    a: 2,
};
var obj2 = {
    a: 4,
};

function thisTo() {
    var a = 3;
    console.log(this.a);
}

var hard = thisTo.bind(obj1);

hard();
setTimeout(hard, 1000);

hard.call(obj2);

hard = thisTo.bind(obj2);
hard();

  在chrome控制台下测试结果输出2,2,4,2。上面一种方式创建了函数hard,它的内部强制将thisTo的this绑定到obj1。无论之后如何调用修饰函数hard,它都会在obj1上调用thisTo。但是将函数hard重新赋值,在它内部重新将thisTo的this绑定到obj2上是可以的。

4. new绑定

  调用构造函数之后新创建的实例对象会绑定到函数调用的this上。

function Cat(name) {
    this.name = name;
    this.miaow = function() {
        console.log(this.name + "miaomiaomiao")
    }
}

var cat1 = new Cat('kitty');

console.log(cat1.name);
cat1.miaow();

var cat2 = new Cat('tom');

console.log(cat2.name);
cat2.miaow();

  控制台输出:

kitty
kittymiaomiaomiao
tom
tommiaomiaomiao

  使用new来调用函数,会自动执行以下操作:

(1)创建一个实例对象。
(2)这个对象的[[prototype]]属性会和原型对象关联。
(3)这个对象会绑定到函数调用的this。
(4)返回这个实例对象。

优先级

  new绑定>显示绑定>隐式绑定>默认绑定

this的例外

  上述四条规则已经可以包含所有正常的函数。但是ES6的箭头函数是个例外。它不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。