JS之 this
1. this存在哪里?
this在日常开发中给人一种它好像用的不多,但是又好像无处不在的错觉。但是它确实无处不在。
它是一个特殊的关键字,被自动定义在所有函数的作用域中。
2. 为什么要用this?
先说结论:希望在函数可以自动引用合适的上下文对象。
先放不用this的代码:
function upper(context) {
return context.name.toUpperCase()
}
function speak(context) {
var greet = "你好,我是" + upper(context)
console.log(greet);
}
var me = {
name: 'Bob'
}
var you = {
name: "Kyle"
}
console.log(upper(me)); // BOB
console.log(upper(you)); // KYLE
speak(me) // 你好,我是BOB
speak(you) // 你好,我是KYLE
上面代码中通过将对象由形参的方式传递给函数,然后在函数内手动获取到此对象形参的属性进行操作。
下面放上使用this的代码:
function upper() {
return this.name.toUpperCase()
}
function speak() {
var greet = "你好,我是" + upper.call(this)
console.log(greet);
}
var me = {
name: 'Bob'
}
var you = {
name: "Kyle"
}
console.log(upper.call(me)); // BOB
console.log(upper.call(you)); // KYLE
speak.call(me) // 你好,我是BOB
speak.call(you) // 你好,我是KYLE
两种方法实现的功能一样,但是随着使用模式越来越复杂,显式的传递上下文对象会让代码越来越混乱。但是this提供了一种更为优雅的方式来隐式“传递”一个独享引用,因此可以将API设计的更加简洁且易于复用。
3.对于this的常见误解
3.1 指向自身
我们很容易把this理解成指向函数自身。先看一段示例代码:
function foo(n) {
console.log("foo---" + n);
this.count++
}
foo.count = 0
for (var i = 0; i < 10; i++) {
if (i > 5) {
foo(i)
}
}
console.log(foo.count); // 0
最终输出结果:
foo()里的console语句执行了4此,说明foo()确实被调用了4次,但是foo.count
仍然是0,这只能说明一个问题:foo()
函数里的this.count !== foo.count
那么this.count++
到底加到哪里去了?先说结论:它加到了window全局对象上了,且window.count
打印出来是NaN
。
- 为什么是
NaN
?- 因为
window.count
初始没有这个值,所以是undefined,undefined++
就是NaN。
- 因为
- 为什么加到了window上?
- 因为直接调用foo()时,此时的foo()函数所处的上下文是window。(这里先放这不用理解)。
3.2 this指向函数的作用域
this在任何情况下都不指向函数的词法作用域
。在JS内部,作用域和对象类似,可见的标识符都是它的属性。但是作用域对象无法通过JS代码访问,它存在于JavaScript引擎内部。
4. this是什么?
this是在运行时绑定的,并不是在编写时绑定。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(这句话很重要)。
当一个函数被调用时,会创建一个执行上下文,这个上下文包含函数在哪里被调用、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。
5. 调用位置。
上面说了,this的绑定完全取决于函数的调用位置(即函数的调用方法)。
在理解this的绑定过程之前,要先理解调用位置。这里还需要理解另外一个东西:调用栈
。
调用栈
什么是调用栈:
由于JS只有一个单线程,因此只有一个调用栈,它同一时间只能做一件事,当运行到一个函数,这个函数就会被放到栈顶的位置,当函数return
时,就会将这个函数从栈顶弹出。这就是调用栈。可以理解为这样一个模型:
现在给一个示例来说明调用栈和调用位置:
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()
当执行baz()时,baz被压入栈底,此时调用栈中有一个baz;执行到bar()时,bar又被压入栈,此时调用栈由下往上是baz - bar
;执行到foo()
,foo被压入栈底,此时调用栈是baz - bar - foo
。如下图所示:
后续的话。当foo()执行结束,foo被弹出栈;bar()执行结束,bar被弹出栈;baz()执行结束,baz也被弹出栈。到此调用栈清空。
接下来可以看调用位置是如何决定this的绑定对象的。
6.this的4种绑定规则
6.1 默认绑定
默认绑定
的前提是:独立函数调用。当其他规则无法应用时,可以把这个规则当作默认规则。
先见一段代码:
function foo(){
console.log(this.a);
}
var a = 2
foo() // 2
声明在全局作用域的变量就是全局对象
的一个同名属性。就比如var a = 2
,就好像全局对象多了一个值为2的属性a全局对象 : { a: 2 }
。
这里当调用foo()时,this.a被解析成了全局变量a。因为这里函数调用时应用了this的默认绑定
,因此this指向了全局对象。且这里foo()
函数是直接调用的,并没有像xxx.foo()
这样通过xxx.foo()调用,因此只能使用默认绑定
,无法应用其他规则。
严格模式下的特殊情况
如果使用了严格模式,那么全局对象将无法使用默认绑定,这时候的this会被绑定到undefined。
6.2 隐式绑定
先看如下代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
上面代码中,foo作为obj对象的引用,当foo()被调用(obj.foo()
)时,它的落脚点指向obj对象。当函数引用有上下文对象时,隐式绑定
规则就会把函数调用中的this绑定到这个上下文对象。这时候的this.a
和obj.a
是一样的。
需要注意的是:对象属性引用链中只有最后一层会影响调用位置。示例:
function foo() {
console.log(this.a);
}
var obj1 = {
foo: foo,
a: 3
}
var obj2 = {
obj1: obj1,
a: 4
}
obj2.obj1.foo() // 3
需要注意一件事:函数也属于引用类型,要防止因引用的是函数地址问题而导致的隐式丢失
。
小结:隐式绑定时,必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。
6.3 显式绑定(使用call、apply、bind)
通过call
,apply
,bind
方法将某个对象绑定到this上,接着在调用函数时指定这个this。因为可以直接指定this的绑定对象,因此称为显式绑定。
先看代码:
function foo() {
console.log(this.a);
}
var obj1 = {
a: 3
}
foo.call(obj1) // 3
通过调用foo.call(..),可以在调用foo时强制把它的this绑定到obj上。
这两个函数的功能完全一样,在没有参数时的使用方法完全一样,第一个参数就是需要绑定this的对象。但是如果需要向foo()传递参数时,就有点些许的差别:call()后面的参数是一个参数列表:foo.call(obj, param1, param2, ..., paramN);apply()的第二个参数是一个参数数组:foo.apply(obj, [param1, param2, ..., paramN])。
注意:如果传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..)).。
但是这里如果要调用foo()方法,需要每次都调用foo.call()
方法。这时候就可以通过bind()
实现重复使用:
function foo() {
console.log(this.a);
}
var obj1 = {
a: 3
}
var b = foo.bind(obj1)
b() // 3
通过bind()
,创建了一个新函数b,它会把参数设置为this的上下文并调用原始函数。
6.4 new绑定
JS中的构造函数,本质上就是一个普通函数,只不过它是被new操作符调用的。
所有的内置对象函数(Number(..)等)都可以用new来调用,这种函数调用被称为构造函数调用。
new操作符调用函数时,执行了哪些操作。
- 创建了一个新对象
- 将新对象的原型与父对象的原型链接
- 将新对象绑定到函数调用的this
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
见如下代码:
function foo(a) {
this.a = a
}
var bar = new foo(2)
console.log(bar.a); // 2
用new来调用foo()时,会构造一个新对象并把它绑定到foo(..)调用中的this上。
7. 判断this
看完了上面4种this绑定规则,可以按照下面顺序来判断函数在某个调用位置应用的是哪条规则。
-
函数是否是用new调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo()
。 -
函数是否通过call、apply(显示绑定)或者硬绑定调用?如果是,this绑定的是指定的对象。
var bar = foo.call(obj2)
。 -
函数是否在某个上下文对象中调用(隐式绑定)?如果是,this绑定的是这个上下文对象。
obj1.foo()
。 -
如果都不是的话,使用默认绑定。严格模式下绑到undefined,否则绑定到全局对象。
foo()
8. 箭头函数中的this。
箭头函数不适用于上面的this的4种绑定规则,而是根据外层(函数或全局)作用域来决定的this。
先看代码:
function foo() {
return (a) => {
console.log(this.a); // this继承自foo()
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1)
bar.call(obj2) // 2
foo()里的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1。bar(箭头函数的引用)也会绑定到obj1,且箭头函数的绑定无法被修改(new也不行)。
ES6之前,通过self = this
的方法一样确保函数的this绑定到指定对象。
function foo() {
var self = this;
setTimeout(function () {
console.log(self.a);
}, 100);
}
var obj = { a: 2 };
foo.call(obj); // 2