通俗易懂之JavaScript手动实现call方法
📖明白call方法作用,并据此梳理脉络
-
call方法作用:
我们调用call方法的目的,即改变函数中this的指向。 -
梳理脉络:
- 前提:调用call方法的函数不能用箭头函数(ES6新增)来声明。这里并不是说箭头函数不能调用call方法,也可以调用,但是没有意义。因为箭头函数比较特殊,它的this在函数声明时就已经确定了,即函数声明时所处的对象,是无法被改变的。
光说不练假把式,让代码说话:
var name = 'Jack'; var obj = {name:'Bob'}; var showName = () => { // 用ES6箭头函数来声明 console.log(this.name); }; var showName2 = function() { // ES5中函数声明方式 console.log(this.name); }; showName.call(obj); // 输出'Jack' showName2.call(obj); // 输出'Bob'
可见,箭头函数调用call方法是没有意义的,无法改变this的指向。
- 手写思路:拿上述showName2函数举例。既然目的是改变this的指向,其实这里用改变是不准确的,因为函数声明时,this并未确定。要明白只有函数执行时,this才被确定(箭头函数除外~),所以这里用‘指定’比较好,即目的为指定函数中this的指向。 这里要把showName2函数中的this指向call方法传入的第一个参数obj,我们可以考虑给obj添加一个属性fn,把showName2函数赋值给它,通过obj.fn()方式调用,即可达到目的。
光说不练假把式,让代码说话:
Function.prototype.myCall = function(context){// 在Function构造函数的原型上添加我们自己的call方法:myCall const content = context || window; /* 若有传参,则为context;若无传参,则为window。 例如showName2.myCall(obj),则content就指向obj */ const fn = Symbol(); // 这里用Symbol类型(ES6新增基本类型)声明一个变量,确保独一无二,作为接下来给content新添加属性的key content[fn] = this; /* 这里的this,谁调用myCall方法,指的就是谁。 例如showName2.myCall(obj),执行完这行代码,即给obj新增了一个fn属性,值为showName2函数 */ const result = content.fn(...Array.from(arguments).slice(1)); /* 通过content.fn()方式调用方法,this被绑定为content即obj。 把参数传递进去就可以了,...是ES6的扩展运算符,Array.from()也是ES6语法,把类数组对象转为数组,因为第一个参数是this,所以把它剥掉 */ delete content[fn]; // 删除添加的临时属性 return result; } showName2.myCall(obj); // 输出'Bob'
以上就完成了我们自己的call方法,其中有用到了ES6语法。如不想用ES6语法,可参照如下代码:
Function.prototype.myCall = function(context){ var content = context || window; var fn = 'fn'+Math.random(); while(o1.hasOwnProperty(fn)){ fn = 'fn'+Math.random(); } content[fn] = this; var args = []; for(var i=1,length=arguments.length;i<length;i++){ args.push('arguments['+i+']'); } var result = eval('content.fn('+args+')');/* eval函数把字符串解析为js代码来执行,所以上面在args中push字符串格式 */ delete content[fn]; return result; } showName2.myCall(obj); // 输出'Bob'
至此结束,与诸君共勉~
- 前提:调用call方法的函数不能用箭头函数(ES6新增)来声明。这里并不是说箭头函数不能调用call方法,也可以调用,但是没有意义。因为箭头函数比较特殊,它的this在函数声明时就已经确定了,即函数声明时所处的对象,是无法被改变的。