手写js-call&apply模拟实现
call
的用途:
总结
- 改变this中的this指向
- 执行this
举个🌰
let obj = {
value: 1
};
let fn1 = function() {
console.log(this.value);
};
fn1.call(obj); // 1
1.改变this中的this指向
call的this
指向fn1,因此改变fn1的this
指向obj
2.执行this
执行fn1,此时fn1的this
指向obj
为了更好理解,来一道奇奇怪怪的题
let fn1 = function () {
console.log(1);
};
let fn2 = function () {
console.log(2);
};
fn1.call.call.call(fn2);
按照运算符执行顺序,从左往右执行。
前面这一堆
fn1.call.call.call
根据原型链机制,相当于从函数中不断找Function.prototype.call
这个方法
let fn1 = function () {
console.log(1);
};
let fn2 = function () {
console.log(2);
};
fn1.call.call.call(fn2);
//=> call.call(fn2);
1.改变this中的this指向
第二个call的this
指向第一个call,因此改变第一个call的this
指向fn2
2.执行this
执行第一个call,此时第一个call的this
指向fn2
打印结果为2
模拟实现
1.改变this中的this指向
2.执行改变指向后的this
Function.prototype.myCall = function (context) {
// 此时this:fn1
// 目的是修改this中的this 即fn1中的this
// context.fn1 => context.this
// 这样写是会报错的,可以这样
context.fn = this;
// 注意,这里是给context添加了一个属性存放fn1(this)
// 后面是需要清除的
// 2.执行改变指向后的this
context.fn();
delete context.fn;
}
var obj = {
value: 1
};
var fn1 = function() {
console.log(this.value);
};
fn1.myCall(obj); //=> 1
完善
增加对原始数据类型的包装,如果传入null作为上下文,this中的this指向window
var fn1 = function () {
console.log(this);
}
Function.prototype.myCall = function (context) {
context.fn = this;
context.fn();
delete context.fn;
}
fn1.myCall('abc') //=> Uncaught TypeError:context.fn is not a function
// 还有一种情况,在非严格模式下传入null,this中的this指向window
var fn1 = function () {
console.log(this);
}
Function.prototype.myCall = function (context) {
// 存在就包装成对象,不存在就指向window
context = context ? Object(context): window;
context.fn = this;
context.fn();
delete context.fn;
}
fn1.myCall('abc'); //=> String {"abc", fn: ƒ}
call方法是可以传入参数的,并且还应该有返回值
var obj = {
value: 1
};
var fn1 = function(x, y) {
console.log(this.value, x, y);
return x + y;
};
var result = fn1.call(obj, 1, 2); // 1 1 2
console.log(result); //=> 3
继续完善
// 使用ES6的展开运算符非常方便
Function.prototype.myCall = function (context, ...arg) {
context = context ? Object(context): window;
context.fn = this;
//保存返回值,因为要删去 context.fn所以不能直接return
var result = context.fn(...arg);
delete context.fn;
return result;
}
var obj = {
value: 1
};
var fn1 = function(x, y) {
console.log(this.value, x, y);
return x + y;
};
var result = fn1.myCall(obj, 1, 2); // 1 1 2
console.log(result); //=> 3
但是call
方法是ES3的方法,如果不使用ES6如何解决传参问题?
// 需要实现的目的
var result = context.fn(argments[0], arguments[1],...);
Function.prototype.myCall = function (context) {
context = context ? Object(context): window;
context.fn = this;
var args = [];
// 注意arguments应该从1开始
for(var i = 1; i < arguments.length; i++) {
// 存储字符串['arguments[0]',...]
args.push('arguments[' + i +']');
}
// 字符串拼接的时候会自动调用Array.toString方法
// ['arguments[0]',...] => 'arguments[0], ...'
// 拼接以后使用eval方法执行
var result = eval('context.fn(' + args + ')');
delete context.fn;
return result;
}
核心思想就是拼凑出’context.fn(arguments[0], …)’,然后eval执行
apply实现
apply其实是类似的,与call的区别就在于传参形式,传入的应该是一个数组
var result = fn1.apply(obj, [1, 2]); 效果与 fn1.call(obj,1,2); 是等价的
Function.prototype.myApply = function (context, arr) {
context = context ? Object(context): window;
context.fn = this;
var result = null;
if (!arr) {
result = context.fn();
} else {
var args = [];
for(var i = 0; i < arr.length; i++) {
args.push('arr[' + i +']');
}
result = eval('context.fn(' + args +')');
}
delete context.fn;
return result;
}
var obj = {
value: 1
};
var fn1 = function(x, y) {
console.log(this.value, x, y);
return x + y;
};
var result = fn1.myApply(obj, [1, 2]); // 1 1 2
console.log(result); //=> 3
核心思想就是拼凑出 ‘context.fn(arr[0], arr[1], …)’