this的指向对于JS是一块比较难以理解的内容,但又是一块很重要的内容。使用差不多两年的JS,但是一直对this的指向半知不解。下面是我这个小菜鸡对this的指向的理解,记录一下自己的想法,参考于《你不知道的JavaScript(上卷)》,如有错误,请指出,多多谅解。
想要了解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的四个绑定规则:
1、默认绑定
首先介绍的是是最常见的函数调用类型:独立函数的调用。“默认绑定”可以认为无法应用其他规则时的默认绑定规则。
看下面的代码:
function foo () {
console.log(this.a)
}
var a = 2
foo()
接下来我们可以看到 foo() 的调用,this.a被解析成全局对象 a 。因为在这个例子中,this应用了默认绑定规则,绑定了全局对象。
解析:怎么知道这里应用了默认绑定的规则呢?从调用位置来看,foo() 的调用是直接使用不带任何修饰符的函数调用,无法引用其他规则,只能走默认绑定规则(我存留的疑问:非严格模式下的默认绑定就是绑定全局定向吗? 这是一种规定吗?我暂时这么去理解。)
2、隐式绑定
该规则需要考虑调用位置是否存在上下文(或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。)
function foo () {
console.log(this.a)
}
var a = 5
var obj = {
a: 2,
foo: foo
}
obj.foo() // 2
首先需要注意的是foo()的声明方式,然后是被作为引用属性添加到obj的的属性中,因此无论是直接在 obj 中定义 还是 先定义再添加为引用属性。这个函数严格来说都不属于obj 对象(没太理解这句话。)
当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
function foo() {
console.log(this.a)
}
var obj2 = {
a: 2,
foo: foo
}
var obj1 = {
a: 42,
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 = 'test'
bar() // test
虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
还有一种情况需要注意:作为回调函数时,绑定也会丢失
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var a = 'loss this'
function bar (fn) {
fn()
}
bar(obj.foo) // loss this
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一
个例子一样。如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别。(可以回顾函数的形参)
3、显示绑定
思考下面的代码:
function foo () {
console.log(this.a)
}
var obj = {
a: 'input a'
}
foo.call(obj) // input a
通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
注意:如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者new Number(..))。这通常被称为“装箱”。
可以理解为:显式绑定就是利用call、apply、bind的来显式绑定this
4、new 绑定
这是第四条也是最后一条 this 的绑定规则。首先我们重新定义一下 JavaScript 中的“构造函数”。在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
先了解一下new来调用函数,或者说构建函数调用时,会执行哪些步骤:
- 创建一个新的对象
- 这个新对象会被执行[[原型]]连接(按我的理解:其实就是执行构造函数,初始化新对象)
- this会绑定到新对象中
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
例子:
function foo (a) {
this.a = a
}
var bar = foo(2)
bar.a // 2
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
优先级的判断:
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:
1. 函数是否在 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 的绑定原理了。
不过……凡事总有例外。
需要注意的一点:
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则
function foo () {
console.log(this.a)
}
var a = 'input a'
foo.call(null) // input a