this

什么是 this

为什么要用 this

this 可以当做一个隐式传递的上下文,避免每次都需要显示地传递。

function foo(context) {
    console.log(context.a)
}

var obj1 = { a: 1 }
var obj2 = { a: 2 }

foo(obj1)   // 1
foo(obj2)   // 2

上面的代码中,每次都显式地传入了一个 context 上下文。

但我们可以利用 this 来更加优雅的实现上列代码:

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

var obj1 = { a: 1 }
var obj2 = { a: 2 }

foo.call(obj1)  // 1
foo.call(obj2)  // 2

this 的误区

下面是两种常见的误区:

  • this 指向函数本身

  • this 指向函数的作用域

this 不指向函数自身的对象

虽然在 JavaScript 中,函数也是对象。但 this 并不是指向函数自身的对象。

例如,你想向函数对象内写入一个属性,来记录函数被调用的次数:

function foo() {
    this.count++
}

foo.count = 0

for (let i = 0; i < 10; i++) {
    foo()
}

console.log(foo.count)  // 0

在函数内部使用 this.count 并不能读取到函数对象内的 count 属性。

如果要实现类似效果,将 this.count 改成 foo.count

function foo() {
    foo.count++
}

foo.count = 0

for (let i = 0; i < 10; i++) {
    foo()
}

console.log(foo.count)  // 10

this 不指向函数所在的词法作用域

不要把词法作用域和函数自身对象搞混在一起。虽然他们很相似,但作用域“对象”无法通过 JavaScript 的代码访问,他只能由引擎自身访问。

this 是什么

this 是在运行时绑定的,而不是在编写的时候绑定的。

this 的绑定和函数声明的位置没有关系,只取决于函数调用的方式(在哪里被调用)

当函数在调用时,会创建一个活动记录。包含调用栈,函数调用的方式,传入的参数等。this 就是其中的一个属性。

绑定规则

默认绑定

默认绑定是在无法应用其他规则时的 fallback option

当应用了默认绑定规则,this 指向全局对象

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

var a = 0

foo()   // 0

上面的代码函数在调用时,应用了默认绑定规则,因此 this 指向全局对象

如果使用了严格模式,this 会被绑定为 undefined

function foo() {
    "use strict"
    console.log(this.a)
}

var a = 0

foo()   // TypeError

只有在函数内部应用严格模式才会限制默认绑定规则

相反,在严格模式下调用函数并不会影响默认绑定规则

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

var a = 0;

(function() {
    "use strict"
    foo();   // 0 在严格模式下调用函数不影响
})();

隐式绑定

函数的引用有上下文对象时,函数中的 this 会绑定到这个上下文对象上:

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

var obj = {
    foo: foo,
    a: 0
}

obj.foo()   // 0

foo 被对象 obj 引用,调用时 this 绑定在 obj

但是,如果存在引用链,那么以最后一层调用为主:

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

var obj1 = {
    foo: foo,
    a: 0
}

var obj2 = {
    a: 1,
    obj1: obj1
}

obj2.obj1.foo() // 0

绑定丢失

在某些情况下,隐式绑定可能会发生绑定丢失。取而代之应用了默认绑定规则

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

var obj = {
    foo: foo,
    a: 0
}

// bar 只是对 foo 的一个引用
const bar = obj.foo

bar()   // undefined

除此之外,在回调函数中也很容易丢失绑定:

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

function doFoo(fn) {
    // 这里的 fn 其实是对 foo 的引用
    fn()
}

var obj = {
    foo: foo,
    a: 0
}

// 虽然传入给回调函数看起来有隐式绑定,但其实实质上传的只是 foo 的引用
doFoo(obj.foo)  // undefined

即便是传入给内置的回调函数,如 setTimeout 也会发生绑定丢失:

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

var obj = {
    foo: foo,
    a: 0
}

setTimeout(obj.foo, 0) // undefined

显式绑定

我们可以在一个对象内引用一个函数,并用这个对象的属性来调用该函数,从而使得 this 隐式绑定到该对象上。但是我们也可以使用显示绑定,将 this 绑定到某一特定对象上。

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

var obj = {
    a: 0
}

foo.call(obj)   // 0

如果传入的是一个值类型,那么 call() 方法会把它包装成一个对象。如:new String(), new Boolean()...

call 接受一个个单独的参数,apply 接受一个参数数组

硬绑定

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

var obj = {
    a: 0
}

var bar = function() {
    foo.call(obj)
}

// 无论如何调用 bar,this 始终绑定为 obj
bar()               // 0
setTimeout(bar, 0)  // 0
bar.call({})        // 0

每次调用 bar 的时候,在他的内部都会调用一次显示绑定,因此 obj 都会被绑定在 this 上面。

new 绑定

JavaScript 中的 new 关键字只是对函数的构造调用,而非构造函数。不要把它和面向对象里的实例化混为一谈

当使用 new 调用函数时,将会发生以下四个步骤:

  1. 创建一个全新的对象

  2. 将这个对象连接到函数的原型对象上

  3. this 绑定到这个对象上

  4. 如果函数自身没有返回对象(返回值类型不算),则返回这个对象(即 this ,它目前就是指向该对象)

new 在 JavaScript 中只是一种函数调用的方式,并不是什么实例化,千万不要搞混!

绑定优先级

如果想要明确一个 this 的绑定,按照以下优先级顺序进行判断:

  1. 函数是否存在 new 构造调用

  2. 是否使用 call bind 进行显式绑定

  3. 是否被某一个上下文对象调用,如:obj.foo()

  4. 上述规则都不满足时,使用默认绑定;严格模式下绑定到 undefined,否则绑定至全局对象

例外

  • null undefined 传入 call apply bind 中会被忽略,转而使用默认绑定

    在某些不需要关心this 是什么的时候特别有用

  • 在间接引用的情况下应用的是默认绑定规则,而不是隐式绑绑定规则

如果想在 call apply bind 中传入 null undefined 来应用默认绑定规则,会有可能造成对全局对象的污染,函数中的某些操作可能会修改全局对象中的属性。

一种更加安全的做法是创建一个 DMZ 对象:

function foo() {}

// Object.create(null) 不会创建 Object.prototype,它比 {} 更空
const DMZ = Object.create(null)

// 你可以用这个 DMZ 来代替 undefined / null,从而实现更安全的调用
foo.call(DMZ)

箭头函数

箭头函数的 this 不使用上述 4 种绑定规则。根据外层的词法作用域所决定。

具体来说,箭头函数内的 this 会继承外层函数的 this 绑定:

箭头函数压根没有 this ,他只是继承

function foo() {
    // 箭头函数内的 this 绑定至 foo() 中的 this
    return () => {
        console.log(this.a)
    }
}

var obj = {
    a: 0
}

const bar = foo.call(obj)

bar()   // 0

如果不使用箭头函数,上述代码在调用 bar() 时,其实会应用默认绑定规则:

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

var obj = {
    a: 0
}

var a = 'opps, global'

const bar = foo.call(obj)

// bar 中的 this 绑定到了全局对象上
bar()   // opps, global

又或者,在回调函数中,如果在不使用箭头函数的情况下,很容易在不知不觉中应用默认绑定:

function foo() {
    setTimeout(function() {
        console.log(this.a)
    }, 0)    
}

var obj = {
    a: 0
}

var a = 'opps, global'

// 你以为给 foo 绑定了 obj,但其实回调函数在调用时已经丢失了 this,转而应用了默认绑定规则!
foo.call(obj)    // oops, global

在没有 ES6 箭头函数时,可以使用以下方法解决:

function foo() {
    // 在函数作用域内捕获一个 this, 使用闭包机制
    var self = this
    setTimeout(function() {
        console.log(self.a)
    }, 0)    
}

var obj = {
    a: 0
}

var a = 'opps, global'

foo.call(obj)   // 0

在对象内使用箭头函数

通常,我们会在函数内使用箭头函数;这个箭头函数的 this 会捕获所在函数的词法作用域。

但是,如果一个对象的某个属性持有一个箭头函数呢?就像这样:

var a = 'global'

var obj = {  // 并不创建一个作用域
  a: 'obj',
  foo: () => console.log(this.a)
}

obj.foo() // 'global'

Last updated