上篇《2.2 this
全面解析》了解到函数调用中 this
绑定的四条规则,找到函数的调用位置并判断应当应用哪条规则。这篇学习假设某个调用位置可以应用多条规则,就必须给这些规则设定优先级。
毫无疑问,默认绑定的优先级是四条规则中最低的,所以可以先不考虑它。
那么隐式绑定和显式绑定哪个优先级更高?
function foo() {
console.log(this.a)
}
var obj1 = {
a: 2,
foo: foo
}
var obj2 = {
a: 3,
foo: foo
}
obj1.foo() // 2
obj2.foo() // 3
obj1.foo.call(obj2) // 3
obj2.foo.call(obj1) // 2
结果:显式绑定优先级更高,也就是说在判断时应当先考虑是否可以应用显式绑定。
现在需要搞清楚 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)
console.log(obj1.a) //2
console.log(bar.a) //4
结论:new
绑定比隐式绑定优先级高
new
绑定和显式绑定谁的优先级更高呢?
拓展:
new
和call
/apply
无法一起使用,因此无法通过new foo.call(obj1)
来直接测试。但是可以使用硬绑定来测试它们的优先级。
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) //4
console.log(baz.a) //4
解析:
bar
被硬绑定到obj1
上,但是new bar(3)
并没有把obj1.a
修改为3
new
修改了硬绑定(到obj1
的)调用bar(...)
中的this
- 因为使用了
new
绑定,得到了一个名字为baz
的新对象,并且baz.a
的值是3
再回头看看之前介绍的 “裸” 辅助函数 bind
:
function bind(fn, obj) {
return function () {
fn.apply(obj, arguments)
}
}
因为看起来在辅助函数中 new
操作符的调用无法修改 this
绑定,但是在刚才的代码中 new
确实修改了 this
绑定。
实际上, ES5 中内置的 Function.prototype.bind(...)
更加复杂。
下面是引用 MDN 提供的一种 bind(...)
实现:
if (!Function.prototype.bind) {
Function.prototype.bind == function (oThis) {
if (typeof this !== "function") {
// 与 ECMAScript 5 最接近的内部 IsCallable 函数
throw new TypeError(
"Function.prototype.bind - what is trying " + "to be bound is not callable"
)
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () { },
fBound = function () {
return fToBind.apply((
this instanceof fNOP && oThis ? this : oThis
), aArgs.concat(
Array.prototype.slice.call(arguments)
))
}
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
return fBound
}
}
拓展:
这种
bind(..)
是一种polyfill
代码 (polyfill
就是我们常说的刮墙用的腻子,polyfill
代码主要用于旧浏览器的兼容,比如说在旧的浏览器中并没有内置bind
函数,因此可以使用polyfill
代码在旧浏览器中实现新的功能),对于new
使用的硬绑定函数来说,这段polyfill
代码和 ES5 内置的bind(..)
函数并不完全相同。由于polyfill
并不是内置函数,所以无法创建一个不包含.prototype
的函数。因此会具有一些副作用。如果你要在new
中使用硬绑定函数并且依赖polyfill
代码的话,一定要非常小心。
下面是 new
修改 this
的相关代码:
this instanceof fNOP && oThis ? this : oThis
// ... 以及
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
简单来说,这段代码会判断硬绑定函数是否被 new
调用,如果是的话就会使用新创建的 this
替换硬绑定的 this
。
那为什么要在
new
中使用硬绑定函数呢? 直接使用普通函数不是更简单吗?
之所以要在new
中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new
进行初始化时就可以只传入其余的参数。bind(...)
的功能之一就是可以把除了第一个参数(第一个参数用于绑定this
)之外的其他参数都传给下层的参数(这种技术称为“部分应用”,是“柯里化”的一种)。
function foo(p1, p2) {
this.val = p1 + p2
}
// 之所以使用 null 是因为在本例中并不关心硬绑定的 this 是什么
// 反正使用 new 时 this 会被修改
var bar = foo.bind(null, "p1")
var baz = new bar("p2")
console.log(baz.val)
---------------------------------------------
> p1p2
总结
可以按照下面的顺序来根据优先级来判断函数在某个调用位置应用的是哪条规则:
- 函数是否在
new
中调用(new
绑定)?如果是的话this
绑定的是新创建的对象(var bar = new foo()
) - 函数是否通过
call
、apply
(显式绑定) 或者硬绑定调用?如果是的话,this
绑定的是指定的对象(var bar = foo.call(obj2)
) - 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,
this
绑定的是哪个上下文对象(var bar = foo()
) - 如果都不是的话,使用默认绑定。如果再严格模式下,就绑定到
undefined
, 否则绑定到全局对象(var bar = foo()
)