Li Mei

个人技术博客

嗨,我是李梅,一名前端开发者。


主要是记录个人对技术的理解和开发过程中遇到的问题,欢迎了解更多。

JS:深入理解JavaScript-this

在上一篇文章【JS:深入理解JavaScript-执行上下文】中介绍了执行上下文是如何工作的。在这篇文章里会介绍执行上下文中的ThisBinding,也就是JavaScript中的this。

有四种可执行代码可以创建执行上下文,分别是global code function code moduleeval code。接下来分别介绍这global code function code可执行代码中的this(ThisBinding)到底指的是什么。

global code的this

在JS引擎运行global code之前,会创建一个全局执行上下文压入执行栈的栈底,这个全局执行上文的ThisBinding绑定的是全局对象,在浏览器里指的就是window。

function code的this

在文章【JS:深入理解JavaScript-词法环境】提到过JavaScript是静态作用域,词法环境是由代码结构决定的,开发把代码写成什么样,词法环境就是怎么样,跟方法在哪里调用没有关系。但是对于函数的this刚好反过来,跟代码在哪里定义没有关系,而跟代码在哪里调用有关系。一般我们调用函数有以下四种方式:

  • 普通函数调用,比如foo()或者(functon(){})()
  • 作为对象方法调用,比如obj.foo()
  • 构造函数调用,比如new foo()
  • 使用call、apply、bind等方法调用

在介绍着几种函数调用之前,我们先来看下ECMAScript对this的规范:

ECMAScript规范: 严格模式时,函数内的this绑定严格指向传入的thisArgument。非严格模式时,若传入的thisArgument不为undefined或null时,函数内的this绑定指向传入的thisArgument;为undefined或null时,函数内的this绑定指向全局的this。

普通函数调用

普通函数的调用,包括函数调用foo()和立即调用函数表达式(functon(){})(),传到函数里的thisArgument是undefined。根据ECMAScript规范,如果在非严格模式下,普通函数里的this就是全局对象,而在严格模式下就为undefined。

比如有以下代码:

var a = 2;
function foo(val) {
    console.log(this.a); //2
    console.log(val); //3
}
foo(3); 
/**
foo(3) 相当于是:foo().call(undefined,3) 的简化版本。
为了代码方便,所以有直接普通函数调用,thisArgument 默认是undefined,不用每次都敲这个undefined。
**/

如果把foo方法改为严格模式:

var a = 2;
function foo() {
    "use strict"
    console.log(this.a)
}
foo();

执行上面这段代码,会报错:

VM162:4 Uncaught TypeError: Cannot read property 'a' of undefined
    at foo (<anonymous>:4:22)

对象方法调用

作为对象方法调用,传到函数里的thisArgument是该对象。比如有如下代码:

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

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

obj.foo(); //2

需要注意的是,只有离函数最近的这个对象,才是该函数的this,比如有代码:

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

var obj2 = {
	a: 42,
	foo: foo
};

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

obj1.obj2.foo(); //42

还有一种比较看起来像对象方法调用,实际上是普通函数调用:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
var bar = obj.foo; 
var a = "global variable";
bar(); // global variable

构造函数调用

new functionname()构造函数调用,this指的是构造出来的新对象。

function foo(a) {
	this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

使用call、apply、bind等方法调用

call、apply、bind调用,可以显示传递对象给函数的thisArg,默认这几个函数的第一个形参是thisArg:

Function.prototype.apply( thisArg, argArray )
Function.prototype.call( thisArg , arg1, [ arg2, ... ] )
Function.prototype.bind( thisArg , [ arg1, [ arg2, ... ] ] )

需要注意的是当thisArg为null或者undefined,在非严格模式下,this是全局对象。

var obj = {
    a: 1
};

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

print.call(null);//window
print.call(undefined);//window
print.call(obj);//obj

箭头函数的this

箭头函数在调用的时候不会绑定this,它会去词法环境链上寻找this(parent scope),所以箭头函数的this取决于它定义的位置(包裹箭头函数的第一个普通函数的this),也就是箭头函数会跟包着它的作用域共享一个词法作用域。

window.a = 10
const foo = () => {
    console.log(this.a)
}
foo.call({a: 20}) // 10

let obj = {
    a: 20,
    foo: foo
}
obj.foo() //10

function bar() {
    foo()
}
bar.call({a: 20}) //10

回调函数的this

window.a = 10
let obj = {
    a: 20,
    foo: function () {
        console.log(this.a)
    }
}
setTimeout(obj.foo, 0) //10

上面代码运行结果是10,作为回调函数的时候,传递的是函数体,并不是函数名。在执行栈里,obj.foo已经执行完成被弹出执行栈,此时执行栈里只有全局执行上下文,setTimeout回调函数体执行的时候this为全局对象。

要想避免这种情况,有两种方法,第一种方法是使用bind返回的指定好this绑定的函数作为回调函数传入:

  • 使用bind指定this
    setTimeout(obj.foo.bind({a: 20}), 0) // 20
    
  • 使用箭头函数
    window.a = 10
    function foo() {
      return () => {
          console.log(this.a)
      }
    }
    const arrowFn = foo.call({a: 20})
    arrowFn() // 20
    setTimeout(arrowFn, 0) //20
    

案例分析

var deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function () {
        return function () {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
        };
    },
};

var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

在这个例子中,在 createCardPicker里面本身会返回一个匿名函数,这个匿名函数在倒数第二行代码的时候才会执行,这个时候deck并不是this的指向,在非严格模式的情况下,这里的this指的是全局对象window(如果是严格模式,那么this指向的是undefined), 所以在指向代码的时候会报错而不是弹出相关的信息。

为了修复这里的error,我们可以把 return 的方法改为箭头函数:

var deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function () {
        // NOTE: 箭头函数的this,指向包裹它最近普通函数的this
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
        };
    },
};

var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

或者,我们可以用 bind 来绑定 this:

var deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function () {
        return function () {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
        }.bind(this);
    },
};

var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

总结

  • 箭头函数中没有this绑定,this的值取决于其创建时所在词法环境链中最近的this绑定

  • 非严格模式下,函数普通调用,this指向全局对象

  • 严格模式下,函数普通调用,this为undefined

  • 函数作为对象方法调用,this指向该对象

  • 函数作为构造函数配合new调用,this指向构造出的新对象

  • 非严格模式下,函数通过call、apply、bind等间接调用,this指向传入的第一个参数, 传入的第一个参数若为undefined或null,this指向全局对象

  • 格模式下函数通过call、apply、bind等间接调用,this严格指向传入的第一个参数