这三个函数都用于改变函数中 this
指向,但是稍有不同,可以分为两组:all/apply
和 bind
。
call/apply
call
和 apply
具有相似的行为,只是接收参数的方式不同,call
方法接收若干个参数的列表,而 apply
接收包含多个参数的数组或类数组对象:
fun.call(thisArg[, arg1[, arg2[, …]]])
fun.apply(thisArg, [argsArray])
polyfill
为了更深一步了解这个函数,我们来尝试实现它,
我们要绑定这个函数到 thisArg
这个很简单,直接把函数作为 thisArg
的属性,调用即可:
Function.prototype.myCall = function(thisArg) { |
不过这个实现并没有考虑传入额外的参数的情况,如果要传入额外的参数,这里我们可以使用展开运算符 ...
:
Function.prototype.myCall = function(thisArg, ...args) { |
当然,如果不允许用展开运算符的话,可以用 eval
组装语句:
returnVal = eval(`thisArg[tmpKey](${args.join(',')})`); |
如果熟悉 Function
的构造函数(new Function ([arg1[, arg2[, ...argN]],] functionBody)
),也可以用其来实现,但是需要注意的是 Function
构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,这样当我们使用的时候必须把使用的变量(thisArg
、argNames
)存储到全局变量里,这样显然不是很方便,因此只是提一下这个思路:
// thisArg 和 argNames 必须存储为全局变量,否则无法访问到 |
这样我们就基本实现了这个功能,但是考虑到 thisArg.fn = fn
可能和 thisArg
原有的属性冲突,这里可以引入 Symbol
:
let tmpKey = Symbol('myCall'); |
当然如果 Symbol
不允许使用,也可以轻松实现我们需要的不会冲突的键值:
var getUniqueKey = function(obj) { |
接下来我们考虑到可能会执行中出现错误,最终 delete thisArg[tmpKey]
没有被执行,我们可以将其放置到 try...catch...finally
语句中执行:
try { |
这样已经基本实现了,最后,我们可以为其做参数的校验,最终,实现的完整代码如下:
Function.prototype.myCall = function(thisArg, ...args) { |
调用测试:
var obj = { |
当然,这个实现只是一个思路,并不能适用于所有的场合,比如,当对象被密封 Object.isSealed
或者冻结 Object.isFrozen
的时候,thisArg[tmpKey] = fn
操作将会失败:
Object.seal(obj); |
不过,密封和冻结对象只针对当前对象,不影响其原型对象,可以将 thisArg[tmpKey] = fn
操作移动到其非密封和冻结原型对象上:
let protoObject = thisArg; |
还有,如果目标方法牵扯操作 Symbol 也可能受到影响。
在类数组上调用数组方法
常见的类数组比如 arguments
等,只要拥有和数组一样的结构都可以调用数组的方法
function logArgs() { |
apply.call、call.apply、apply.apply、call.call
之前在几个地方看到这种绕圈子的用法,绕了好久,这里以 Function.prototype.apply.call(log, console, args)
为例分析一下:
1.Function.prototype.apply
等价于: funcApply.call(log, console, args)
2.去掉 call
等价于:log.funcApply(console, args)
3.去掉 apply
等价于 console.log(args)
也就是说,起的作用是,把第一个函数参数(log
)的 this
指针 绑定到第二个对象参数上,并把第三个参数作为参数传入执行。
同理,apply
函数的第二个参数是数组,因此要这样使用:Function.prototype.call.apply(log, [console, args])
当然 Function.prototype.call.call
和 Function.prototype.apply.apply
也是这样,不再详细解释,总结如下:
Function.prototype.apply.call(func, thisArg, callFuncArgs);
Function.prototype.call.call(func, thisArg, callFuncArgs);
Function.prototype.call.apply(func, [thisArg, callFuncArgs]);
Function.prototype.apply.apply(func, [thisArg, callFuncArgs]);
var log = function() { |
这个可以简化代码,不过大大降低了可读性,理解就好,比如,反柯里化的实现代码:
Function.prototype.unCurry = function() { |
上述示例将 Array
的 push
函数推广到了对象上,在对象上调用 push
函数。
bind
bind 同样是改变 this 指向的,和call/apply 不同点在于不会立即执行函数,而是提供了一个绑定了新的this值的函数。
polyfill
可以直接使用 apply
方法实现:
if (!Function.prototype.bind) { |
但是考虑到可能作为构造函数使用,需要做一个改进:
if (!Function.prototype.bind) { |
这里解释下其中的几个要点:
1.typeof this !== "function"
检查保证要绑定的是个函数,否则抛出异常。
if (typeof this !== "function") { |
2.bind
函数要求当函数被作为构造器的时候,忽略提供的 this
值,因此这里做一个检测,当 this instanceof fNOP
此时 this
指向的是 fBound
的原型,这种情况说明函数被作为构造函数调用,因此此时让 this
指向自身。仅当 this is not instanceof fNOP
才起作用.
fBound = function () { |
3.之前的函数可能存在原型所以需要为新函数增加原型,但是为了避免修改新函数,设置原型为之前函数原型对象的拷贝:
// 避免 new 调用的时候无法得到原函数的原型属性 |
当然这个实现并不完善,主要问题有:函数存在 prototype 属性、函数的length 不正确。
配合回调函数
在一些回调函数中,如 setTimeout 中,this 关键字会指向全局对象,这个时候可以使用 bind 函数提前绑定 this 指向。
var obj = { |
call.bind、bind.call、apply.bind、bind.apply
与上面类似,用于把更改函数参数的 this
指针,Function.prototype.call.bind(func)
实际上等价于 func.call
var arrayLikeToArray = Function.prototype.call.bind(Array.prototype.slice); |
同样的 Function.prototype.apply.bind
功能类似,不再说明。
这里再看一下 Function.prototype.bind.call
,先看个例子:
Function.prototype.bind.call(log, console)(1, 2, 3) |
可以改变一个函数的 this
到特定的对象上,并获得在这个对象执行这个操作的函数,如上述的 push
示例也可以这样改造成一个 obj
的专有 push
方法:
var obj = {}; |