探秘JS中的this指针

174 阅读6分钟

JS语言中的this指针是一个令人头疼的问题,如果不理解其背后的原理,常常会因为一些问题而被绕的云山雾罩、不明就里。今天,我们尝试整理this指针可能出现的所有场景,通过不同的场景总结出一套简单且通用的理论帮助你快速判断this指针的指向。

先来看一个例子:

const user = {
    name: "Scott Smith",
    sayHello: function () {
        console.log("Hello, " + this.name);
    }
};

user.sayHello();
const sayHello = user.sayHello;
sayHello();

在浏览器的控制台或node环境中运行上述代码,你会发现,代码的运行结果是:

Hello, Scott Smith
Hello, undefined

为什么会这样呢?

JS执行上下文环境(Execution Context)

我们知道,JS是一门解释型的脚本语言。所谓解释型的语言,即该语言不需要经过编译步骤,而是通过JS解释器逐行解释执行。代码被执行的环境称之为“JS执行的上下文环境”,JS Runtime维护着一个上下文环境的栈,当前执行的上下文环境位于栈顶。JS上下文的运行环境在运行期才会被决定,而且随时可能会改变,JS甚至提供了一些方法用于改变JS执行的上下文环境,这个后文会讲到,这就是产生This指针指向不明问题的根源。

附加知识(可跳过)

JS代码可以运行在浏览器或Node环境中,浏览器环境的全局对象是window,而Node环境的全局对象是global,本文我们假设JS代码运行在浏览器环境中,大家可以在浏览器的控制台复制上述代码执行测试。

简单分析,我们可以发现上述代码执行的上下文环境分别是user对象与window对象

代码 执行上下文
user.sayHello() user
sayHello() window

window对象中当然没有name属性,因此代码中引用name变量的地方会出现undefined,也就出现上面的执行结果。

通过上面的分析,我们可以得到下面几条结论:

  1. this指针始终指向当前代码运行的上下文环境
  2. 如果js代码直接在全局通过函数调用,其运行的上下文环境是window对象中,this指向window对象
  3. 如果js代码运行在某个对象中,其运行的上下文环境就是当前对象, this指向当前对象

如果是简单的代码环境,我们可以根据以上3条结论轻易得到this指针的指向。可是,当代码变得复杂之后,由于this指针的可变化性,将导致this指针变得难以捉摸、变幻莫测。

为了巩固上面的知识点,我们先尝试来做一道题:

function exec(a, b, callback) {
    callback(a + b);
}

const user = {
    a: 3,
    b: 4,
    foo: function () {
        exec(this.a, this.b, function (c) {
            console.log(c + "," + (this === window));
        })

    }
};

user.foo();

根据上面的结论3可以得出结论,这里的c一定等于7,但this === window中的this是指向user对象吗?答案是,不是!这里的this出现在函数的参数callback中,callback本身是一个函数,其执行的上下文是window对象。所以,这里的this应该指向window,最终结果应该是7,true

严格模式(strict mode)

严格模式是来自于ECMA-262中的一个规范,其目的是改善JS的错误检查、避免JS中的一些错误设计可能带来的问题。关于严格模式更详细的介绍,请关注我的微信公众号欧阳锋工作室

在严格模式下,this指针的指向有了一些变化,看下面这个例子:

function strictMode() {
    'use strict';
    console.log(this === window);
}

strictMode(); // 这里将打印false

在严格模式下,函数中的this指针将不再指向window对象,而是undefined,因此上述代码会打印false。这是严格模式的设计规范,至于为什么要这样设计,这是另一个值得探讨的话题。

构造函数中的this

JS是一门神奇的语言,通过在某个函数前面增加new关键字就可以创建一个JS对象,这看起来很像面向对象语言里面的new。当然,这只是一个语法糖,并且是一个不太容易理解的语法糖。尤其的对于同时熟悉面向过程与面向对象编程的同学来说更加难以理解。

在这个场景下,this指针指向当前构造函数创建的实例,看下面的例子:

function Person(fn, ln) {
    this.firstName = fn;
    this.lastName = ln;

    this.displayName = function () {
        console.log(this.firstName + " " + this.lastName);
    }
}

const person = new Person("Scott", "Smith");
person.displayName(); // 这里将打印Scott Smith

PS: 虽然难以理解,却为支持面向对象编程的ES6转ES5埋下了伏笔...

函数apply、call、bind

JS语言中,允许通过上面三个函数人为改变函数的作用域,这其实是一个很糟糕的设计!看下面的例子:

function Person(fn, ln) {
    this.firstName = fn;
    this.lastName = ln;

    this.displayName = function () {
        console.log(this.firstName + " " + this.lastName);
    }
}

const person = new Person("Scott", "Smith");
person.displayName();

const person1 = new Person("Steve", "Jobs");

// 以下三个方法均会打印Steve Jobs
person.displayName.apply(person1);
person.displayName.call(person1);
person.displayName.bind(person1)();

这里的displayName通过apply、call、bind三个函数将函数的作用域由person实例转移到了person1。因此,最终会得到Steve Jobs这样的执行结果。

箭头函数

JS一直在致力于解决this指针带来的问题,ES6中的箭头函数就是一个典型的例子。在箭头函数中,this指针始终指向同样的对象,一旦绑定将无法通过apply、call、bind函数改变this指向。先看一个不用箭头函数的例子:

function Person(fn, ln) {
    this.firstName = fn;
    this.lastName = ln;

    this.displayName = function () {
        const foo = function () {
            console.log("foo => " + this.firstName + "," + this.lastName);
        };
        foo();
    }
}

const person = new Person("Scott", "Smith");
person.displayName();

由于foo的作用域在函数体中,this指针指向window对象,window对象中当然不存在firstNamelastName属性。因此,这里将打印foo => undefined,undefined

如果我们使用箭头函数,将会发生什么变化呢?

function Person(fn, ln) {
    this.firstName = fn;
    this.lastName = ln;

    this.displayName = function () {
        const foo = () => {
            console.log("foo => " + this.firstName + "," + this.lastName);
        };
        foo();
    }
}

const person = new Person("Scott", "Smith");
person.displayName();

由于箭头函数不会改变this指针的指向,即箭头函数中的this指针如同在函数外一样,始终指向当前person实例。因此,这里将得到正确的打印结果foo => Scott,Smith

结论

以上就是this指针可能存在的所有场景,我们发现,由于严格模式与箭头函数的加入,使得原本就比较难以理解的this指针开始变得更加棘手起来。

事实上,如果我们将以上几种情况区分开,结论依然是非常清晰的。最后,我用一个表格来总结this指针在不同场景下的表现以及推断方法。

场景一:ES5

  • 如果this指针出现在简单函数中,this指向window对象
  • 如果this指针出现在对象的属性函数中,this指向当前对象
  • 如果使用了apply、call、bind等函数改变了this指针的执行,以改变后的this指针指向为准。

场景二:严格模式

在严格模式中,简单函数中的this指针指向undefined

场景三:箭头函数

箭头函数不会改变this指向,this指针始终指向原有上下文环境。

最佳实践

  • 在对象中,应该始终使用箭头函数代替普通函数使用。
  • 应该避免通过apply、call、bind等函数频繁改变this指针指向。
  • 应该使用严格模式进行约束,避免this指针不可控。

欧阳锋工作室