现在了解了函数调用中this绑定的四条规则,需要做的是找到函数的调用位置并判断应用了哪条规则。如果调用位置应用多条规则,就必须给这些规则设定优先级。
毫无疑问,默认绑定是四条规则中最低的,可以暂时先不考虑。
测试下隐式绑定和显式绑定优先级:
function foo() {
console.log(this.a);
}
var obj1 = {
a: 1,
foo: foo
}
var obj2 = {
a: 2,
foo: foo
}
obj1.foo() // 1
obj2.foo() // 2
obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 1
显式绑定优先级更高,也就是说在判断时应当先考虑是否可以应用显式绑定。
测试下new绑定和隐式绑定优先级:
function foo(something) {
this.a = something
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2)
console.log(obj1.a) // 2
obj1.foo.call(obj2, 3)
console.log(obj2.a) // 3
var bar = new obj1.foo(4) // new 和 call/apply 无法一起使用,因此无法通过new foo.call(obj1)来直接测试
console.log(obj1.a) // 2
console.log(bar.a) // 4
new绑定比隐式绑定优先级高。接下来就是比较new绑定和显式绑定?
在看代码之前,先回忆下硬绑定是如何工作的。Function.prototype.bind(...)会创建一个新的包装函数,这个函数会忽略当前的this绑定(无论绑定的对象时什么),并把提供的对象绑定到this上。
这样看起来硬绑定(也是显式绑定的一种)似乎b比new绑定的优先级更高。
function foo(something) {
this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
var baz = new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) // 3
bar被硬绑定到了obj1上,但是new bar(3)并没有向我们预计的那样把obj1.a的值修改为3。相反,new修改了硬绑定(到obj1的)调用bar(...)中的this。因此使用了new绑定,我们得到了一个名字为baz的新对象,并且baz.a的值是3。
判断this
如果要判断一个运行中函数this的绑定,那么就需要找到这个函数的直接调用位置。找到之后按照以下规则判断this绑定的对象。
- 1.函数是否在new中调用(new绑定)?如果是this绑定的是新创建的对象
var bar = new foo() - 2.函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是this绑定的是指定的对象
var bar = foo.call(obj1) - 3.函数是否在某个上下文对象中调用(隐式绑定)?如果是this绑定的是那个上下文对象
var bar = obj1.foo() - 4.如果都不是,使用默认绑定。如果在严格模式下,就绑定到了undefined,否则绑定到了全局对象。
var bar = foo()
绑定例外
1.被忽略的this
如果你把null或者undefined作为this的绑定对象传入到call/apply/bind,这些值在调用时会被忽略,实际应用的会是默认绑定
function foo() {
console.log(this.a)
}
var a = 2;
foo.call(null) // 2
什么情况下你会传入null?
一种非常常见的做法是使用apply(...)来“展开”一个数组,并当做参数传入一个函数。类似的,bind(...)可以对参数进行kelihua柯里化(预先设置一些参数),非常有用:
function foo(a, b) {
console.log("a: " + a + ", b:" + b)
}
// 把数组展开成参数
foo.apply(null, [2, 3]) // a: 2, b:3
// 使用bind(...)进行柯里化
var baz = foo.bind(null, 4)
baz(5) //a: 4, b:5
这两种方法都需要传入一个参数当做this的绑定对象。如果函数不关心this的话,你仍然需要传入一个占位值,这时null就是个不错的选择。
使用null带来的副作用是,如果某个函数确实使用了this,那么默认绑定会把this绑定到全局对象中。(可以使用Object.create(null)来代替null更安全)
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)(); // 4
赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo(),所以会应用默认绑定。
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式,严格模式下this绑定到undefined,否则this绑定到全局对象
3.软绑定
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
foo内部创建的箭头函数会捕获调用foo()的this,由于foo()的this绑定到的是obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法修改(new也不行)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo() {
setTimeout(() => {
// 这里的this在词法上继承自foo()
console.log(this.a)
}, 100)
}
var obj = {
a: 2
}
foo.call(obj) // 2
箭头函数可以像bind(...)一样确保函数的this被绑定到指定对象。此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。实际上,ES6之前我们就已经在使用一种几乎和箭头函数一样模式
function foo() {
var self = this;
setTimeout(function() {
// 这里的this在词法上继承自foo()
console.log(self.a)
}, 100)
}
var obj = {
a: 2
}
foo.call(obj) // 2
虽然self = this和箭头函数看起来都可以取代bind(...),但从本质上来说他们想取代的是this机制。
如果你经常编写this风格的代码,但绝大部分都会使用self = this或者箭头函数来否定this机制,那你或许应当:
- 1.只使用词法作用域并完全抛弃this风格代码
- 2.完全采取this风格,在必要时使用bind(...),尽量避免self=this和箭头函数。