this
关键字是JS
中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。
为什么要使用this
this
提供了一种更优雅的方式来隐式 ”传递“ 一个对象引用,因此可以把API
设计得更加简洁且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用this
则不会这样。
this
是在运行时进行绑定的,他的上下文取决于函数调用时的各种条件。this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this
就是这个记录的一个属性,会在函数执行的过程中用到。
this
既不指向函数自身也不指向函数的词法作用域,this
实际上是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
调用位置
function baz() { // 当前调用栈是:baz // 因此,当前调用位置是全局作用域 console.log("baz"); bar(); } function bar() { // 当前调用栈是:baz - bar // 因此,当前调用位置是baz console.log("bar"); foo(); } function foo() { // 当前调用栈是:baz - bar - foo // 因此,当前调用位置是bar console.log("foo"); } baz();
绑定规则
函数的执行过程中调用位置如何决定this
的绑定对象:你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。
默认绑定
最常见的函数调用类型:独立函数调用。
function foo() { console.log(this.a); } var a = 2; foo(); // 2
当调用foo
时,this.a
被解析成全局变量a
。为什么?因为在本例中,函数调用时应用了this
的默认绑定,因此this
指向全局对象。
怎么知道是应用默认绑定?可以通过分析调用位置。在代码中,foo
是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
如果使用严格模式,则不能将全局对象用于默认绑定,this
会绑定到undefined
。
function foo() { "use strict" console.log(this.a); } var a = 2; foo(); // TypeError: Cannot read property 'a' of undefined
这里有一个微妙但非常重要的细节,虽然this
的绑定规则完全取决于调用位置,但只有foo
运行在非严格模式下时默认绑定才能绑定到全局对象,在严格模式下调用foo
不影响绑定。
function foo() { console.log(this.a); } var a = 2; "use strict" foo(); // 2
隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说被某个对象拥有或者包含。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
当 foo
被调用时,他的前面加上了对 obj
的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this
绑定到这个上下文对象。因此,this.a
和 obj.a
是一样的。
对象属性引用链中只有最后一层在调用位置中起作用。
function foo() { console.log(this.a); } var obj2 = { a: 2, foo: foo }; var obj1 = { a: 1, obj2: obj2 }; obj1.obj2.foo(); // 2
隐式丢失
一个最常见的this
绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定,从而把this
绑定到全局对象或者undefined
上。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; var bar = obj.foo; var a = 3; bar(); // 3
一种更微妙、更常见、更出乎意料的情况发生在传入回调函数时:
function foo() { console.log(this.a); } function doFoo(fn) { fn(); } var obj = { a: 2, foo: foo } var a = "global"; doFoo(obj.foo); // "global" , 无论全局变量a是在obj之前还是之后声明并赋值
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。如果把函数传入语言内置函数而不是传入自己声明的函数,结果也是一样的。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo } var a = "global"; setTimeout(obj.foo, 100); // "global"
如上,回调函数丢失this
绑定是非常常见的,此外,调用回调函数还可能会修改this
。
显式绑定
如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
具体来说,可以使用call
和apply
方法。这两个方法如何工作呢,他们的第一个参数是一个对象,是给this
准备的,接着在调用函数时将其绑定到this
。因为你可以直接指定this
的绑定对象,因此我们称之为显式绑定。
function foo() { console.log(this.a); } var obj = { a: 2 }; foo.call(obj); // 2
可惜,显示绑定仍然无法解决丢失绑定的问题。
硬绑定
硬绑定可以解决丢失绑定的问题。
我们创建函数bar
并在内部手动调用foo.call(obj)
,因此强制把foo
的this
绑定到了obj
,之后无论如何调用函数bar
,他总会在obj
上调用foo
。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
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
硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值
function foo(something) { console.log(this.a, something); return this.a + something; } var obj = { a: 2 }; var bar = function() { return foo.apply(obj, arguments); } var b = bar(3); // 2 3 console.log(b); // 5
另一种是创建一个可以重复使用的辅助函数
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.protoprototype.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
new 绑定
在JS
中,构造函数只是一些使用new
操作符时被调用的函数,他们并不会属于某个类,也不会实例化一个类。实际上,他们甚至不能说是一种特殊的函数类型,他们只是被new
操作符调用的普通函数而已。
function foo(a) { this.a = a; } var bar = new foo(2); console.log(bar.a); // 2
使用new
来调用foo
时,我们会构造一个新对象并把它绑定到foo
调用中的this
上。
优先级
如果某个调用位置应用了多条规则怎么办?为了解决这个问题就必须给这些规则设定优先级。
毫无疑问,默认绑定的优先级是四条规则中最低的,new绑定比显式绑定优先级高。显示绑定的优先级比隐式绑定高。