1. this的理解
this关键字是javascript中最复杂的机制之一,无论是一个JavaScript初学者还是已经有一些使用经验的老鸟来说,清晰的理解this的指向也是很有必要的.
1.1 关于this的误解
1.1.1 指向函数自身
这个理解从英语语法角度来说是可以说得通的.然后通过this来存储一些函数自身的状态属性.然而我们可以通过一个很简单例子来说明这个的错误,比如:
function foo() {
console.log(this.count)
}
foo.count = 1
foo() // undefined
但是从上面的例子中我们可以看到,this实际上并没有指向foo本身, 至于为什么打印结果是undefined,我们后面会讲到.
1.2 this的指向
实际上,this的指向并不是确定的,它不像函数的作用域一样在定义的时候就已经确定,它更像动态作用域一样,它只关心函数的调用位置,以及是被谁调用的.
2. this的绑定规则
下面让我们来看看,具体的情况下,函数的this到底是指向谁的
2.1 默认绑定
function foo() {
console.log(this)
}
foo() // window
function foo() {
"use strict"
console.log(this)
}
foo() // undefined
上面的例子可以看出来, 独立函数调用时, 该函数的this指向在非严格模式下是window的, 在严格模式下则是undefined.
2.2 隐式绑定
2.2.1.一般情况
function foo() {
this.a = 3
console.log(this.a)
}
let obj1 = {
foo,
a: 2,
}
obj1.foo() // a
console.lgo(obj1) // {foo:Fcuntion,a:3}
作为对象的属性,被对象调用时,该函数的this指向了他的调用者.
2.2.2.特殊情况
- 隐式丢失
var a = 2
function foo() {
console.log(this.a)
}
let obj = {
a: 1,
foo,
}
let bar = obj.foo
bar()//2
bar是obj.foo的一个引用,但实际上它引用的是foo函数本身,因此此时的bar()实际上是一个不带任何修饰的函数调用,因此采用了默认绑定.
- 作为回调函数
function foo() {
console.log(this.a)
}
function doFn(fn) {
fn()
}
let obj = {
a: 1,
foo,
}
doFn(obj.foo) // fn = obj.foo undefined
和上面的情况基本相同,doFn函数中的参数fn实际上就是对obj.foo的引用,也会采用默认绑定.
2.3 显式绑定
上面的隐式绑定,我们必须在一个对象内部包含一个指向函数的属性, 通过这个属性来间接调用函数,那么我们不想在对象内部包含函数,也想通过这个对象来调用函数,有没有办法呢?
答案是有的, 那就是通过显示绑定来实现.
2.3.1 一般情况下,我们就是通过函数原型上的call,apply方法来实现的,我们也称这种方式为显式绑定
function foo() {
this.c = 3
console.log(this)
console.log(this.a)
}
let obj = {
a: 1,
b: 2,
}
foo.call(obj) // {a:1,b:2,c:3} 1
foo.apply(obj) // {a:1,b:2,c:3} 1
通过call和apply方法, 我们在调用foo函数时,强制将它的this绑定到obj上.
从函数绑定的调度来说,call,apply是没有区别的,它们主要是在给函数传递参数上有一定的区别.
2.3.2 实际上,像上面这两种方法,也无法解决我们之前的提出的绑定丢失的情况.我们就可以使用第三种方法了, 硬绑定(另类的显示绑定),利用函数原型上的bind方法.
var a = 2
function foo() {
console.log(this.a)
}
let obj = { a: 1, foo }
let bar = obj.foo.bind(obj)
bar() //1
通过bind方法返回了一个被绑定了this的函数, 并且这个函数的this不会再被改变,通过这种方法,就可以解决上面提到的绑定丢失的情况.
2.4 new关键字绑定
利用new关键字,调用函数,也可以实现this的绑定, 我们就需要知道,在使用new关键字调用函数的时候,到底发生了什么?
1. 创建一个新的对象
2. 将这个对象的[[prototype]]赋值为函数的prototype
3. 将函数的this绑定为创建的对象
4. 如果这个函数没有返回一个对象的话,就会把创建的对象返回.
function Foo() {
this.a = 1
}
let f = new Foo()
console.log(f.a)//1
在使用new调用foo函数时, 将创建出的f对象, 绑定到了foo种的this上.
2.5 优先级比较
我们再来看看,以上几种方法的优先级.通过上面的例子,就能看出了,默认绑定的优先级是最低的.我们再来看看另外几种的优先级呢?
2.5.1 显示-隐式
function foo() {
console.log(this.a)
}
let obj1 = {
a: 1,
foo,
}
let obj2 = {
a: 2,
foo,
}
obj1.foo() // 1
obj2.foo() // 2
obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 1
上面两个,采用了隐式绑定,正常打印出了1,2,下面两种,先采用了隐式绑定,再通过call方法绑定了this,同时修改了函数的this,可以看出,显示绑定优先级是大于隐式绑定的.
2.6.2 显示-new
由于, call,apply方法是无法和new关键字一起使用的.因此,我们可以比较一下bind和new.
function foo(a) {
this.a = a
console.log(this.a)
}
let obj1 = {
a: 1,
}
let bar = foo.bind(obj1)
bar(2) // 2
let newB = new bar(3)
console.log(newB.a) // 3, 3
通过上面的结果,我们也可以看出来,通过bind绑定了的函数, 最后也被new关键字改变了this的指向, 因此, new关键调用的函数的优先级是要高于显示绑定的.
2.6.特殊情况
3.1 apply,call,bind
如果,我们在使用apply,call方法时,传入了undefined或者null时,它们在函数调用的时候会被忽略,函数实际采用了默认绑定规则.
function foo() {
console.log(this)
}
let obj = {
a: 1,
}
foo.call(obj) // {a:1}
foo.call(null) //undefined
foo.call(undefined) //undefined
3.2 箭头函数
箭头函数无法通过call,apply,bind,new等方式绑定this,它没有属于自己this,它的this来自于它的上层作用域.
3. 面试题
经过上面的学习,我们来一道面试题,检验一下吧.
name = "window"
var person1 = {
name: "person1",
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => console.log(this.name)
},
}
var person2 = { name: "person2" }
person1.foo1()
person1.foo1.call(person2)
person1.foo2()
person1.foo2.call(person2)
person1.foo3()()
person1.foo3().call(person2)
person1.foo3.call(person2)()
person1.foo4()()
person1.foo4().call(person2)
person1.foo4.call(person2)()
// person1.foo1() // 隐式调用 :person1
// person1.foo1.call(person2) // 显式调用优先级大于隐式调用: person2
// person1.foo2() // 箭头函数不绑定this,找上层作用域 window
// person1.foo2.call(person2) // 箭头函数无法通过call/apply/bind进行this绑定 window
// person1.foo3()() // 独立函数调用: window
// person1.foo3().call(person2) // 显式调用: person2
// person1.foo3.call(person2)() //独立函数调用 :window
// person1.foo4()() // 上层函数作用域 :person1
// person1.foo4().call(person2) // 上层函数作用域: person1
// person1.foo4.call(person2)() // 上层函数作用域: person2