• 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

  • 相关阅读:
    Java实现 LeetCode 173 二叉搜索树迭代器
    PHP array_reverse() 函数
    PHP array_replace_recursive() 函数
    PHP array_replace() 函数
    PHP array_reduce() 函数
    PHP array_rand() 函数
    C# 通配符转正则
    win10 uwp 验证输入 自定义用户控件
    win10 uwp 验证输入 自定义用户控件
    win10 uwp 验证输入 自定义用户控件
  • 原文地址:https://www.cnblogs.com/Scooby/p/16302392.html
Copyright © 2020-2023  润新知