在某些场景下 this
的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。
被忽略的 this
如果把 null
或者 undefined
作为 this
的绑定对象传入 call
、apply
或者 bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则:
function foo(){
console.log(this.a)
}
var a = 2
foo.call(null)
-------------------------------------
> 2
什么情况下会传入 null
呢?
一种非常常见的做法是使用 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)
------------------------------------------------------
> a:2,b:3
> a:2,b:3
这两种方法都需要传入参数当作 this
的绑定对象。如果函数并不关心 this
的话,仍然需要传入一个占位值,这时 null
可能是一个不错的选择就像代码所示。
然而,总是使用 null
来忽略 this
绑定可能产生一些副作用。如果某个函数确实使用了 this
(比如第三方库中的一个函数),那默认绑定规则则会把 this
绑定到全局对象(在浏览器中这个对象是 window
),这将导致不可预计的后果(比如修改全局对象)。
更安全的 this
一种“更安全”的做法是传入一个特殊的对象,把 this
绑定到这个对象不会对你的程序产生任何副作用。就像网络(以及军队)一样,可以创建一个 “DNZ”(demilitarized zone, 非军事区)对象——它就是一个空的非委托的对象。
如果再忽略 this
榜单时总是传入一个 DMZ 对象,就什么都不用担心了,因为任何对于 this
的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。
由于这个对象完全是一个空对象,可以用变量名 ø
来表示它(也可以用其他符号表示)。
无论叫什么,在 JavaScript
中创建一个空对象最简单的方法都是 Object.create(null)
。Object.create(null)
和 {}
很像,但是并不会创建 Object.prototype
这个委托,所以 {}
“更空”。
function foo(a, b) {
console.log("a:" + a + ',b:' + b)
}
// 创建 DMZ 空对象
var ø = Object.create(null)
// 把数组展开成参数
foo.apply(ø,[2,3])
// 使用bind(..)进行柯里化
var bar = foo.bind(ø,2)
bar(3)
---------------------------------------------
> a:2,b:3
> a:2,b:3
使用变量名 ø
不仅让函数变得更加 “安全”,而且可以提高代码的可读性,因为 ø
表示“我希望 this
是空”,这比 null
的含义更清楚。
间接引用
另一个需要注意的是,(有意或着无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:
function foo(a, b) {
console.log(this.a)
}
var a = 2
var o = {
a:3,
foo:foo
}
var p ={
a:4
}
o.foo();
(p.foo = o.foo)();
-----------------------------------------
> 3
> 2
赋值表达式 p.foo = o.foo
的返回值是目标函数的引用,因此调用位置是 foo()
而不是 p.foo
或者 o.foo
。根据之前说的,这里会应用默认绑定。
注意:对于默认绑定来说,决定 this
绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this
会被绑定到 undefined
,否则 this
会被绑定到全局对象。
软绑定
之前提到硬绑定这种方式可以把 this
强制绑定到指定的对象(除了使用 new
时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改 this
。
如果可以给默认绑定指定一个全局对象和 undefined
意外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this
的能力。
可以通过一种被称为软绑定的方法来实现想要的效果:
if (!Function.prototype.softBind) {
Function.prototype.softBind = function (obj) {
var fn = this
// 捕获所有 curried 参数
var curried = [].slice.call(arguments, 1)
var bound = function () {
return fn.apply(
(!this || this === (window || global)) ?
obj : this.curried.concat.apply(curried, arguments)
);
}
bound.prototype = Object.create(fn.prototype)
return bound
}
}
除了软绑定之外,softBind(..)
的其他原理和 ES5 内置的 bind(..)
类似。它会对指定的函数进行封装,首先检查调用时的 this
, 如果 this
绑定到全局对象或者 undefined
,那就把指定的默认对象 obj
绑定到 this
,否则不会修改 this
。此外,这段代码还支持可选的柯里化。
接下来看看 softBind
是否实现了软绑定功能:
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind(obj)
fooOBJ() // name: obj
obj2.foo = foo.softBind(obj)
obj2.foo() // name: obj2
fooOBJ.call(obj3) // name: obj3
setTimeout(obj2.foo, 10) // name: obj
可以看到,软绑定版本的 foo()
可以手动将 this
绑定到 obj2
或者 obj3
上,但如果应用默认绑定,则会将 this
绑定到 obj
。