前言
这一次,我们继续学习javaScript中另外两个东西,call和apply
每个函数都包含两个非继承而来的方法:call()方法和apply()方法。
正文内容
Call
首先搬出来它的定义
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
举个例子!
window.color = 'red';
document.color = 'yellow';
var s1 = {color: 'blue'};
function changeColor() {
console.log(this.color);
}
changeColor.call(); //red (默认传递参数)
changeColor.call(this); //red
changeColor.call(window); //red
changeColor.call(document); //yellow
changeColor.call(s1); //blue
效果显而易见,这个方法可以通过传入参数的不同,改变this的指向。
在这段代码中,不传参,不改变this 指向的时候,结果都是输出默认的 red
在我们传入 document 和 s1 对象时 ,this指针发生了改变,获取到了这些对象里的 color的值。
我们再举一个传入参数的例子
var Cat = {
words : '喵喵喵',
speak : function (say) {
console.log(say + ''+ this.words)
}
}
Cat.speak('Speak'); // 结果:Speak喵喵喵
var Dog = {
words:'汪汪汪'
}
// 将this的指向改变成了Dog
Cat.speak.call(Dog, 'Speak'); // 结果: Speak汪汪汪
这段代码首先通过传参方式 Cat.speak ( ); 其中 say 来自传入的参数,words来自 Cat中的赋值。
而后一句打印,通过 call 方式来改变了 Cat 中 this 的指向,传递的第一个参数代表 改变之后的 this 指向。所以输出打印的 say 来自传入的第二个参数,words来自改变之后的this指向的Dog
Apply
效果和 call 类似,直接上个例子
window.number = 'one';
document.number = 'two';
var s1 = {number: 'three' };
function changeNumber(){
console.log(this.number);
}
changeNumber.apply(); // one (默认传参)
changeNumber.apply(window); // one
changeNumber.apply(this); // one
changeNumber.apply(document); // two
changeNumber.apply(s1); // three
在这段代码中,不传参,不改变this 指向的时候,结果都是输出默认的 one
在我们传入 document 和 s1 对象时 ,this指针发生了改变,获取到了这些对象里的 number 的值。
再来一个例子
function Pet(words){
this.words = words;
this.speak = function () {
console.log( this.words)
}
}
function Dog(words){
//Pet.call(this, words); //使用call方式 结果: 汪汪汪
Pet.apply(this, arguments); //使用apply方式 结果: 汪汪汪
}
var dog = new Dog('汪汪汪');
dog.speak();
通过apply ,在执行 Pet()时, 使得 this 指针指向 Dog的参数列表 arguments
说到这,call 和 apply 的差别我们也能猜得到了
Call 和 Apply 的差别
apply()方法 接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
语法:apply([thisObj [,argArray] ]);
call()方法 第一个参数和apply()方法的一样,但是传递给函数的参数必须列举出来。
语法:call([thisObject[,arg1 [,arg2 [,...,argn]]]]);
再来两个例子巩固一下
function add(c,d){
return this.a + this.b + c + d;
}
var s = {a:1, b:2 };
console.log(add.call(s,3,4)); // 1+2+3+4 = 10
console.log(add.apply(s,[5,6])); // 1+2+5+6 = 14
需要注意的是 apply传入的参数一定得是数组,call传入的必须列举
手动模拟实现
看到这里,终于大致弄明白了这两个方法,其实也就是改变了 this的指向,应该不是很难,动手实操一下加深印象
Call的模拟实现
尝试一
var num= 1;
var obj = {
num: 2
};
function fn() {
console.log(this.num);
};
fn(); //1
fn.call(obj); //2
首先分析一下流程,call 方法一共做了两件事
- 修改了 this 指向
- 执行了某个函数
那我们根据思路写一下试试
var num= 1;
var obj = {
num: 2
};
function fn() {
console.log(this.num);
};
----------------------上面的部分直接拿过来-------------------------------
Function.prototype.my_cell= function (obj) {
obj.fn = this; // 此时this就是函数fn
obj.fn(); // 执行fn
delete obj.fn; //删除fn
};
fn(); //1
fn.my_cell(obj); //2 调用我们自己写的 call 方法
写这段代码学到了一部分新知识
- 我们通过
Function.prototype.xxx
的形式绑定了my_cell 方法,使所有函数都可以直接访问被绑定的方法 fn.my_cell ( obj ) 属于隐式绑定
,所以在执行时 my_cell 时内部 this 指向 fn,后续的 obj.fn = this; 其实就是把 this 指向的 fn() 当作 obj 的一个属性,属性名叫 fn- 此时对象 obj 已经有了 fn 方法,执行 obj.fn,因为隐式绑定的问题,此时的 fn 内部的 this 指向 obj,所以 num 的值从 obj 中获得,输出结果 2
- 最后通过delete删除了 obj 上的 fn 方法,执行完不删除会导致obj上的属性越来越多,造成冗余
我们到目前为止,成功的改变了 this 的指向,但是目前还不可以接收参数,还得改改…
尝试二
思考过后,因为函数传递的参数我们也不能很好的确定,有多少我们也无法预测,突然想到函数有一个 arguments 属性,代指函数接收的所有参数,它是一个类数组,比如刚刚的代码我们拿来改改。
var num = 1;
var obj = {
num: 2
};
function fn() {
console.log(this.num);
};
----------------------上面的部分直接拿过来-------------------------------
Function.prototype.my_cell = function (obj) {
console.log(arguments);
};
fn(); //1
fn.my_cell(obj, "参数一", "参数二", "参数三");
输出结果正是我们所要的
可以很清楚的看到,arguments 里第一个参数,是我们需要让 this 指向的对象,从第二个参数开始,一直到最后一个参数为止,都是真正的参数。通过 for 循环把后面的东西拿出来
var num = 1;
var obj = {
num: 2
};
function fn() {
console.log(this.num);
};
----------------------上面的部分直接拿过来-------------------------------
Function.prototype.my_cell = function (obj) {
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
};
console.log(args);
};
fn();
fn.my_cell(obj, "参数一", "参数二", "参数三");
输出一下
嗯符合要求,但是 call 方法不接受整个数组为参数,还必须切开才行…
又是一阵查资料,发现了之前没有怎么学习的 eval 方法,没这方法还真不好折腾,举个例子认识一下
var fn = function (a, b, c) {
console.log(a + b + c);
};
var arr = [1, 2, 3];
fn(1, 2, 3);//6
eval("fn(" + arr + ")");//6
两种输出结果居然一样!?
细究之下,发现 eval 在执行时会将变量转为字符串,这里隐性执行了 arr.toString()
console.log([1, 2, 3].toString()); //"1,2,3"
console.log([1, 2, 3].join(',')); //"1,2,3"
这俩也是一样的
补充一下join方法是将数组元素通过指定符号分割,化成字符串
可以看出 eval 帮我们做了数组处理,这里就不需要再使用 join 方法了
因此eval("fn(" + arr + ")")
可以看成eval("fn(1,2,3)")
。
好家伙,这可太方便了。
到现在为止,我们代码长这样
var num = 1;
var obj = {
num: 2
};
function fn(value1, value2, value3) {
console.log(this.num + " " + value1 + " " + value2 + " " + value3);
}
Function.prototype.my_cell = function (obj) {
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
}
obj.fn = this; // 此时this就是函数fn
eval("obj.fn(" + args + ")"); // 执行fn
delete obj.fn; //删除fn
};
// fn();
fn.my_cell(obj, "参数一", "参数二", "参数三");
运行之后
嗯???这报错很显然是把 “参数一”看成了一个变量,我传递的不是个字符串吗,怎么变成变量了…
往上细看,是这里出了问题,这里已经把传进去的字符串解析了,下面再拿来用解析的时候自然就当作变量了…
args.push(arguments[i]);
那我们把这一句改一改
args.push("arguments[" + i + "]");
改成这样之后,就算他在 push 时先解析也无所谓了,eval 解析时遇到的是 arguments[1] ,arguments[2] 之类的东西,依然可以找得到具体的值。
于是代码就这样写了
var num = 1;
var obj = {
num: 2
};
function fn(value1, value2, value3) {
console.log(this.num + " " + value1 + " " + value2 + " " + value3);
}
Function.prototype.my_cell = function (obj) {
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push("arguments[" + i + "]");
}
obj.fn = this; // 此时this就是函数fn
eval("obj.fn(" + args + ")"); // 执行fn
delete obj.fn; //删除fn
}
// fn();
fn.my_cell(obj, "参数一", "参数二", "参数三");
结果也是我们想要的的
到这里,基本功能已经完成了,只需要考虑一些合法性问题,当 call 第一个参数为 undefined 或者 null 时,this 默认指向 window,我们再来稍加改进。
尝试三
我们传递一个 null 看看
fn.my_cell(null, "参数一", "参数二", "参数三");
报错了,解决方法很简单,通过一个三目判断实现
obj = obj ? Object(obj) : window;
这样就可以了,打印的结果是 1
所以模拟 call 函数的代码完成
var num = 1;
var obj = {
num: 2
};
function fn(value1, value2, value3) {
console.log(this.num + " " + value1 + " " + value2 + " " + value3);
}
Function.prototype.my_cell = function (obj) {
obj = obj ? Object(obj) : window;
var args = [];
for (var i = 1, len = arguments.length; i < len; i++) {
args.push("arguments[" + i + "]");
}
obj.fn = this;
eval("obj.fn(" + args + ")");
delete obj.fn;
}
// fn();
fn.my_cell(obj, "参数一", "参数二", "参数三");
Apply 的模拟实现
apply方法因为接受的参数是一个数组,所以模拟起来就更简单了,理解了call实现,也就可以少走一些坑。
var num = 1;
var obj = {
num: 2
};
function fn(value1, value2, value3) {
console.log(this.num + " " + value1 + " " + value2 + " " + value3);
}
Function.prototype.my_apply = function (obj, arr) {
obj = obj ? Object(obj) : window;
obj.fn = this;
if (!arr) { //判断数组是否为空
obj.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) { //遍历传入的数组
args.push("arr[" + i + "]");
}
eval("obj.fn(" + args + ")");
}
delete obj.fn;
}
fn.my_apply(obj, ["参数一", "参数二", "参数三"]);
结果,也符合我们的要求。
总结
到这里,我们也算是完成了对于 call 和 apply 的模拟,收获还是非常大的,撒花庆祝!!