你真的懂js中的this?

744 阅读6分钟

1. 为什么要用this?

thisjs中最为复杂的机制之一。大部分开发者写了很久的js代码都可能没有主动使用过this,或者遇到就直接"忽略",但其实this本质上提供了一种更加优雅的方式来传递对象引用,可以使代码更加简洁和易于复用,所以全面了解this是有必要的。

2. this的误解

在学习this之前,先来说说它的两个误区。第一个就是this并不是指代本身,这里是因为用了词法作用域才会出现这样的理解;第二个就是this的绑定和声明没有任何关系,它取决于调用时候的条件。

3. this的理解

this的绑定是在调用的时候确定下来的,和声明没有任何关系。

3.1. this绑定规则(4个)

this绑定规则有四个,分别是:默认绑定,隐式绑定,显式绑定,new绑定。

3.1.1. 默认绑定

独立函数调用,无法应用其他规则的时候使用的默认规则。这个规则根据会根据严格模式非严格模式绑定不一样的值,严格模式use strict绑定的是undefined,而非严格模式绑定的是全局对象(浏览器就是window)。

// 非严格模式
function foo() {
    console.log(this);
}
foo(); // window


// 严格模式
function bar() {
    'use strict'
    console.log(this);
}
bar(); // undefined

3.1.2. 隐式绑定

第二个this绑定的规则就是隐式绑定,其实就是调用的位置是否有上下文对象。来看下面这个例子:

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

var obj = {
    a: 1,
    foo: foo
};

obj.foo(); // 1

这里调用foo函数的方式是通过obj.foo(),使这个函数被调用的时候加上了obj这个上下文对象,隐式绑定这个规则会把函数中的this绑定到obj这个对象上,因此this.aobj.a是一样的。

多个层级引用

大多数情况下,调用函数的层级都不是简单的一层,如果出现多层的话,函数中的this会绑定在哪一个对象上?例如:obj1.obj2.obj3.foo(),可以看下面的例子:

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

var obj2 = {
    a: 2,
    foo: foo
};

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

// 多个对象引用函数的this会绑定在上一层或者最后一层的属性上
obj1.obj2.foo(); // 2

这个例子中的foo中的this绑定在obj2对象上,也就是说函数中的this绑定在上一层或者最后一层对象属性上。

隐式丢失

隐式绑定还有一个比较常见的问题就是this绑定时会丢失绑定对象,这样this会应用默认绑定规则,绑定到全局对象或者undefined上,这取决于是否严格模式(use strict)。

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

var obj = {
    a: 1,
    foo: foo
};

var f = obj.foo; // 这里f是obj.foo的引用,引用的是foo函数本身
f(); // undefined; 

上面例子中的fobj.foo的引用,也就是foo函数本身,当执行f函数时,相当于直接执行f()foo函数中的this应用默认绑定规则,把当前函数内的this绑定到全局对象上(window)。

这里还有一个关于setTimeout经典的例子:

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

var obj = {
  a: 'obj的a属性',
  foo: foo
}

var a = '全局变量a';

setTimeout(obj.foo, 0); // '全局变量a'

这里传入setTimeout的其实是obj.foo引用的函数本身,也就是foo函数,那么这个时候也是应用默认绑定规则,函数里的this会绑定到全局对象上。其实回调函数丢失this绑定是非常常见的,所以在处理回调函数的this绑定时候要注意。

3.1.3. 显式绑定

显式绑定一般使用的就是applycallbind这几个方法,其中applycall都是调用函数并且把它的this绑定在方法的第一个参数上,这两个方法的区别在于其他参数上。而bind也会绑定this到第一个参数对象上,但并不执行该函数。

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

var obj = {
  a: 'obj的a属性'
};

foo.apply(obj); // 'obj的a属性'

这里使用foo.apply(obj)显式地绑定foo函数里面的thisobj对象上,foo函数里的this.a也就是obj.a

apply和call

applycall通过第一个参数影响函数里面的this绑定,但有时候第一个参数并不是普通的对象。

function foo() {
  console.log(this.toFixed(2));
}

foo.apply(3); // 3.00

apply方法里面的3明显不是对象,那在apply执行后,函数里面的this会被转换成new Number(3),所以最终执行的就是new Number(3).toFixed(2)输出结果是3.00applycall如果第一个参数传入原始值(数字,字符串,布尔值)来绑定this对象,那么this绑定的当前值是(new Number()new Boolean()new String()) 。

还有一种特殊情况就是,applycall第一个参数传入nullundefined,那么函数中的this绑定被忽略,使用默认绑定规则,this绑定在全局对象或者undefined(这个取决于是否严格模式)。

bind

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

var obj = {
  a: 'obj的a属性'
};

var f = foo.bind(obj);
f('f的参数'); // obj的a属性 f的参数

bind()会返回一个新的函数,并且会为函数的this绑定到第一个参数上。

3.1.4. new绑定

js中,构造函数其实是使用new操作符时调用的函数,构造函数并不属于某个类,实际上也不会实例化一个类,构造函数就是一个使用new操作符调用的普通函数。

function Foo(a) {
  this.a = a;
}

var f = new Foo(1);
console.log(f.a); // 1

这里的new操作符调用Foo()函数时,内部会创建一个新的对象并把它绑定到Foo()调用的this上,如果这个函数没有其他返回值对象,那么函数就会自动返回这个新对象。

3.2. 箭头函数的this

上面介绍的this绑定的四个规则已经可以包含所有“正常“的函数。但是ES6中的箭头函数却无法使用这些规则。箭头函数的this是根据外层作用域来决定的。来看看箭头函数的词法作用域:

function foo() {
  return () => {
    console.log(this);
  }
}

var obj = {
  a: 'obj的a属性'
};

var f = foo.call(obj);
f(); // {a: 'obj的a属性'}

上面的f()输出的并不是window全局对象,而是obj对象,因为在调用foo函数的时候使用了call方法显式绑定到obj属性上,而foo函数的返回值是箭头函数,那么这个返回的箭头函数里面的this绑定就是外层foo函数执行时绑定的obj对象上,所以这里的箭头函数的this最终绑定在obj对象上。