你不知道的this全面解析

290 阅读3分钟

this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

绑定规则

默认绑定

最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

思考一下下面的代码:

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

var a = 2;
foo() // 2

你应该注意到的第一件事是,声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同名属性。

接下来我们可以看到当调用foo()时,this.a被解析成了全局变量a。为什么?因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会绑定到undefined:

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

var a = 2;
foo(); // TypeError: this is undefined

这里有一个微妙但是非常重要的细节,虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定:

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

var a = 2;
(function() {
	"use strict";
  
  foo(); // 2
})()

隐式绑定

思考下面的代码:

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

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

obj.foo(); // 2

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。举例来说:

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

var obj2 = {
	a: 42,
  foo: foo
}
var obj1 = {
	a: 2,
  obj2: obj2
}

obj1.obj2.foo() // 42
隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。

思考下面的代码:

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

function obj = {
  a: 2,
  foo: foo
};

var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"  

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

function foo() {
  console.log(this.a);
}
function doFoo(fn) {
  // fn其实引用的是foo
  fn(); // <-- 调用位置!
}
var obj = {
  a: 2,
  foo: foo
};

var a = "oops, global"; // a是全局对象的属性

doFoo(obj.foo); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。

如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别:

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

var obj = {
  a: 2,
  foo: foo
};
var a = "oops, global"; // a是全局对象的属性

setTimeout(obj.foo, 100); // "oops, global"

显式绑定

在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。

JavaScript中的“所有”函数都有一些有用的特性,可以使用函数的call(..)和apply(..)方法来实现不想在对象内部包含函数引用,而在某个对象上强制调用函数。

它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。

思考下面的代码:

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

foo.call(obj); // 2

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。

硬绑定

显式绑定仍然无法解决我们之前提出的丢失绑定问题。是显式绑定的一个变种可以解决这个问题。

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

var obj = {
  a:2
};
var bar = function() {
  foo.call(obj);
};

bar(); // 2
setTimeout(bar, 100); // 2

// 硬绑定的bar不可能再修改它的this
bar.call(window); // 2

我们来看看这个变种到底是怎样工作的。我们创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

另一种使用方法是创建一个可以重复使用的辅助函数:

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}

// 简单的辅助绑定函数
function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments);
  };
}
var obj = {
  a:2
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5

由于硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind,它的用法如下:

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}
var obj = {
  a:2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

bind(..)会返回一个硬编码的新函数,它会把你指定的参数设置为this的上下文并调用原始函数。

API调用的“上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。举例来说:

function foo(el) {
  console.log(el, this.id);
}
var obj = {
  id: "awesome"
};

// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach(foo, obj);
// 1 awesome 2 awesome 3 awesome
        

new绑定

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作

  1. 使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

思考下面的代码:

function foo(a) {
  this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。new是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。

绑定规则优先级

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

this词法

箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。