你不知道的this!

118 阅读6分钟

你真正了解this吗?

this的定义

  • this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。

为什么要使用this

  • this提供了一种更优雅的方式来隐式"传递"一个对象引用,因此可以将API设计得更加简洁并易于复用。

this的作用域

  • this在任何情况下都不指向函数的词法作用域,在JavaScript内部,作用域确实和对象类似,可见的标识符都是它的属性,但是作用域“对象”无法通过JavaScript代码访问,它存在于JavaScript引擎内部

this到底是什么

  • 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。
  • this就是记录的其中一个属性,会在函数执行的过程中用到。

总结:

  • this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

this全面解析

调用位置

  • 调用位置是指函数在代码中被调用的位置(而不是声明的位置)。

  • 通常来说,寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有那么简单,因为某些编程模式可能会隐藏真正的调用位置

  • 最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中··

function baz () {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log('baz')
    bar() // bar的调用位置
}

function bar () {
    // 当前调用栈是 baz——>bar
    // 因此,当前调用位置在baz中
    console.log('bar')
    foo() // foo的调用位置
}

function foo () {
    // 当前调用栈是 baz——>bar——>foo
    // 因此,当前调用位置在bar中
    console.log('foo')
}
baz() // baz的调用位置

绑定规则

默认绑定

  • 独立函数调用
<script type="text/javascript">
    // "use strict";
    function foo() {
        console.log(this.a);
    }
    var a = 2;
    foo();
</script>
  • 非严格模式下:因为这里的函数调用应用了this的默认绑定,因此this在这里指向全局对象(Window )

  • 严格模式下:全局对象将无法使用默认绑定,因此this会指向undefined

细节:虽然this的绑定规则完全取决于调用的位置,但是只有在非严格模式下时,默认绑定才能绑定到全局对象,严格模式下与函数的调用位置无关,有时候你可能会使用到一些第三方的,其严格程度和你代码有所不同,因此一定要注意这类兼容性细节。

隐式绑定

  • 即函数调用的位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种方法可能会造成一些误导。
// 默认是在非严格模式下
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo,
};
obj.foo() // 2
  • 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的
  • 注意:对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
function foo() {
    console.log(this.a);
}
var obj2 = { a: 42, foo: foo };
var obj1 = { a: 2, obj2: obj2 };
obj1.obj2.foo(); // 42

隐式丢失

  • 一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式

显示绑定

  • JavaScript中绝大多数函数以及你自己创建的所有函数都可以用call()和apply方法,它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为显示绑定
function foo() {
    console.log(this.a);
    console.log(this); // 指向obj对象
}
var obj = {
    a: 2,
};
foo.call(obj);
  • 如果你传入了一个原始值 (字符串类型、布尔类型、数字类型) 来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String()、new Boolean()或者new Number())这通常被称为“装箱”

硬绑定

function foo() {
    console.log(this.a);
    console.log(this); // 指向obj对象
}
var obj = {
    a: 2,
};
var bar = function () {
    foo.call(obj);
};
bar();
setTimeout(bar, 100);
// 硬绑定的bar不可能再修改它的this
bar.call(window);
  • 我们创建了函数bar(),并在它内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一个显示的强制绑定,因此我们称之为硬绑定。

硬绑定的经典应用场景

function foo(something) {
    console.log(this);
    console.log(this.a, something);
    return this.a + something;
}
var obj = {
    a: 2,
};
var bar = function () {
    return foo.apply(obj, arguments);
};
var b = bar(3); // 2 3
console.log(b); // 5

bind方法

function foo(something) {
    console.log(this);
    console.log(this.a, something);
    return this.a + something;
}
var obj = {
    a: 2,
};
var bar = foo.bind(obj);
var b = bar(3);
console.log(b);
  • bind()会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数

new绑定

new操作符做了什么事情?

  • 创建(或者说构造) 一个全新的对象
  • 这个新对象会被执行[[原型]]连接(将新对象与构造函数通过原型链连接)
  • 这个新对象会绑定到函数调用的this
  • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a) {
    console.log(this); // 指向 new出来的foo对象
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
  • 使用new来调用foo()时,我们会构造一个新对象把它绑定到foo()调用中的this上。new是最后一种可以影响函数调用时this绑定行为的用法,我们称之为new绑定

总结

  • 如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象
  1. 由new 调用? 绑定到新创建的对象
  2. 由call或apply(或者bind)调用?绑定到新指定的对象
  3. 由上下文对象调用?绑定到那个上下文对象
  4. 默认:在严格模式下绑定到Undefined,否则绑定到全局对象(Window)
注意:有些调用可能在无意中使用默认绑定规则。如果想更 "安全" 的忽略this绑定,你可以使用一个DMZ对象,比如 ø =Object.create(null),以保护全局对象。  

Es6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论绑定到什么)