JavaScript 中 this 的秘密

472 阅读5分钟

this介绍

在 JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象下运行,而 this 就是函数运行时的环境。

JavaScript 支持运行环境动态切换,也就是说 this 的指向是动态的,在函数定义的时候是确定不了的,只有函数执行的时候才能确定,实际上 this 的最终指向的是那个调用它的对象。

this 动态性背后的原理

在 JavaScript 中,变量的实质是一个指向原始对象的内存地址,原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象,其中[[value]]对应属性的值。

当属性的值是一个函数,此时函数又单独保存在内存里,属性的值又是该函数的地址。

由于函数是独立于原始对象单独保存的,因此可以在不同的环境中执行,而 this 的设计就是为了方便在函数体内部直接获得当前的运行环境。

bg2018061803.png

不同场景下 this 指向

全局环境

在浏览器全局环境下,this 始终指向全局对象(window), 无论是否严格模式;

普通函数,非严格模式下,指向 window。严格模式下,指向 undefined。

构造函数

构造函数中的 this,指的是实例对象。

window.identity = "The Window"

function Obj() {
  this.identity = "My Object"

  this.getIdentityFunc = function () {
    console.log(this.identity)
  }

  this.getIdentityFunc1 = () => {
    console.log(this.identity)
  }
}

const obj = new Obj()  // new 关键字可以改变 this 的指向,将 this 指向对象 obj
obj.getIdentityFunc()  // My Object
obj.getIdentityFunc1() // My Object

补充:new 关键字会创建一个空的对象,然后会自动调用一个函数 apply 方法,将 this 指向这个空对象,这样的话函数内部的 this 就会被这个空的对象替代。

对象方法

对象没有自己的上下文,对象的方法里面包含 this,this 指向就是方法运行时所在的对象。

var obj ={
  foo: function () {
    console.log(this);
  }
};
obj.foo() // obj

// 注意1:下面运行环境已经变成全局环境
var bar = obj.foo; bar() // window
(obj.foo = obj.foo)() // window
(false || obj.foo)() // window
(1, obj.foo)() // window

// 注意2: 对象嵌套不会向上找
var a = {
  p: 'Hello',
  b: {
    m: function() {
      console.log(this.p);
    }
  }
};
a.b.m() // undefined,因为实际调用 m 的是 b,b 中不存在 p 这个属性

数组方法

数组的方法,如mapforeach,允许提供一个函数作为参数。这个函数内部不应该使用this。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) { // 执行 o.f() 时,this 指向 o
      console.log(this.v + ' ' + item); // this 是 window,取不到 p 的值
    });
  }
}

// 修改1: 使用中间变量固定
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    var that = this;
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    });
  }
}

// 修改2: 利用第二个参数,固定运行环境
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    }, this);
  }
}

// 修改3: 使用箭头函数
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach((item) => {
      console.log(that.v+' '+item);
    });
  }
}

原型链

原型链中的方法的 this 仍然指向调用它的对象。

var o = {
  f : function(){ 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5

箭头函数

箭头函数没有自己的 this,箭头函数的 this 就是上下文中定义的 this。

var identity = "window"
var obj = {
    identity: 'object',
    testIdentity: this.identity, // 指向对象所在上下文
    getIdentityFunc: () => {
        console.log(this.identity)
    }
}
console.log(obj.testIdentity)  // window
obj.getIdentityFunc()  // window
  • 箭头函数不能用做构造函数,因为没有它自己的this
  • call / apply / bind 方法对于箭头函数来说只是传入参数,无法改变 this 的指向

DOM 事件处理函数

事件处理函数内部的 this 指向触发这个事件的 DOM 元素对象。

定时器

对于定时器内部的回调函数的 this 指向全局对象 window。

注意:定时器、数组方法等,通过回调的方式传入函数,函数中如果有 this,this 的指向很可能会出错。一般可以通过 bind 方法,固定执行环境。有些直接支持 this 作为参数传入。

如何改变 this 指向

JavaScript 提供了 call、apply、bind 这三个方法,来切换/固定 this 的指向。

func.call(thisValue, arg1, arg2, ...)
func.apply(thisValue, [arg1, arg2])
func.bind(thisValue, arg1, arg2)()
  • call 的参数一个个添加,可用来调用对象的原生方法
  • apply 接收一个数组作为函数执行时的参数,方便调用数组的原生方法
  • bind 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数,其中arg1, arg2... 是当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

如何实现 call

将函数的引用 this 赋值给 call 的第一个参数 obj,然后通过 obj 引用调用函数。

Function.prototype.myCall = function() {
    let obj = arguments[0] || window;   // 如果没有传this参数,this将指向window
    let args = [...arguments].slice(1); // 获取第二个及后面的所有参数(arg是一个数组)
    let fn = Symbol();                  // Symbol属性来确定fn唯一
    obj[fn] = this;                     // this指向调用myCall的函数(代指a函数),同时将a函数的引用赋值给obj的fn属性。此时,当a函数调用的时候就是指向obj的了
    let res = obj[fn](...args);         // a函数的引用调用,指向obj,也就是传入的对象
    delete obj[fn];                     // 不能增加obj的属性,所以要删除
    return res;                         // 如果a函数有返回值这里就有返回值,如果a函数没有返回值,同样的这里也没有
}

如何实现 apply

类似 call,区别在于参数格式。

Function.prototype.myApply = function() {
    let obj = arguments[0] || window;   // 如果没有传this参数,this将指向window
    let args = arguments[1];            // 获取参数(arg是一个数组)
    let fn = Symbol();                  // Symbol属性来确定fn唯一
    obj[fn] = this;                     // this指向调用的函数
    let res = obj[fn](...args);         // a函数的引用调用,指向obj,也就是传入的对象
    delete obj[fn];                     // 不能增加obj的属性,所以要删除
    return res;                         // 返回a函数的返回值
}

如何实现 bind

同 call 和 applay 不同的是,bind返回一个函数,不是立即调用。

Function.prototype.myBind = function (object) {
    let obj = object || window;	         // 如果没有传this参数,this将指向window
    let fn = this;                       // this指向调用的函数
    let arg = [...arguments].slice(1);	 // 获取第二个及后面的所有参数(arg是一个数组)
    const fBind = function () {
        /* 如果当前函数执行中的this是fBind的实例,说明是fBind被new了,那么当前this就是函数的实例,否则是obj */
        let o = this instanceof fBind ? this : obj;
        fn.applay(o, args.concat(...arguments));
    }
    
    // 考虑实例化后对原型链的影响
    let temp = function() {};
    temp.prototype = fn.prototype;
    fBind.prototype = new temp();
    
    return fBind;                        // 返回一个函数
}

参考

  1. JavaScript 的 this 原理
  2. JavaScript this 详解