关于 call、apply、bind 之前写过一篇文介绍过,看这里,本文介绍代码实现。
一、call
ES5 实现
// 先将我们自己定义的myCall绑定到 Js的内置类 Function的原型上
Function.prototype.myCall = function( context ) {
// ctx值为context,如果没传context默认window
var ctx = context || window;
// 将参数context后面的参数转化成数组对象
var args = [];
for( var i = 1; i < arguments.length; i++ ){
args.push("arguments[" + i + "]");
}
// 防止与context原有的属性冲突,定义一个随机字符串
var symbol = Date.now().toString(32);
// this指向Function实例,即调用myCall的函数
// 将调用myCall的函数挂载到ctx[symbol]
ctx[symbol] = this;
// 通过eval调用ctx中symbol属性的函数,缓存在result变量中
var result = eval('ctx[symbol]('+args+')');
// 删除ctx上自己添加的属性symbol,保持与之前的context一样
delete ctx[symbol];
// 返回函数执行的结果
return result
}
- 将
myCall
方法定义在Function.prototype
上,每个定义的function
可以直接访问myBind
方法。 myCall
方法参数context
后面的参数转化成数组对象,被保存在args
中,这里可以用call
方法实现,但myCall
方法要实现的就是模拟call
,所以在这里不用call
方法。var symbol = Date.now().toString(32);
创建一个随机字符串作为ctx
对象的临时键名,防止与context原有的属性冲突- 通过
eval
调用ctx
中symbol
属性的函数,缓存在result
变量中 - 删除
ctx
上自己添加的属性symbol
,保持与之前的context
一样 - 最后返回函数执行的结果
result
测试
var a = 10;
var b = 20;
var obj = {
a : 1,
b : 2,
getStr: getStr
}
function getStr( c, d ){
return this.a + this.b + c + d;
}
console.log(getStr(3,4)); // 37
console.log(getStr.myCall(obj,3,4)); // 10
ES6 实现
Function.prototype.myCall = function( context, ...args ) {
let ctx = context || window;
let symbol = Symbol('symbol');
ctx[symbol] = this;
let result = ctx[symbol](...args);
delete ctx[symbol];
return result;
}
有 ES6
语法糖加持,代码可以简化很多。
args
可以通过扩展运算符…
直接赋给ctx[symbol]
调用的参数中。ES6
提供了Symbol
作为唯一值,保证不会与context
自带的属性名冲突。
二、apply
ES5 实现
Function.prototype.myApply = function( context ) {
var ctx = context || window;
var args = arguments[1] || [];
var symbol = Date.now().toString(32);
ctx[symbol] = this;
var result = eval('ctx[symbol]('+args+')');
delete ctx[symbol];
return result
}
apply
方法区别于call
方法,就是传参的形式不同,代码只稍作修改便可。- 不用遍历
arguments
对象,只用一行代码处理var args = arguments[1] || [];
, 其他地方和myCall
实现一样。
测试
var a = 10;
var b = 20;
var obj = {
a : 1,
b : 2,
getStr: getStr
}
function getStr( c, d ){
return this.a + this.b + c + d;
}
console.log(getStr(3,4)); // 37
console.log(getStr.myApply(obj,[3,4])); // 10
ES6 实现
Function.prototype.myApply = function( context, args ) {
let ctx = context || window;
let symbol = Symbol('symbol');
ctx[symbol] = this;
let result = ctx[symbol](...args);
delete ctx[symbol];
return result
}
ES6
实现差别不大。
三、bind
ES5 实现
Function.prototype.myBind = function(context){
// 缓存 this,this指向Function实例化对象,即函数function
var _this = this;
var args = [].slice.call(arguments,1);
return function(){
var newArgs = [].slice.call(arguments);
return _this.apply(context,args.concat(newArgs));
}
}
- 缓存
_this
是为了在return
的function
中使用,因为在这个return
的function
中产生了新的上下文环境,this
指向window
打印它会看到
Function.prototype.myBind = function(context){
...
return function(){
console.log(this); // Window {window: Window, self: Window, document: document, name: "", location: Location, …}
...
}
}
var args = [].slice.call(arguments,1);
语句将myBind
方法参数context
后面的参数转化成数组对象。return
的function
中var newArgs = [].slice.call(arguments);
语句将此函数调用栈中全部参数转化成数组对象,在apply
方法中将两个参数数组链接起来当作第二个参数传入。- 注意
_this.apply(context,args.concat(newArgs));
语句前面有return
,代码整体实现是闭包的展现形式,调用myBind
方法时也是先缓存(或叫做函数预加载),然后二次调用。
测试
var a = 10;
var b = 20;
var obj = {
a : 1,
b : 2,
getStr
}
function getStr( c, d ){
return this.a + this.b + c + d;
}
var fn1 = getStr.myBind(obj);
var fn2 = getStr.myBind(obj,3,4);
console.log(getStr(3,4)); // 37
console.log(fn1(3,4)); // 10
console.log(fn2(5,6)); // 10
console.log(fn2()); // 10
ES6 实现
Function.prototype.myBind = function(context, ...args1){
return (...args2) => {
return this.apply(context,[...args1,...args2])
}
}
- 一样利用扩展运算符 处理参数, 使用 解构赋值 特性组合参数数组。
- 不需要缓存
this
是因为箭头函数在定义时已经确立this
了指向,它等同于上一级作用域中this
指向。