this
关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有的函数作用域中。有两个对this的误解:
- this指向自己
- this指向函数的作用域
其实,不完全正确,也不完全错误。
this是在运行时绑定的,而不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,而只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(也成为执行上下文)。这个记录包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。
调用位置和调用栈
要理解this的绑定过程之前,首先要理解调用位置。调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。例如:
1 | function baz() { |
上面我们声明了baz()
和bar()
两个函数,然后在baz()
里面调用了bar()
。
然后分析上面的代码中的函数的调用位置以及调用栈。首先我们直接调用了baz()
,那么在函数baz
中调用栈为baz
,调用位置为全局作用域。然后在baz
的里面调用了bar()
,那么在函数bar
中,调用栈为baz->bar
,调用位置为baz
。
this的绑定规则
函数的执行过程中的调用位置决定了this的绑定对象。有四条绑定规则:
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
默认绑定
最常用的函数调用类型:独立函数调用。也可以把这条规则看做为无法应用其他规则时的默认规则。例如:
1 | function foo() { |
在这里this.a
被解析为了全局变量中的a
。因为在上面代码中,函数调用的地方为全局作用域中,所以this指向了全局对象。
即默认绑定的情况,在非严格模式下,会将this绑定到全局对象上;在严格模式下,会报错未定义属性。
隐式绑定
隐式绑定需要考虑的是调用位置是否有上下文对象,或是说是否被某个对象包含。例如:
1 | function foo() { |
上面代码,当foo()
被调用时,函数的引用上下文对象为obj
,也就是说隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()
时this被绑定到obj,所以this.a
和obj.a
是一样的。
对象属性引用链中只有最顶层或者说最后一层才会影响调用位置。例如:
1 | function foo () { |
显式绑定
如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,我们可以使用call()
或apply()
方法。
这两个方法的第一个参数是一个对象,他们会把这个对象绑定到this,接着在调用函数时指定这个this。这种方式可以直接指定this的绑定对象,因此称之为显式绑定。
例如:
1 | function foo () { |
注意,入股哦传入了一个原始值(字符串、布尔值、数字)来当做this的绑定对象,这个原始值会被转换为它的对象形式,比如new Number(xx)
。这通常被称为“装箱”。
new绑定
在传统的面向对象语言中,“构造函数”是类中的一种特殊方法,使用new初始化类时会调用类中的构造函数。
JavaScript中也有一个new操作符,使用方法看起来也一样,然而JavaScript中的new的机制实际上和面向对象的语言完全不同。
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,他们并不属于某个类,也不会实例化一个类。实际上,他们只是被new操作符调用的普通函数而已。
实际上JavaScript中并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来构造函数时,会执行下面的操作:
- 创建一个全新的空对象
- 这个原型对象会被执行原型连接
- 这个新对象会被绑定到函数调用的this
- 如果函数没有返回其他对象,那么就自动返回这个新对象。
例如:
1 | function foo () { |
四种规则的优先级
new绑定、显示绑定 高于 隐式绑定 高于 默认绑定。
判断方式:
- 函数是否在new中调用,如果是的话,说明this绑定的是新创建的对象。
- 函数是否通过call、apply(显式绑定)?如果是的话,this绑定的是指定对象
- 函数是否在某个上下文对象中调用(隐式绑定)?是的话,this绑定的是那个上下文对象。
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。