JavaScript中call,apply和bind方法模拟实现
本文最后更新于
2018-01-19, 文中内容可能已过时,请注意甄别~
本内容参考 mqyqingfeng 和 MDN 上关于 bind 的实现,进一步深入了解其本质。欢迎学习,不断进步~
看到一个特有意思的话,贴上方便理解:
猫吃鱼,狗吃肉,奥特曼打小怪兽。
有天狗想吃鱼了
猫.吃鱼.call(狗,鱼)
狗就吃到鱼了
猫成精了,想打怪兽
奥特曼.打小怪兽.call(猫,小怪兽)
call⌗
call和apply类似,都是立即执行函数,除了参数列表不一样其他都差不多,主要看你的参数是什么形式
// 简化打印
var log = console.log.bind(console);
// 每个函数都是构造函数 Function 的实例对象,
// bar.call或者bar.apply函数中的this自然也是指向该实例对象bar的
// 模拟call方法,首先传入一个参数,考虑参数的边界值,null的话为window;
// 将子函数当做需要继承的函数一个对象属性,此处的bar作为一个foo的属性;
// 执行完了删除即可;
/**
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};
**/
Function.prototype.call2 = function(context) {
var context = context || window;
context.fn = this;
// 通过从arguments对象中循环取值放到数组中,解决不定参数问题
var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push(`arguments[${ i }]`)
}
// eval 方式待优化
var result = eval(`context.fn(${args})`);
delete context.fn;
return result;
}
// test
var value = 2;
var foo = {
value: 1
}
function bar(name, age) {
log(this.value, name, age)
return {
value: this.value,
name: name,
age: age
}
}
bar.call(null); // 2
bar.call2(foo, '小米', 19)
apply⌗
var log = console.log.bind(console);
Function.prototype.apply2 = function(context, arr) {
var context = context || window;
context.fn = this;
var result;
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;
}
// test
var foo = {
value: 1
}
function bar() {
log(this.value, [].slice.call(arguments))
}
bar.apply2(foo, ['kevin', '123']);
}
bind⌗
直接调用bind我们发现会返回一个新函数,这就是它和call & apply不一样的地方,当我不想改变上下文环境立即执行,而是想在回调中执行的时候,bind的方法是你最好的选择。另外bind和call一样,参数是列表形式传递。
// 参考MDN中bind的写法
// 引入了Array.prototype.slice(), Array.prototype.concat(), Function.prototype.call() 和 Function.prototype.apply()
// 返回一个函数、传入不定参数
// 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
// 也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效
var log = console.log.bind(console);
Function.prototype.bind2 = function (context) {
// 检测调用bind方法的必须是函数
// !this instanceof Function 写成这种是不是可以
if (typeof this !== "function") {
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
// 获取bind函数从第二个参数到最后一个参数,这里和call的处理是一致的
var args = Array.prototype.slice.call(arguments, 1);
// 直接将fBound.prototype = this.prototype,我们直接修改fBound.prototype的时候,也会影响函数的prototype值被修改。我们通过一个空函数来进行中转
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
// fNOP.prototype = this.prototype; 就是将 this.prototype 原型对象作为 fNOP.prototype 的原型对象,
// 也就是 this.prototype 和 fNOP.prototype 指向同一个对象。
// 像 var f = new fNOP(); 之后找原型链上的属性,就是通过 f.__proto__,因为 f.__proto__ == fNOP.prototype == this.prototype
// 就会去 this.prototype 上找属性了
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
return 2333
}
var bindFoo = bar.bind2(foo, 'jiangshan');
var obj = new bindFoo();
log(obj) // {habit: 'shopping'}
Read other posts