1.关于this
1.1 对this的一些误解
- 误解一:把this理解成指向函数自身
function foo(num){
console.log('foo:' + num)
this.count++
}
foo.count = 0
var i
for(i=0;i<10;i++){
if(i>5){
foo(i)
}
}
console.log(foo.count) //打印0
好家伙居然打印0,可以看出来这样理解this是错误的,this不一定指向函数自身。
实际上这里创建了一个全局变量count。
function foo(){
foo.count = 4 //foo指向它自身
}
setTimeout(function(){
//匿名函数无法指向自身
},10)
因此这样就可以了:
function foo(num){
console.log('foo:' + num)
foo.count++
}
foo.count = 0
var i
for(i=0;i<10;i++){
if(i>5){
foo(i)
}
}
console.log(foo.count) //打印4
但这样回避了this的问题,所以我们可以强行让this指向函数本身。
function foo(num){
console.log('foo:' + num)
this.count++
}
foo.count = 0
var i
for(i=0;i<10;i++){
if(i>5){
foo.call(foo,i)
}
}
console.log(foo.count)
用call()
可以确保this指向函数本身。
- 误解二:this指向函数的作用域(某种情况下是正确的,但有些情况又是错误的)
this在任何情况下都不指向函数的词法作用域。
因此,this在运行时绑定,不是在编写时绑定,其上下文取决于函数调用的各种条件。
当一个函数被调用时,会创建一个活动记录,这个记录包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息
2.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的调用位置
2.1 绑定规则
- 默认绑定
function foo(){
console.log(this.a)
}
var a = 2
foo() //打印2
this.a被解析成全局变量a,因为此时foo()是直接使用不带任何修饰的函数引用进行调用的,所以只能使用默认绑定。
- 隐式绑定
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo() //打印2
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,此处foo()被调用时this绑定到了obj上。
即在一个对象的内部包含一个指向函数的属性,并通过这个属性简介引用函数,从而把this隐式地绑定到这个对象上。
隐式丢失:
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo
var a = "???"
bar() //打印???
此时bar引用了foo函数本身,因此此时的bar是一个不带任何修饰的函数调用,所以应用了默认绑定。
在回调函数中也会出现这样的情况。
function foo(){
console.log(this.a)
}
function doFoo(fn){
//fn其实引用的是foo
fn() // <-调用位置
}
var obj = {
a:2,
foo:foo
}
var a = '???'
doFoo(obj.foo) //打印???
这里其实和上面一样,因为参数传递是一种隐式赋值,所以我们传入函数也会被隐式赋值,最终结果和上面一样是在doFoo()中调用foo函数本身,应用默认绑定。
- 显示绑定
Javascript提供的大多数函数和自己创建的函数都可以使用call()
方法和apply()
方法。
function foo(){
console.log(this.a)
}
var obj = {
a:2
}
foo.call(obj) //打印2
这里用call方法把foo的this显式绑定在obj上了。
如果传入一些字符串类型、布尔类型等的值来当作this的绑定对象,它会自动转换成对象形式,称为装箱。
但是单纯的显式绑定无法解决丢失绑定的问题。
- 硬绑定:es5提供了一个内置方法
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)
console.log(b) //打印5
bind会把指定的参数设置为this的上下文并调用原始函数。
- API调用的"上下文"
许多内置函数都提供一个可选参数称为”上下文“,其作用和bind一样,确保回调函数使用指定的this。
function foo(el){
console.log(el,this.id)
}
var obj = {
id:"jjy"
}
//调用foo()时把this绑定到obj
[1,2,3].forEach(foo,obj)
// 打印1 jjy 2 jjy 3 jjy
- new绑定
使用new来调用函数时,会执行以下操作:
- 创建一个新的对象
- 这个对象会被执行[[Prototype]]连接
- 这个对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
this.a = a
}
var bar = new foo(2)
console.log(bar.a) //2
2.2 优先级
new绑定 -> 显示绑定(call,apply,bind) -> 隐式绑定 -> 默认绑定
但凡事总有例外。
-
当把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时候会被忽略,此时使用默认绑定。
通常用apply来展开一个数组并当做参数传入时候会用上,或者用bind进行柯里化时:
function foo(a,b){ console.log("a:"+a+",b:"+b) } //把数组展开成参数 foo.apply(null,[2,3]) //使用bind进行柯里化 var bar = foo.bind(null,2) bar(3)
在ES6中可以使用
...
来展开数组,比如:foo(…[1,2])但这样不安全,可以用传入一个空对象
{}
或者用Object.create(null)
来代替传入null,这样可以避免某些情况下污染全局对象 -
间接引用
我们可能有时候会创建一个函数的“间接引用”,在这种情况下调用这个函数会应用默认绑定规则。
-
软绑定
自己定义一个软绑定函数,区别于bind(),这个可以手动修改this的绑定对象
2.3 this词法
箭头函数无法使用前面这些规则,根据外层作用域来决定this。
function foo(){
return (a) => {
console.log(this.a)
}
}
var obj1 = {
a:2}
var obj2 = {
a:3}
var bar = foo.call(obj1)
bar.call(obj2) //打印2
foo()内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this绑定到obj1,所以bar引用的箭头函数的this也会绑定到obj1,而且箭头函数的绑定无法修改。