[核心概念] 一文说透 JS 中 this 的基本概念

721 阅读6分钟

this 深入理解

系列开篇

为进入前端的你建立清晰、准确、必要概念和这些概念的之间清晰、准确、必要关联, 让你不管在什么面试中都能淡定从容。没有目录,而是通过概念关联形成了一张知识网络,往下看你就明白了。当你遇到【关联概念】时,可先从括号中的(强/弱)判断简单这个关联是对你正在理解的概念是强相关(得先理解你才能继续往下)还是弱相关(知识拓展)从而提高你的阅读效率。我也会定期更新相关关联概念。

面试题

  • 说说你了解的如何分析 this 指向
  • 箭头函数的 this 了解吗
  • call/apply/bind/new 分别跟 this 的关联
  • 你能实现上述原生函数并讲解原理吗

这是干什么的?为什么要用?

this 是 javascript 中的一个关键字,它提供了一种更优雅的方式来 隐式“传递” 一个对象引用,因此可以将 API 设计得更加简洁并且易于复用

记住有些方法看不懂跳过没关系,文章不是线性的,你可以记录下不清楚的地方,先看下面的,未知永远存在,别害怕。

举个例子,首先我们使用 this,这可能是我们平时有意无意写的代码。

// 定义2个对象 你 和 我
let me = {
    name: "NeverMore"
};
let you = {
    name: "Reader"
};
// 名字转大写的方法
function identify() {
    return this.name.toUpperCase();
}
// 打印问好的方法
function speak() {
    let greeting = "Hello, I'm " + identify.call(this);
    console.log(greeting);
}
identify.call(me); // NEVERMORE
identify.call(you); // READER
speak.call(me); // Hello, I'm NEVERMORE 
speak.call(you); // Hello, I'm READER

很简单的功能吧,用途也一目了然,但你不用 this 的话这两方法需要这么写:

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm " + identify(context);
    console.log(greeting);
}
identify(you); // READER
speak(me); //hello, 我是 NEVERMORE

简单来说就是: 不使用this就需要给 identify() 和 speak() 显式传入一个上下文对象 context。

随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this则不会这样。当我们介绍对象和原型【关联概念】时,你就会明白函数可以自动引用合适的上下文对象有多重要

this 是个什么样的机制,到底如何分析它的指向

  • this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
  • 当一个函数被调用时,会创建一个执行上下文【关联概念】。这个记录会包含函数在哪里被调用(调用栈,执行栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。执行上下文的创建过程的一环就是 this Binding
  • this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被如何调用。
    首先,调用位置就是函数在代码中被调用的位置(而不是声明的位置)。 最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

另外我们可以用 console.trace() 来输出一个堆栈跟踪。

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    
    console.trace( "baz" );
    bar(); // <-- bar的调用位置
}

function bar() {
    // 当前调用栈是:baz --> bar
    // 因此,当前调用位置在baz中
    
    console.trace( "bar" );
    foo(); // <-- foo的调用位置
}

function foo() {
    // 当前调用栈是:baz --> bar --> foo
    // 因此,当前调用位置在bar中
    
    console.trace( "foo" );
}

baz();

很方便,跟我们人肉分析的一致。

对this的误解

在介绍this绑定规则之前,我们先看下对 this 我们通常的误解。

误解1: 指向函数自身

面试确实老听到有人答这个 ^-^

为什么需要从函数内部引用函数自身呢?

常见的原因是递归(从函数内部调用这个函数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。现在我们先来分析一下这个模式,让大家看到 this 并不像我们所想的那样指向函数本身。

function foo(num) {
    console.log("foo: " + num);
    // 记录 foo 被调用的次数
    this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log(foo.count);   // 0  ???

foo 被调用了多少次? console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然是 0。显然从指向函数本身来理解 this 是错误的。

那么如何来处理才能达到我们想的

// 创建另一个带有 count 属性的对象 
function foo(num) {
    console.log("foo: " + num);
    // 记录 foo 被调用的次数
    data.count++;
}
var data = {
    count: 0
};
var i;
for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次? 
console.log( data.count ); // 4

然而,这种方法显然回避了 this 的问题。

  • 解决方法二强制 this 指向 foo 函数对象
function foo(num) {
    console.log("foo: " + num);
    // 记录 foo 被调用的次数
    // 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo 
    this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo.call(foo, i);  // 使用 call(..) 可以确保 this 指向函数对象 foo 本身
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次? 
console.log( foo.count ); // 4	

误解2: 指向函数本身作用域

第二种常见的误解是, this 指向函数的作用域

这个问题有点复杂, 因为在某种情况下它是正确的, 但是在其他情况下它却是错误的。

需要明确的是, this 在任何情况下都不指向函数的词法作用域

在 JavaScript 内部, 作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“ 对象” 无法通过 JavaScript 代码访问, 它存在于 JavaScript 引擎内部。

思考一下下面的代码, 它试图(但是没有成功) 跨越边界, 使用 this 来隐式引用函数的词法作用域:

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log(this.a);
}
foo(); // ReferenceError: a is not defined

首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之后会解释原因。在下篇 [this的绑定规则]。

调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。 此外,编写这段代码的开发者还试图使用 this 联通 foo()bar() 的词法作用域,从而让 bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一个词法作用域内部的东西

每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

this的绑定规则

这边我们先留下对 this 的初步概念。关于如何分析 this 绑定,绑定规则,优先级等下篇补全。 javascript 中的this 的绑定规则

参考