Skip to content

this

概述

简单地说,this 就是属性和方法当前所在的对象。函数可以在不同的上下文里运行,为了在运行时能够知道当前的运行环境,所以设计了 this

js
function f() {
  console.log(this.x);
}
var x = 1;
var obj = {
  f: f,
  x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2

使用场合

  1. 全局调用: this -> 全局对象
  2. 作为对象方法调用: this -> 调用该方法的对象
  3. 作为构造函数调用 (new): this -> 新创建的实例对象
  4. call、apply、bind 调用: this -> 明确指定的对象
  5. DOM 事件处理: this -> 触发事件的 DOM 元素
  6. 箭头函数: this -> 继承其父级作用域的 this

加强this指向理解的一些案例

案例一:将对象的函数赋值给新变量会发生什么

js
var obj = {
    name: 'a',
    foo: function () {
        console.log(this);
    }
};
obj.foo() // {name: 'a', foo: ƒ}

const f = obj.foo
f() // window

解析如下: 设: function () {console.log(this);} 存在堆中的一个位置(0xaaaa) {name: 'a', foo: 0xaaaa} 存在堆中的一个位置(0xbbbb)

我们用obj.foo()相当于在(0xbbbb)这块内存找到了(0xaaaa),然后才调用函数,也就是我们是通过 obj 去调的 foo ,那么函数的运行上下文就是(0xbbbb)这块内存,也就是 obj 对象。

const f = obj.foo 就相当于,让变量f指向(0xbbbb),之后我们再调用的时候,就是直接调用函数,直接就在全局上下文上调用了。


案例二:多层对象嵌套下的this

所谓多层对象,实际上不是嵌套关系,而是指向关系

js
var a = {
    p: 'Hello',
    b: {
        name: 'b',
        m: function () {
            console.log(this, this.p);
        }
    }
};
a.b.m() // {name: 'b', m: ƒ}  undefined

const b = a.b
b.m() // {name: 'b', m: ƒ}  undefined

const m = b.m
m()  // window  undefined

解析同上,注意到:

  • function () {console.log(this, this.p);} 是内存中一块独立区, 设地址为'0x1111'
  • { name: 'b', m: '0x1111' } 同样是内存中的一块独立区,设地址为'0x2222'
  • { p: 'Hello', b: '0x2222' } 同样是内存中的一块独立区 如果调用函数时,是通过某对象去找到函数再去调用的,那内部的 this 就指向这个对象。如果是把函数取出来赋值给新变量的,那直接调用,this 指向全局

案例三:多层函数嵌套下的this

无论何时何地,声明函数,也是在一个内存中独立区域存放了函数,函数是独立的,函数的this仍然是取决于运行时!

即便是在一个函数内部定义函数,这个内部函数和外部函数一样,在内存中是独立地占据一块空间的,和在外侧定义大差不差,唯一区别就在于内部定义的这个函数,外部没有他的引用罢了。

js
const o = {
    a: function () {
        console.log(this)

        function b() {
            console.log(this)
        }
        b()
    }
}

o.a() //  {a: ƒ},  window

const a = o.a
a() // window, window

注意,区分于闭包

js
const o = {
    a: function () {
        console.log(this)
        const that = this

        function b() {
            console.log(that)
        }
        b()
    }
}

o.a() //  {a: ƒ},  {a: ƒ}

const a = o.a
a() // window, window

案例三:定时器回调函数中的this

js
var x = 1;
var obj = {
    x: 2,
    y: function () {
        console.log(this.x);
    }
};
setTimeout(obj.y, 500); // 1

setTimeout(function () {
    obj.y(); // 2
}, 1000);

setTimeout(() => {
    obj.y() // 2
}, 1500);

解析如下:setTimeout的第一个参数函数不是立即执行的,如果在执行时是通过实例调用的形式,那么this指向该实例。

第一种情况,在执行时并不是通过实例调用的, 可理解为在传参的时候直接把函数地址给传进去了,这个情况就相当于: const fy = obj.y 然后 fy()

第二、第三种情况,执行时是执行的匿名函数,匿名函数内部通过实例调用的形式执行函数。

注意事项

  • 避免多层嵌套 this, 提升可读性
  • 避免数组forEach map 等函数中使用this
  • 避免回调函数中使用 this

绑定 this

call

应用:有时实例对象上的方法被重写了,用call来调用对象的原型上的原生方法

js
// 手撕 call

Function.prototype.myCall = function(context, ...args){

    const fn = this
    const ctx = context || window

    // 1. 处理 null
    if (ctx === null || ctx === undefined) {
        ctx = window;
    }
    // 2. 处理原始类型, 使用Object()包装原始类型
    else if (typeof ctx !== 'object' && typeof ctx !== 'function') {
        ctx = Object(ctx); 
    }

    const p = Symbol()
    ctx[p] = fn
    const res = context[p](...args)
    delete ctx[p]
    return res
}

apply

不再多说,和call唯一区别就是参数是以数组形式传入

bind

bind相当于是把内部的this锁死到某个context。 常见的错误是,将包含this的方法直接当作回调函数,一般是先bind拿到新函数,再绑定给回调事件。

js
// 手撕bind,合理利用call
Function.prototype.bind = function(context, ...args){

    const fn = this
    return function(...params){
        return fn.call(context, [...args, ...params])
    }
}