call、apply和bind方法
总结
-
相同点:改变函数的
this
指向绑定到指定的对象上。 -
相同点:三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
-
不同点:传参形式不同。三者主要区别在于第二个参数。
call
和bind
都为接受的是一个参数列表。call
一次性传入所有参数,bind
可以多次传参。而apply
第二个参数是函数接受的参数,以数组的形式传入。 -
不同点:
apply
和call
临时改变一次this指向,并立即执行。返回值为使用调用者提供的this
值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined
。而bind
不会立即执行,返回一个原函数的拷贝,并拥有指定的this
值和初始参数。
实现方式
使用 new Function()
模拟实现的apply
// 浏览器环境 非严格模式
/* es2020获取全局this对象 globalThis对象 */
function getGlobalObject(){
return this;
}
/**
* Function构造函数生成函数
* var sum = new Function('a', 'b', 'return a + b');
* console.log(sum(2, 6));
*
* 构造thisArg[ tempProperty_ ](argList)类型的函数并运行。
*
* */
function generateFunctionCode(argsArrayLength){
var code = 'return arguments[0][arguments[1]](';
for(var i = 0; i < argsArrayLength; i++){
if(i > 0){
code += ',';
}
code += 'arguments[2][' + i + ']';
}
code += ')';
// return arguments[0][arguments[1]](arg1, arg2, arg3...)
return code;
}
/***
*
* apply函数的实质是在传递的this对象上增加原对象上的函数新属性,并运行该属性。
*
* */
Function.prototype.applyFn = function apply(thisArg, argsArray){
// `apply` 方法的 `length` 属性是 `2`。
// 1.如果 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。
if(typeof this !== 'function'){
throw new TypeError(this + ' is not a function');
}
// 2.如果 argArray 是 null 或 undefined, 则
// 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
if(typeof argsArray === 'undefined' || argsArray === null){
argsArray = [];
}
// 3.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .
if(argsArray !== new Object(argsArray)){
throw new TypeError('CreateListFromArrayLike called on non-object');
}
if(typeof thisArg === 'undefined' || thisArg === null){
// 在外面传入的 thisArg 值会修改并成为 this 值。
// ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window
thisArg = getGlobalObject();
}
// ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。
thisArg = new Object(thisArg);
//绑定属性为调用的函数。
// 方法一:使用ES6 Symbol
// const _fn = Symbol('TEMP_PROPERTY');
// 缺点:兼容性问题 Symbol为ES6新增加的属性。
//方法二:使用时间随机函数
//const _fn = "_" + new Date().getTime()
//缺点:可能存在同名属性。
var __fn = '__' + new Date().getTime();
// 万一还是有 先存储一份,删除后,再恢复该值
var originalVal = thisArg[__fn];
// 是否有原始值
var hasOriginalVal = thisArg.hasOwnProperty(__fn);
thisArg[__fn] = this;
// 提供 `thisArg` 作为 `this` 值并以 `argList` 作为参数列表,调用 `func` 的 `[[Call]]` 内部方法,返回结果。
//运行该属性。
//方法一:ES6解构语法 const result = thisArg[ _fn ](...argList);
//方法二:new Function函数方法。兼容性更强。
// var result = thisArg[__fn](...args);
var code = generateFunctionCode(argsArray.length);
var result = (new Function(code))(thisArg, __fn, argsArray);
delete thisArg[__fn];
if(hasOriginalVal){
thisArg[__fn] = originalVal;
}
return result;
};
利用模拟实现的apply
模拟实现call
Function.prototype.callFn = function call(thisArg){
var argsArray = [];
var argumentsLength = arguments.length;
for(var i = 0; i < argumentsLength - 1; i++){
// push方法,内部也有一层循环。所以理论上不使用push性能会更好些。
// argsArray.push(arguments[i + 1]);
argsArray[i] = arguments[i + 1];
}
console.log('argsArray:', argsArray);
return this.applyFn(thisArg, argsArray);
}
// 测试例子
var doSth = function (name, age){
var type = Object.prototype.toString.call(this);
console.log(typeof doSth);
console.log(this === firstArg);
console.log('type:', type);
console.log('this:', this);
console.log('args:', [name, age], arguments);
return 'this--';
};
var name = 'window';
var student = {
name: '若川',
age: 18,
doSth: 'doSth',
__fn: 'doSth',
};
var firstArg = student;
var result = doSth.applyFn(firstArg, [1, {name: 'Rowboat'}]);
var result2 = doSth.callFn(firstArg, 1, {name: 'Rowboat'});
console.log('result:', result);
console.log('result2:', result2);
实现new
调用bind
Function.prototype.bindFn = function bind(thisArg){
if(typeof this !== 'function'){
throw new TypeError(this + ' must be a function');
}
// 存储调用bind的函数本身
var self = this;
// 去除thisArg的其他参数 转成数组
var args = [].slice.call(arguments, 1);
var bound = function(){
// bind返回的函数 的参数转成数组
var boundArgs = [].slice.call(arguments);
var finalArgs = args.concat(boundArgs);
// new 调用时,其实this instanceof bound判断也不是很准确。es6 new.target就是解决这一问题的。
if(this instanceof bound){
// 这里是实现上文描述的 new 的第 3 步
// 3.生成的新对象会绑定到函数调用的`this`。
var result = self.apply(this, finalArgs);
// 这里是实现上文描述的 new 的第 5 步
// 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),
// 那么`new`表达式中的函数调用会自动返回这个新的对象。
var isObject = typeof result === 'object' && result !== null;
var isFunction = typeof result === 'function';
if(isObject || isFunction){
return result;
}
return this;
}
else{
// apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果
return self.apply(thisArg, finalArgs);
}
};
// 这里是实现上文描述的 new 的第 1, 2, 4 步
// 1.创建一个全新的对象
// 2.并且执行[[Prototype]]链接
// 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
// self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作。
if(self.prototype){
// ES5 提供的方案 Object.create()
// bound.prototype = Object.create(self.prototype);
// 但 既然是模拟ES5的bind,那浏览器也基本没有实现Object.create()
// 所以采用 MDN ployfill方案https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
function Empty(){}
Empty.prototype = self.prototype;
bound.prototype = new Empty();
}
return bound;
}
es5-shim
的源码实现bind
var $Array = Array;
var ArrayPrototype = $Array.prototype;
var $Object = Object;
var array_push = ArrayPrototype.push;
var array_slice = ArrayPrototype.slice;
var array_join = ArrayPrototype.join;
var array_concat = ArrayPrototype.concat;
var $Function = Function;
var FunctionPrototype = $Function.prototype;
var apply = FunctionPrototype.apply;
var max = Math.max;
// 简版 源码更复杂些。
var isCallable = function isCallable(value){
if(typeof value !== 'function'){
return false;
}
return true;
};
var Empty = function Empty() {};
// 源码是 defineProperties
// 源码是bind笔者改成bindFn便于测试
FunctionPrototype.bindFn = function bind(that) {
// 1. 设置target保存this的值。
const target = this;
// 2. 如果 IsCallable (Target)为 false,则抛出 TypeError 异常。
if (!isCallable(target)) {
throw new TypeError('Function.prototype.bind called on incompatible ' + target);
}
// 3. 设 A 是一个新的(可能是空的)内部列表。
// 包含所有在 thisArg 之后提供的参数值(arg1、arg2 等),按顺序排列。
// 获取除thisArg之外的其他参数,转换成数组
var args = array_slice.call(arguments, 1);
// 4. 让 f 成为一个新的本机 ECMAScript 对象。
// 11.将F的[[Prototype]]内部属性设置为标准
// 15.3.3.1 中指定的内置函数原型对象。
// 12. 设置 F 的 [[Call]] 内部属性,如中所述
// 15.3.4.5.1。
// 13. 设置 F 的 [[Construct]] 内部属性,如中所述
// 15.3.4.5.2。
// 14. 设置 F 的 [[HasInstance]] 内部属性,如中所述
// 15.3.4.5.3。
var bound;
var binder = function () {
if (this instanceof bound) {
// 15.3.4.5.2 [[构造]]
// 当函数对象的[[Construct]]内部方法,
// 使用绑定函数创建的 F 被调用
// 参数列表 ExtraArgs,采取以下步骤:
// 1. 设target为F的值[[TargetFunction]]内部属性。
// 2. 如果target没有[[Construct]]内部方法, 抛出 TypeError 异常。
// 3. 设 boundArgs 为 F 的 [[BoundArgs]] 内部的属性。
// 4. 设 args 是一个新列表,包含与以相同的顺序列出boundArgs,后跟相同的
// 值与列表 ExtraArgs 的顺序相同。
// 5. 返回调用[[Construct]]内部的方法提供 args 作为参数的目标方法。
var result = apply.call(
target,
this,
array_concat.call(args, array_slice.call(arguments))
);
if ($Object(result) === result) {
return result;
}
return this;
} else {
// 15.3.4.5.1 [[Call]]
// 当函数对象的[[Call]]内部方法,F,
// 使用绑定函数创建的函数被调用
// 这个值和一个参数列表 ExtraArgs,如下
// 采取的步骤:
// 1. 设 boundArgs 为 F 的 [[BoundArgs]] internal 的内部属性。
// 2. 设 boundThis 为 F 的 [[BoundThis]] internal 的内部属性。
// 3. 设 target 为 F 的 [[TargetFunction]] internal 的内部属性。
// 4. 设 args 是一个新列表,包含与列表 boundArgs 相同的值,顺序相同。
// 跟与列表 ExtraArgs 相同的值,顺序相同。
// 5.返回调用target的[[Call]]内部方法的结果,提供boundThis作为this值,提供args作为参数。
// 等效:target.call(this, ...boundArgs, ...args)
return apply.call(
target,
that,
array_concat.call(args, array_slice.call(arguments))
);
}
};
// 15. 如果Target的[[Class]]内部属性是“Function”,那么
// a. 令 L 为 Target 的长度属性减去 A 的长度。
// b. 将 F 的长度自身属性设置为 0 或 L,以较大者为准。
// 16. 否则将 F 的长度自身属性设置为 0。
var boundLength = max(0, target.length - args.length);
// 17.将F的length自身属性的属性设置为values
// 在 15.3.5.1 中指定。
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
array_push.call(boundArgs, '$' + i);
}
// XXX 使用所需数量的参数构建动态函数是设置函数长度属性的唯一方法。
// 在启用内容安全策略的环境中(例如 Chrome 扩展程序),所有使用 eval 或 Function costructor 都会引发异常。
// 然而在所有这些环境中 Function.prototype.bind 存在。所以这段代码永远不会被执行。
// 这里是Function构造方式生成形参length $1, $2, $3...
bound = $Function('binder', 'return function (' + array_join.call(boundArgs, ',') + '){ return binder.apply(this, arguments); }')(binder);
if (target.prototype) {
Empty.prototype = target.prototype;
bound.prototype = new Empty();
Empty.prototype = null;
}
// TODO
// 18. 将 F 的 [[Extensible]] 内部属性设置为 true。
// TODO
// 19. 让 thrower 成为 [[ThrowTypeError]] 函数对象 (13.2.3)。
// 20. 调用 F 的 [[DefineOwnProperty]] 内部方法参数 "caller"。
// PropertyDescriptor {[[Get]]: thrower, [[Set]]:
// thrower, [[Enumerable]]: false, [[Configurable]]: false}, and false。
// 21. 调用 F 的 [[DefineOwnProperty]] 内部方法
// 参数 "arguments", PropertyDescriptor {[[Get]]: thrower,
// [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},
// 和错误。
// TODO
// 注意使用 Function.prototype.bind 创建的函数对象不
// 具有原型属性或 [[Code]]、[[FormalParameters]] 和
// [[Scope]] 内部属性。
// XXX 不能删除 pure-js 中的原型。
// 22. 返回 F。
return bound;
};
参考文档
面试官问:能否模拟实现JS的call和apply方法 https://juejin.cn/post/6844903728147857415
彻底弄懂bind,apply,call三者的区别 https://zhuanlan.zhihu.com/p/82340026
bind方法 https://juejin.cn/post/6844903718089916429
es5-shim
的源码实现bind
https://github.com/es-shims/es5-shim/blob/master/es5-shim.js#L201-L335