【前端进阶】如何正确判断this的指向?(注意区分严格模式和非严格模式)

背景:由高级前端工程师@刘小夕 在github上发起的一个开源项目:每个工作日发布一个前端相关的问题,每周进行一次汇总。我觉得很不错,也参与其中,如果你也感兴趣,可以一起参与,项目地址:https://github.com/YvetteLau/Step-By-Step

this对象是在运行时基于函数的执行环境绑定的,所以要正确判断this的指向,要看它是在什么场景下使用。

全局环境

无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this都执行全局对象

// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true

this.a = 10;
console.log(window.a); // 10

函数(运行内)环境

在函数内部,this的指向取决于函数被调用的方式。

1.简单调用

1.1 非严格模式:

this默认指向全局对象。

function f1() {
  return this;
}

f1() === window; // true

1.2 严格模式:

this默认指向undefined

function f2() {
  "use strict";  // 这里是严格模式
  return this;
}

f2() === undefined;  // true

2.函数通过call()apply()调用

当一个函数在其主体中使用this关键字时,可以通过使用函数继承自Function.prototypecallapply方法将this的值绑定到调用中的特定对象。

// 将一个对象作为call和apply的第一个参数,this会被绑定到这个对象。

var obj = {
  a: 'Custom'
}

var a = 'Global';

function whatsThis(arg) {
    return this.a;  // this的值取决于函数的调用方式
}

whatsThis();  // 'Global'
whatsThis.call(obj);  // 'Custom'
whatsThis.apply(obj); // 'Custom'

call()apply还可以有其他用法,再看一个例子:

function add(c, d) {
    return this.a + this.b + c + d;
}

var o = {
    a: 1,
    b: 3
}

// 第一个参数是作为`this`使用的对象
// 后续参数作为参数传递给函数调用
add.call(o, 5, 7);  // 1 + 3 + 5 + 7 = 16

// 第一参数也是作为`this`使用的对象
// 第二个参数是一个数组,数组里的元素用作函数调用中的参数
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

使用call()apply()函数的时候要注意,如果传递给this的值不是一个对象,JavaScript会尝试使用内部ToObject操作将其转换为对象。

3.bind方法

ECMAScript5引入了Function.prototype.bind。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久的被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

function f() {
    return this.a;
}

var g = f.bind({
    name: 'Jack'
});

console.log(g()); // Jack

// bind只生效一次
var h = g.bind({
    name: 'Tom'
});

console.log(h()); // Jack

var o = {
    a: 37,
    f: f,
    g: g,
    h: h
}

console.log(o.f(), o.g(), o.h()); // 37, Jack, Jack

4.作为对象的方法

当函数作为对象里的方法被调用时,它们的this是调用该函数的对象。

下面的例子中,当o.f()被调用时,函数内的this将绑定到o对象。

var o = {
    prop: 30,
    f: function () {
        return this.prop;
    }
}

console.log(o.f()); // 30

请注意:这样的行为,根本不受函数定义方式或位置的影响。

在前面的例子中,我们在定义对象o的同时,将函数内联定义为成员f。但是,我们也可以先定义函数,然后再将其附属到o.f。这样做会导致相同的行为:

var o = {
    prop: 30
}

function independent() {
    return this.prop;
}

o.f = independent;

console.log(o.f()); // 30

这表明函数是从of成员调用的才是重点。

同样,this的绑定只受最靠近的成员的引用的影响。在下面这个例子中,我们把一个方法g当做对象o.b的函数调用。在这次执行期间,函数中的this将指向o.b。事实证明,这与他是对象o的成员没有多大关系,最靠近的引用才是最重要的。

o.b = {
    g: independent,
    prop: 42
}

console.log(o.b.g()); // 42

还有一点需要注意的,匿名函数的执行环境具有全局性,因此其this对象通常指向window。看下面的例子:

var name = "The Window";

var obj = {
    name: "My Object",
    getNameFunc: function () {
        return function () {
            return this.name;
        }
    }
}

alert(obj.getNameFunc()());  // "The Window"(在非严格模式下)

这边如果我们想让返回值为My Object,可以怎么做呢?把外部作用域的this对象保存在一个闭包能够访问的变量里,就可以让比闭包访问该对象了,看下面的例子:

var name = "The Window";

var obj = {
    name: "My Object",
    getNameFunc: function () {
        var that = this;
        return function () {
            return that.name;
        }
    }
}

alert(obj.getNameFunc()());  // "My Object"

5.作为构造函数

当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。

注意:虽然构造器返回的默认值是this所指的那个对象,但它仍可以手动返回其他的对象(如果返回值不是一个对象,则返回this对象)。

function C() {
    this.a = 30;
}

var o = new C();
console.log(o.a); // 30

function C2() {
    this.a = 30;
    return { a: 38 };
}

var o2 = new C2();
console.log(o2.a); // 38

6.箭头函数

引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。

所以箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this

还有一句话,我觉得非常重要:就是箭头函数中的this是在定义函数的时候绑定的,而不是在执行函数的时候绑定。

我们看个例子:

var age = 40;

var o2 = {
  age: 30,
  f1: () => {
    return this.age;
  }
}

console.log(o2.f1()); // 40

如果是普通的函数,这边是输出30才对,但是由于箭头函数中的this是定义时候绑定的,就是this是继承自父执行上下文中的this,比如这里的箭头函数本书所在的对象为obj,而obj的父执行上下文是window,因此这里的this.age实际上表示的是window.age,所以输出40

箭头函数有几个使用注意点

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当做构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用reset参数替代。
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。

this使用注意点

1.避免多层this

由于this的指向是不确定的,所以切勿在函数中包含多层的this

var o = {
    f1: function () {
        console.log(this);

        var f2 = function () {
            console.log(this);
        }
    }
}

o.f1();
// Object
// Window

上面代码包含两层this,结果运行后,第一层指向对象o,第二层指向全局对象,因为实际执行的是下面的代码:

var temp = function () {
    console.log(this);
}

var o = {
    f1: function () {
        console.log(this);
        var f2 = temp();
    }
}

一个解决办法就是在第二层改用一个指向外层this的变量。

var o = {
    f1: function () {
        console.log(this);
        var that = this;
        var f2 = function () {
            console.log(that);
        }
    }
}

o.f1();
// Object
// Object

2.避免数组处理方法中的this

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

var o = {
    v: 'hello',
    p: ['a1', 'a2'],
    f: function () {
        this.p.forEach(function (item) {
            console.log(this.v + ' ' + item);
        })
    }
}

o.f();
// undefined a1
// undefined a2

上面代码中,forEach方法的回调函数中的this,其实是指向window对象,因此取不到o.v的值。原因跟上一段的多层this是一样的,就是内层的this不指向外部,而是指向顶层对象。

解决这个问题的一种方法是,跟上面一个一样,使用中间变量固定this

另一种方法是将this当作forEach方法的第二个参数,固定它的运行环境。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function () {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}

o.f();
// hello a1
// hello a2

3.避免回调函数中的this

回调函数中的this往往会改变指向,最好避免使用。

var o = new Object();
o.f = function () {
    console.log(this === o);
}

// jQuery的写法
$('#button').on('click', o.f);

上面代码中,点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的DOM对象,因为f方法是在按钮对象的环境中被调用的。

参考

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this#%E8%AF%AD%E6%B3%95

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions

http://es6.ruanyifeng.com/#docs/function#%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0

https://wangdoc.com/javascript/oop/this.html