JavaScript - 模拟 apply、bind

358 阅读4分钟

这是一首 简单的小情歌~~~

青玉案·元夕 东风夜放花千树。更吹落、星如雨 宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞 蛾儿雪柳黄金缕。笑语盈盈暗香去 众里寻他千百度。蓦然回首,那人却在,灯火阑珊处

原生的方法

小彩蛋

无意中发现,有了这个可以直接将 demo 示例的地址放置在 blog 中啦,点开就能看到效果了,舒服啊。

call 与 apply

原生的 callapply 的功能基本相同,只是在使用时,他们的参数不同

// apply 
fun.apply(thisArg, [argsArray]);

// call
fun.call(thisArg, arg1, arg2, ...)

// apply 的第二个参数是数组或者类数组对象,作为 fun函数 调用的参数
// call 的 arg1,arg2... 参数列表 

callapply 都有 thisArg 参数,参考下 MDN 上的定义

在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。

总结如下

  • callapply 都可以指定 函数执行时的 this 值,通过第一个参数指定
  • 非严格模式,第一个参数为 nullundefinedthis 指向全局对象;
  • 第一个参数为原始值(数字,字符串,布尔值),this 指向原始值的自动包装对象
  • apply 通过数组的方式传递参数,call 通过列举方式传递参数

JavaScript-关于this绑定 中,有实现使用 apply 来 实现 bind 的功能,所以我们只需要将 callapply 模拟出来就可以实现 bind 功能啦 ~

鉴于 callapply 功能类似,本文只做了 apply 的模拟,有兴趣的同学可以自行实现 call 的模拟

开工

先来个简单的

//var name = 'global name';
function foo(){
    console.log(this.name);
}

var obj1 = {
    name: 'xiaodaidai'
};

var obj2 = {
    name: 'xiaopangzi'
};

Function.prototype.applyNew = function(thisArg){
    thisArg.fn = this;
    thisArg.fn();
    delete this.fn;
}

foo.applyNew(obj1); // xiaodaidai
foo.applyNew(obj2); // xiaopangzi

这一版是最简单的,我们可以发现他有许多不足,如

  • thisArg 为空时,程序出错,应该默认为全局对象
  • thisArg 为基本类型时,this未指向该原始值的自动包装对象
  • 无法给函数传递参数
  • 函数没有返回值

结合以下代码,看下原生的apply这几种情况的输出

function foo(name){
    console.log(name);
    console.log(this);
    console.log(this.toString());
    return name;
}

// thisArg 为空
var name1 =  foo.apply();                     // name1 = undefined
/* console 输出
   undefined
   Window {stop: ƒ, open: ƒ, alert: ƒ, confirm: ƒ, prompt: ƒ, …}
   [object Window] */

// thisArg 为基本类型
var name2 =  foo.apply(123);                  // name2 = undefined
/* console 输出
   undefined
   Number {[[PrimitiveValue]]: 123}
   123 */

// 带参数
var name3 =  foo.apply(123,['xiaodaidai']);  // name3 = xiaodaidai
/* console 输出
   xiaodaidai
   Number {[[PrimitiveValue]]: 123}
   123 */

thisArg 为 null 或者 undefined

这个比较简单,只要我们加个判断,如果为 null 或者 undefined ,我们只需要直接在全局对象上定义一个函数,然后调用

(function(global){
	Function.prototype.applynew = function(thisArg){  
	
		thisArg = thisArg || global; 

		thisArg.fn = this;

		thisArg.fn();

		delete thisArg[fn]; 
	}; 
})(window);

thisArg 为 基本类型

为基本类型,我们可以调用基本类型的包装函数,将它装箱(我使用了eval函数)

(function(global){
	Function.prototype.applynew = function(thisArg){  
	    // 让字符串首字母大写
		const capotalize = ([first,...rest]) =>{
			return first.toUpperCase() + rest.join('');
		};
        
        // 获取thisArg的类型,并将首字母大写;如 'number' -> 'Number'
		let objtype = capotalize(typeof thisArg);
        
        // 如果是基本类型(除去 null 和 undefined),装箱
		if(objtype !== 'Object' && objtype !=='Undefined' ){
			thisArg = eval('new ' + objtype + '('+ thisArg +')');
		}else{
			thisArg = thisArg || global;
		}
 
		thisArg.fn = this;

		thisArg.fn();

		delete thisArg[fn];
 
	}; 
})(window);    // 通过立即执行函数将全局对象传入,这里是global

传递参数、并返回值

(function(global){
	Function.prototype.applynew = function(thisArg,args){ 

		args = args || [];

		const capotalize = ([first,...rest]) =>{
			return first.toUpperCase() + rest.join('');
		};

		let objtype = capotalize(typeof thisArg);

		if(objtype !== 'Object' && objtype !=='Undefined' ){
			thisArg = eval('new ' + objtype + '('+ thisArg +')');
		}else{
			thisArg = thisArg || global;
		}

		var fn = Symbol('callback'); 

		thisArg[fn] = this;

		var returnValue = thisArg[fn](...args);

		delete thisArg[fn];

		return returnValue;
	}; 
})(window);

到这里 apply 的模拟就基本上告一个段落了。


使用 applynew 来 模拟 bind

这里直接借鉴 JavaScript-关于this绑定 中的代码,只需要替换下 apply -> applynew

(function(global){
	Function.prototype.applynew = function(thisArg,args){ 

		args = args || [];

		const capotalize = ([first,...rest]) =>{
			return first.toUpperCase() + rest.join('');
		};

		let objtype = capotalize(typeof thisArg);

		if(objtype !== 'Object' && objtype !=='Undefined' ){
			thisArg = eval('new ' + objtype + '('+ thisArg +')');
		}else{
			thisArg = thisArg || global;
		}

		var fn = Symbol('callback'); 

		thisArg[fn] = this;

		var returnValue = thisArg[fn](...args);

		delete thisArg[fn];

		return returnValue;
	}; 
    
    
	Function.prototype.bindnew = function(obj){
		var fn = this;
		return function(){
			return fn.applynew(obj,arguments);
		}
	};
})(window);

结尾

上面的实现还有许多问题

  • 使用了ES6的语法
  • 基本类型作为 thisArg 输出 this 属性上会看到函数
  • bindNew 应该也还有许多不严谨的地方

能力和时间有限,暂时就先这么处理。

完善

这个是完善版的callNew、apply

function capotalize(str){
    	var classArry = {};
    	"Boolean Number String".split(" ").forEach(function(item){
			classArry[item.toLowerCase()] = item;
    	}); 
    	return classArry[str];
    }


    Function.prototype.callNew = Function.prototype.callNew ||  function(context) {
    	var contextClass = capotalize(typeof context);
    	if(contextClass){
    		context = eval('new ' + contextClass + '(context)');
    	}else{
    		context = context || window;
    	}
        
        var args = [],
            result;
        context._fn_ = context._fn_ || this;
        if (arguments.length > 1) {
        	for(var i = 1; i < arguments.length; i++){
        		 args.push('arguments[' + i + ']');
        	} 
            result = eval('context._fn_(' + args + ')');
        }else{
        	 result = context._fn_();
        }  
        delete context._fn_;
        return result;
    }
    var obj = {
        val: 'obj1'
    };

    var person = {
        name: 'xiaodaidai',
        age: 19
    }

    function test(person) {
    	if(person){
    		console.log(person.name);
        	console.log(person.age);
    	} 
    	console.log(this);
        return this.val;
    }
    var value = test.callNew(obj, person);
    console.log(value);
 	console.log(test.callNew(obj));


    Function.prototype.applyNew = Function.prototype.applyNew ||  function(context, arry) {
        var contextClass = capotalize(typeof context);
    	if(contextClass){
    		context = eval('new ' + contextClass + '(context)');
    	}else{
    		context = context || window;
    	}
        var result, args = [];
        context._fn_ = context._fn_ || this;
        if (arry) {
        	for(var i = 0; i < arry.length; i++){
        		args.push('arry[' + i + ']');
        	}  
            result = eval('context._fn_(' + args + ')');

        } else {
            result = context._fn_();
        }
        delete context._fn_;
        return result;
    }
    var param = [];
    param.push(person);
    value = test.applyNew(obj, param);
    console.log(value);
    console.log(test.applyNew(obj));

demo展示

源码

参考文档

es5-apply

前端小密圈