之前一直对this的指向很模糊,找了一些别人的博客看,又重新看了一下《你不知道的JavaScript》,感觉基本上是弄懂了,挑一些重点的地方记录一下,有些地方对我来说书上解释写的不够多,所以自己做下补充以方便理解,有理解错的地方还望指出。
一.澄清误区
首先你需要知道:
1.this并不指向函数自身
2.this的作用域在任何情况下都不指向函数的词法作用域。
举个例子:
function foo() { var a = 2; this.bar(); } function bar() { console.log(this.a); } foo();//ReferenceError: a is not defined
第一次看这里的时候就掉坑里了,想当然的以为foo()中this指向window,而bar()又在全局中故能调用bar(),但实际上这是错的,不能使用this来引用一个词法作用域内部的东西。
this是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里调用。跟函数声明的位置没有任何关系,要区别于函数的作用域,函数的作用域是在它被定义的时候确定的。
要判断一个运行中函数的this绑定,需要找到这个函数的直接调用位置。
二.调用位置
调用位置就是函数在代码中被调用到的位置。
this有四条绑定规则:
1.默认绑定:非严格模式下this指向全局对象,严格模式绑定到undefined。 var bar = foo()
2.隐式绑定:this指向包含它的函数的对象,要注意隐式丢失的情况。 var bar = obj1.foo()
3.显示绑定:this指向call()、apply()和bind()方法指定的对象。 var bar = foo.call(obj2)
4.new绑定:this指向构造的新对象。 var bar = new foo()
优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
具体看下面:
1.默认绑定
无法应用其他规则时默认使用默认绑定。如果函数独立调用,使用默认绑定。
function foo() { //看函数体是否处于严格模式,是看这个位置("use strict")
console.log(this.a); } var a = 2; foo(); //2 仅foo()函数本身,为独立调用
在上面的代码中,foo()是使用不带任何修饰的函数引用进行调用的,所以会使用默认绑定,非严格模式下this指向全局对象,严格模式绑定到undefined。不是指调用位置是否处于严格模式,而是函数体是否处于严格模式。
2.隐式绑定
考虑函数的调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo //foo()被当作引用属性添加到obj中,此时它被obj对象包含,此时this指向obj } obj.foo(); //2
对象属性引用链中只有最后一层会影响调用位置。
function foo() { console.log(this.a); } var obj2 = { a: 42, foo: foo } var obj1 = { a: 2, obj2: obj2 } obj1.obj2.foo(); //42 虽然这里有obj1对象和obj2对象,但是obj2才是处于最后一层最接近foo(),所以会指向obj2
隐式丢失的情况
来看下面的代码:
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo } var bar = obj.foo; //虽然bar是obj.foo的一个引用,但实际上bar引用的只是foo函数本身,可以看成bar() = foo(),此时foo()是独立调用故绑定到全局对象 var a = "oops, global"; bar(); //oops, global
还有一种常见的隐式丢失的情况是传入回调函数
function foo() { console.log(this.a); } function doFoo(fn) { fn(); } var obj = { a: 2, foo: foo } var a = "oops, global"; doFoo(obj.foo); //oops, global obj.foo作为参数传入实际上隐式赋值给了fn,可以看成fn = obj.foo, //此时又回到了上一例代码的情况,引用的是foo函数本身,看成fn()= foo(),独立调用指向全局对象
3.显示绑定
使用函数的call()和apply()方法可以使用显示绑定,这两个方法的第一个参数都是一个对象,他们会把这个对象绑定到this,在调用函数时指定这个this,因为可以指定绑定对象故称做显示绑定。
function foo() { console.log(this.a); } var obj = { a: 2 } foo.call(obj);
但是这种显示绑定没有解决隐式丢失的问题,要解决这个问题可以使用硬绑定。
function foo() { console.log(this.a); } var obj = { a: 2 } var bar = function() { foo.call(obj); } bar(); //2 setTimeout(bar, 100) //2 bar.call(window); //2
创建了一个函数bar(),并在它的内部手动调用foo.call(obj),因此强制把foo的this绑定到了obj,之后无论如何调用函数bar,它总会手动在obj上调用foo,这叫做硬绑定。
硬绑定非常常用,ES5中提供了内置的方法Function.prototype.bind,bind()会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。
硬绑定的两个典型应用场景:
- 创建一个包裹函数,传入所有的参数并返回接收到的所有值
function foo(something) { console.log(this.a, something); return this.a + something; //返回接收到的所有this.a和something } var obj = { a: 2 } var bar = function() { return foo.apply(obj, arguments); //this指向obj,arguments为传入的参数3 } var b = bar(3); //2 3 console.log(b); //5
- 创建一个可以重复使用的辅助函数
function foo(something) { console.log(this.a, something); return this.a + something; } function bind(fn, obj) { return function() { return fn.apply(obj, arguments); }; } var obj = { a: 2 } var bar = bind(foo, obj); var b = bar(3); //2 3 console.log(b); //5
4.new绑定
使用new来调用函数,或者说发生构造函数调用时,会执行下面的操作:
①创建(构造)一个全新的对象。
②这个新对象会被执行[[原型]]连接。
③这个新对象会绑定到函数调用的this。
④如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) { this.a = a; } var bar = new foo(2);//使用new操作符调用foo(),构造了一个新对象bar并把它绑定到foo()的this上 console.log(bar.a); //2
三.绑定例外
有一些例外情况需要注意
1.把null或undefined作为this的绑定对象传入call、apply或bind。
null和undefined在调用时会被忽略,然后应用默认绑定。
function foo() { console.log(this.a); } var a = 2; foo.call(null);//2
有时你可能选择null作为一个占位值而选择null作为参数,但是总是用null来忽略this的绑定可能产生一些副作用。一种更安全的做法是传入一个空的非委托对象,把this绑定到这个对象不会对你的程序产生任何副作用。
2.无意间创建了一个函数的“间接引用”
同样会应用默认绑定,最容易在赋值时发生。
function foo() { console.log(this.a); } var a = 2; var o = { a: 3, foo: foo } var p = { a: 4 } o.foo();//3 (p.foo = o.foo);//2 p.foo = o.foo的返回值是目标函数的引用,故调用的位置是foo(),又会应用默认绑定
3.软绑定 softBind()
硬绑定会降低函数的灵活性,使用硬绑定后就无法使用隐式绑定或显示绑定来修改this。
软绑定:给默认绑定指定一个全局对象和undefined以外的值,可以实现和硬绑定相同的效果,同时保留隐式绑定或显示绑定修改this的能力。
除了软绑定外,softBind()的其他原理和bind()类似。首先检查调用时的this,如果this绑定到全局对象或者undefined,就把指定的对象绑定到this,否则不会修改this。
function foo() { console.log("name:" + this.name); } var obj = { name: "obj" } var obj2 = { name: "obj2" } var obj3 = { name: "obj3" } var fooOBJ = foo.softBind(obj); fooOBJ();//obj obj2.foo = foo.softBind(obj); obj2.foo();//obj2 obj2.foo()调用时,this绑定到obj2上,不是全局对象也不是undefined,所以不会调用指定的obj,而是使用原来的obj2。 fooOBJ.call(obj3);//硬绑定时绑定了obj3,后面不可再修改 setTimeout(obj2.foo, 10);//假设setTimeout(fn,10),fn实际上引用的只是foo(),可以看成fn = obj2.foo,类似前面的隐式绑定丢失, //会使用默认绑定到全局,这时会发生软绑定,绑定到指定对象obj
4.箭头函数
箭头函数无法使用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()的this,foo()的this会显示绑定到obj1, //bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改
箭头函数常用于回调函数中,例如事件处理器或者定时器
function foo() { setTimeout( () => { console.log(this.a);//箭头函数会继承外层函数调用的this绑定,这里箭头函数的外层函数是foo(),foo()的this绑定到obj,所以箭头函数的this也绑定到obj。 }, 100); } var obj = { a: 2 } foo.call(obj);//2