《你不知道的javascript》系列绝对是javascript进阶最好的书籍之一,推荐大家阅读。本文是此书的读书笔记与总结。
使用this的原因
this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
this误区
学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域
首先我们比较在两段代码:
function foo(num) {
console.log( "foo: " + num );
// 记录foo被调用的次数
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++)="" {="" if="" (i=""> 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次?
console.log( foo.count ); // 0 -- WTF?
function foo(num) {
console.log( "foo: " + num );
// 记录foo被调用的次数
// 注意,在当前的调用方式下(参见下方代码),this确实指向foo
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用call(..)可以确保this指向函数对象foo本身
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次?
console.log( foo.count ); // 4
第一段代码和第二段代码只有一个地方不同,就是在调用foo时使用call保证了this指向函数foo本身,因此也产生了不同的结果,说明this并不是简单的指向函数自身,而是有一套绑定的规则。
this规则
调用位置
在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。通常来说,寻找调用位置就是寻找“函数被调用的位置”,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈是baz -> bar
// 因此,当前调用位置在baz中
console.log( "bar" );
foo(); // <-- foo的调用位置
}
function foo() {
// 当前调用栈是baz -> bar -> foo
// 因此,当前调用位置在bar中
console.log( "foo" );
}
baz(); // <-- baz的调用位置
绑定规则
作用域链查找遵循”就近原则”;this谁调用就指向谁。
找到调用位置,再根据规则判断this的绑定对象
默认绑定
在非严格模式下默认绑定会绑定到全局对象。使用严格模式,this会绑定到undefined。
function test() {
console.log(this);
};
test();
// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage, sessionStorage: Storage, webkitStorageInfo: DeprecatedStorageInfo…}
'use strict';
function test() {
console.log(this);
};
test();
// undefined
隐式绑定
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
当foo()被调用时,它的落脚点指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
注意
- 遵循”就近原则”,当出现
obj1.obj2.foo()
时,只有最后一层会影响调用位置。 - 回调函数会丢失this绑定
显式绑定(call,apply)
可以使用函数的call(..)和apply(..)方法
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
思考下面的代码:
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通常被称为“装箱”。
new绑定
在 js 中,为了实现类,我们需要定义一些构造函数,在调用一个构造函数的时候需要加上 new 这个关键字:
function Person(name) {
this.name = name;
console.log(this);
}
var p = new Person('kevin');
// Person {name: "kevin"}
我们可以看到当作构造函数调用时,this 指向了这个构造函数调用时候实例化出来的对象,new是最后一种可以影响函数调用时this绑定行为的方法,称之为new绑定。
当然,构造函数其实也是一个函数,如果我们把它当作一个普通函数执行,这个 this 仍然执行全局:
function Person(name) {
this.name = name;
console.log(this);
}
var p = Person('kevin');
// Window
优先级
- 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo()
2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
var bar = obj1.foo()
4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。
ES6箭头函数
箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
看看箭头函数的词法作用域:
function foo() {
// 返回一个箭头函数
return (a) => {
//this继承自foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this 也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo() {
setTimeout(() => {
// 这里的this在此法上继承自foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象,简单来说, 箭头函数中的 this 只和定义它时候的作用域的 this 有关,而与在哪里以及如何调用它无关,同时它的 this 指向是不可改变的。
总结
如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象。
- 由new调用?绑定到新创建的对象。
- 由call或者apply(或者bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到undefined,否则绑定到全局对象。
ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。
总之this是javascript中比较复杂的机制,很特别也很方便,需要我们在不断的使用中体会。