超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

apply.call、call.call、call.apply、call.bind

这三个函数都用于改变函数中 this 指向,但是稍有不同,可以分为两组:all/applybind

call/apply

callapply 具有相似的行为,只是接收参数的方式不同,call 方法接收若干个参数的列表,而 apply 接收包含多个参数的数组或类数组对象:

fun.call(thisArg[, arg1[, arg2[, …]]])
fun.apply(thisArg, [argsArray])

polyfill

为了更深一步了解这个函数,我们来尝试实现它,

我们要绑定这个函数到 thisArg 这个很简单,直接把函数作为 thisArg 的属性,调用即可:

Function.prototype.myCall = function(thisArg) {
let fn = this;
thisArg.fn = fn;
let returnVal = thisArg.fn();
delete context.fn;
return returnVal;
}

不过这个实现并没有考虑传入额外的参数的情况,如果要传入额外的参数,这里我们可以使用展开运算符 ...

Function.prototype.myCall = function(thisArg, ...args) {
let fn = this;
thisArg.fn = fn;
let returnVal = thisArg.fn(...args);
delete context.fn;
return returnVal;
}

当然,如果不允许用展开运算符的话,可以用 eval 组装语句:

returnVal = eval(`thisArg[tmpKey](${args.join(',')})`);

如果熟悉 Function 的构造函数(new Function ([arg1[, arg2[, ...argN]],] functionBody)),也可以用其来实现,但是需要注意的是 Function 构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,这样当我们使用的时候必须把使用的变量(thisArgargNames)存储到全局变量里,这样显然不是很方便,因此只是提一下这个思路:

// thisArg 和 argNames 必须存储为全局变量,否则无法访问到
window.thisArg = thisArg;
window.argNames = args.map((item, index) => 'arg' + index).join(',')
let returnVal = new Function(argNames, 'thisArg.fn(' + argNames + ')')(...args);

这样我们就基本实现了这个功能,但是考虑到 thisArg.fn = fn 可能和 thisArg 原有的属性冲突,这里可以引入 Symbol

let tmpKey = Symbol('myCall');
thisArg[tmpKey] = fn;

当然如果 Symbol 不允许使用,也可以轻松实现我们需要的不会冲突的键值:

var getUniqueKey = function(obj) {
let uniqueKey = 'symbol' + Math.random();
if (uniqueKey in obj) {
uniqueKey = getUniqueProperty(obj);
}
return uniqueKey;
}

接下来我们考虑到可能会执行中出现错误,最终 delete thisArg[tmpKey] 没有被执行,我们可以将其放置到 try...catch...finally 语句中执行:

try {
returnVal = thisArg[tmpKey](...args);
// 或者使用 eval 执行
//returnVal = eval(`thisArg[tmpKey](${args.join(',')})`);
} catch (e) {
// 把异常抛出外部处理
throw e;
} finally {
// 确保删除该属性
delete thisArg[tmpKey];
}

这样已经基本实现了,最后,我们可以为其做参数的校验,最终,实现的完整代码如下:

Function.prototype.myCall = function(thisArg, ...args) {
let fn = this;

// this 不是 function 则抛出 TypeError 错误。
if (typeof fn !== 'function') {
throw new Error('TypeError.')
}

// 值为 null 或 undefined 时,thisArg 指向全局对象
if (thisArg === null || thisArg === undefined) {
thisArg = window;
}

// 值为原始值(数字,字符串,布尔值),thisArg 指向该原始值的自动包装对象
if (typeof thisArg === 'boolean' || typeof thisArg === 'number' || typeof thisArg === 'string') {
thisArg = Object(thisArg);
}

// 为 thisArg 指定一个属性指向该函数,为了避免和原有属性冲突,使用 Symbol。
let returnVal, tmpKey = Symbol('myCall');

thisArg[tmpKey] = fn;
// 为了避免出现错误,将调用放到 try...catch 代码块中执行,并在 finally 中释放该属性。
try {
returnVal = thisArg[tmpKey](...args);
// 或者使用 eval 执行
//returnVal = eval(`thisArg[tmpKey](${args.join(',')})`);
} catch (e) {
// 把异常抛出外部处理
throw e;
} finally {
// 确保删除该属性
delete thisArg[tmpKey];
}
return returnVal;
}

调用测试:

var obj = {
name: 'obj'
};

var obj2 = {
name: 'obj2',
hello: function() {
console.log('hello, ' + this.name)
}
}

obj2.hello.myCall(obj);
// hello, obj

当然,这个实现只是一个思路,并不能适用于所有的场合,比如,当对象被密封 Object.isSealed 或者冻结 Object.isFrozen 的时候,thisArg[tmpKey] = fn 操作将会失败:

Object.seal(obj);
// Uncaught TypeError: thisArg[tmpKey] is not a function

不过,密封和冻结对象只针对当前对象,不影响其原型对象,可以将 thisArg[tmpKey] = fn 操作移动到其非密封和冻结原型对象上:

let protoObject = thisArg;
while (Object.isSealed(protoObject) || Object.isFrozen(protoObject)) {
protoObject = protoObject.__proto__ || {};
}
protoObject[tmpKey] = fn;
// ...其他操作
delete protoObject[tmpKey];

还有,如果目标方法牵扯操作 Symbol 也可能受到影响。

在类数组上调用数组方法

常见的类数组比如 arguments 等,只要拥有和数组一样的结构都可以调用数组的方法

function logArgs() {
console.log([].join.call(arguments, ','));
}
logArgs(1, 2, 3, 4);
// 1,2,3,4

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.callFunction.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() {
return console.log('My log output: ' + [].slice.call(arguments).join(', '));
}

Function.prototype.apply.call(log, console, [1, 2, 3]); // 第三个参数必须为数组或者省略
Function.prototype.call.call(log, console, 1, 2, 3);
Function.prototype.call.apply(log, [console, 1, 2, 3]); // 第二个参数必须为数组或者省略
Function.prototype.apply.apply(log, [console, [1, 2, 3]]); // 第二个参数必须为数组或者省略,若为数组的则其二个参数也必须为数组或者省略

这个可以简化代码,不过大大降低了可读性,理解就好,比如,反柯里化的实现代码:

Function.prototype.unCurry = function() {
var _fn = this;
return function() {
return Function.prototype.call.apply(_fn, arguments);
}
}

var push = Array.prototype.push.unCurry();
var obj = {};
push(obj, "a");
console.log(obj); // Object {0: "a", length: 1}

上述示例将 Arraypush 函数推广到了对象上,在对象上调用 push 函数。

bind

bind 同样是改变 this 指向的,和call/apply 不同点在于不会立即执行函数,而是提供了一个绑定了新的this值的函数。

polyfill

可以直接使用 apply 方法实现:

if (!Function.prototype.bind) {
Function.prototype.bind = function(){
var fn = this, presetArgs = [].slice.call(arguments);
var context = presetArgs.shift();
return function() {
return fn.apply(context, presetArgs.concat([].slice.call(arguments)));
};
};
};

但是考虑到可能作为构造函数使用,需要做一个改进:

if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
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 () {
// this instanceof fNOP ? this : oThis || this
// 当作为构造函数时,this 指向当前对象,避免绑定到错误对象,
return fToBind.apply(this instanceof fNOP ? this : oThis || this,
aArgs.concat(Array.prototype.slice.call(arguments)));
};

// 避免 new 调用的时候无法得到原函数的原型属性
// 修改 fBound 的原型指向原有函数的原型的复制,避免修改新函数的原型影响到原有的函数的原型
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

return fBound;
};
}

这里解释下其中的几个要点:
1.typeof this !== "function" 检查保证要绑定的是个函数,否则抛出异常。

if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}

2.bind 函数要求当函数被作为构造器的时候,忽略提供的 this 值,因此这里做一个检测,当 this instanceof fNOP 此时 this 指向的是 fBound 的原型,这种情况说明函数被作为构造函数调用,因此此时让 this 指向自身。仅当 this is not instanceof fNOP 才起作用.

fBound = function () {
// this instanceof fNOP ? this : oThis || this
// 当作为构造函数时,this 指向当前对象,避免绑定到错误对象,
return fToBind.apply(this instanceof fNOP ? this : oThis || this,
aArgs.concat(Array.prototype.slice.call(arguments)));
};

3.之前的函数可能存在原型所以需要为新函数增加原型,但是为了避免修改新函数,设置原型为之前函数原型对象的拷贝:

// 避免 new 调用的时候无法得到原函数的原型属性
// 修改 fBound 的原型指向原有函数的原型的复制,避免修改新函数的原型影响到原有的函数的原型
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

当然这个实现并不完善,主要问题有:函数存在 prototype 属性、函数的length 不正确。

配合回调函数

在一些回调函数中,如 setTimeout 中,this 关键字会指向全局对象,这个时候可以使用 bind 函数提前绑定 this 指向。

var obj = {
name: 'obj',
sayHello:function() {
console.log('hello, ' + this.name);
}
}

var name="window";
setTimeout(obj.sayHello, 100);
// hello, window
setTimeout(obj.sayHello.bind(obj), 100);
// hello, 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);
var arrLike = {0:"a", 1:"b", 2:"c", length:3};

var arr = arrayLikeToArray(arrLike);
console.log(Object.prototype.toString.call(arr)) // "[object Array]"

同样的 Function.prototype.apply.bind 功能类似,不再说明。

这里再看一下 Function.prototype.bind.call,先看个例子:

Function.prototype.bind.call(log, console)(1, 2, 3)

//这等价于
log.bind(console)(1, 2, 3);

可以改变一个函数的 this 到特定的对象上,并获得在这个对象执行这个操作的函数,如上述的 push 示例也可以这样改造成一个 obj 的专有 push 方法:

var obj = {};
var pushToObj = Function.prototype.bind.call(Array.prototype.push, obj);
pushToObj("a");
console.log(obj); // Object {0: "a", length: 1}

参考