call、apply和bind方法

总结#

  • 相同点:改变函数的this指向绑定到指定的对象上。

  • 相同点:三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。

  • 不同点:传参形式不同。三者主要区别在于第二个参数。callbind都为接受的是一个参数列表。call一次性传入所有参数,bind可以多次传参。而apply第二个参数是函数接受的参数,以数组的形式传入。

  • 不同点:applycall临时改变一次this指向,并立即执行。返回值为使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。而bind不会立即执行,返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

实现方式#

使用 new Function() 模拟实现的apply#

Copy
// 浏览器环境 非严格模式 /* 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#

Copy
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#

Copy
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#

Copy
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

posted @   Scok  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
CONTENTS