this全面解析

487 阅读3分钟

一、基本概念

  • 即不指向自身,也不指向词法作用域
  • this 实际上是在函数被调用时发生绑定,它指向什么取决于函数在哪里被调用

例如:

// 全局作用域中直接调用
var name = 'hello world'
function foo() {
    consoel.log(this.name)
}
foo() // 'hello world' this 指向window,因为函数foo()是在全局作用域下调用,相当于 window.foo()

// 作为方法调用
function foo() {
    console.log(this.name)
}
var obj = {
    name: '小明'foo: foo
}
obj.foo() // '小明', 此时 this 指向了对象 obj,因为函数 foo() 是作为了对象 obj 的一个方法调用的

二、调用位置

在理解 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的调用位置

三、绑定规则

3.1默认绑定

function foo() {
    console.log(this.a)
}
var a = 2
foo() // 2

foo()是直接在全局作用域下调用的,this 指向的是全局作用域,然后在全局作用域中有一个声明的变量 a, 因此 this.a 访问到了全局作用域中的变量 a

3.2隐式绑定

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

3.2.1 隐式丢失

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

function foo() {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo
}
var bar = obj.foo // 函数别名
var a = 'hello world'
bar() // 'hello world'

虽然barobj.foo的一个引用,但实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此它应用了默认绑定。

传入回调函数时

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    // fn 其实引用的是 foo
    fn(); // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

参数传递其实也是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样

3.3 显示绑定

使用 call()、apply()、bind()都可以显示的绑定this,看下面代码:

function foo() {
    console.log(this.a)
}
var obj = { a:2 }
foo.call( obj ) // 2
  • 通过foo.call(...)我们可以在调用foo时强制把this指向Obj
  • 如果传入了一个原始值(string、number、bolean)来当作this的绑定对象,这个原始值会被转化为对象形式。通常称为“装箱”

3.3.1 硬绑定

使用call、apply

function foo() {
  console.log(this.a)
}
var obj = {
  a: 2
}
var bar = function() {
  foo.call(obj)
}
bar() // 2
setTimeout(bar, 100) // 2
bar.call(window) // 2

使用bind()函数绑定this

function foo(something) {
    console.log(this.a, something)
    return this.a + something
}
var obj = {
    a: 2
}
var bar = foo.bind(obj)
bar(3)

3.3.2 new绑定

使用new操作符调用构造函数会经历下面4个步骤:

  • 创建一个新对象
  • 将构造函数的作用域赋值给新对象(this指向这个新对象)
  • 执行构造函数中的代码(为新对象添加属性和方法)
  • 返回这个新对象
function foo(a) {
    this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

使用 new 来调用foo()时,foo()函数中的 this 会绑定到 bar 对象上,因此,bar.a 实际上就是函数 foo() 里面的 this.a

四、绑定优先级

隐式绑定和显示绑定的优先级哪个更高?

function foo() {
    console.log(this.a)
}
let obj1 = {
    a: 2,
    foo: foo
}
let obj2 = {
    a: 3,
    foo: foo
}
obj1.foo.call(obj2) // 3
obj2.foo.call(obj1) // 2

上面代码中可以看出,显示绑定的优先级更高

五、小结

如果要判断一个运行中的和桉树的this绑定,需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面四条规则来判断this的绑定对象

  • new调用?绑定到新创建的对象
  • 由call或者apply(或者bind)调用?绑定到指定的对象
  • 由上下文对象调用?绑定那个上下文对象
  • 默认:在严格模式下绑定到undefined,否则绑定到全局对象