call、apply和bind方法

总结

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

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

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

  • 不同点:applycall临时改变一次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

posted @ 2022-05-23 18:44  Scok  阅读(40)  评论(0编辑  收藏  举报