什么是this
this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
需要明确的是,this在任何情况下都不指向函数的词法作用域。在JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JavaScript代码访问,它存在于JavaScript引擎内部。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。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 = 10
foo() // 10
函数调用时应用了this的默认绑定,因此 this指向全局对象。当调用foo()时,this.a被解析成了全局变量a。
我们可以通过分析调用位置来看看foo()是如何调用的。
隐式绑定
除了调用栈调用位置外,另一条需要考虑的规则是调用位置是否有上下文对象
我们先看看下面这个例子
function foo(){
console.log(this.a)
}
var objk = {
a:2,
foo:foo
}
objk.foo() // 2
当foo()被调用时,它的落脚点确实指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
显式绑定
说到显示绑定,必须要提到call
和apply
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
function foo(){
console.log(this.a)
}
var obj = {
a:3
}
foo()// 报错
foo.call(obj) // 3
通过foo.call(..)
,我们可以在调用foo时强制把它的this 绑定到obj上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(…)、new Boolean(…) 或者new Number(…))。这通常被称为“装箱”。
硬绑定
由于硬绑定是一种非常常用的模式,所以在 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的上下文并调用原始函数。
new绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
根据优先级判断this
现在我们已经了解了函数调用中 this 绑定的几条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则该怎么办?为了解决这个问题就必须给这些规则设定优先级,这就是我们接下来要介绍的内容。
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的
顺序来进行判断:
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
- 函数是否通过
call、apply
(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 - 指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上
- 下文对象。
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到
- 全局对象。
var bar = foo()
箭头函数的this
ES6 中介绍了一种无法使用这些绑定规则的特殊函数类型:箭头函数。
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。箭头函数不使用 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 也不 行!)