1. call的实现
- 首先 context 为可选参数,如果不传的话默认上下文为 window
- 为了避免函数名与上下文(context)的属性发生冲突,使用Symbol类型作为唯一值
- 因为call可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
- 最后执行并删除ctx[func]
Function.prototype.myCall = function(context) {
// 判断调用者是否为函数 this指向调用myCall的函数
if(typeof this !== 'function') {
throw new Error('caller must be a function')
}
// 1. 第一个参数为null或者undefined时,this指向全局对象window;
// 2. this 参数可以传基本类型数据,原生的 call 会自动用 Object() 转换
const ctx = Object(context) || window;
//新建一个唯一的Symbol变量避免重复
const func = Symbol();
ctx[func] = this; // 将函数设置为对象的属性
const args = [...arguments].slice(1);
const res = ctx[func](...args);
delete ctx[func];
return res;
}
2. apply的实现
Function.prototype.myApply = function (context) {
// 判断调用者是否为函数 this指向调用myApply的函数
if(typeof this !== 'function') {
throw new Error('caller must be a function')
}
const ctx = Object(context) || window;
const func = Symbol();
ctx[func] = this;
let res;
// 判断是否有参数传入
if(arguments[1]) {
res =ctx[func](...arguments[1])
} else {
res = ctx[func]
}
delete ctx[func];
return res;
}
3. bind的实现
我们一步步实现以下bind的四点特性:
- 可以指定this
- 返回一个函数
- 可以传入参数
- 柯里化
模拟实现第一步
Function.prototype.myBind = function(context) {
const self = this; // this 指向调用者
return function() { // 实现第2点
return self.apply(context); // 实现第1点
}
}
模拟实现第二步
Function.prototype.myBind = function(context) {
const self = this; // this 指向调用者
// 实现第3点,因为第一个参数是指定的this,所以只汲取第1个之后的参数
const args = [...arguments].slice(1);
return function() { // 实现第2点
// 实现第4点,这时的arguments是指bind返回的函数传入的参数,即return function的参数
const bindArgs = [...arguments];
return self.apply(context, args.concat(bindArgs)); // 实现第1点
}
}
模拟实现第三步
到现在已经完成大部分了,但是还有一个难点,bind有以下特性:
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
太抽象了,举个例子来理解下:
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20
obj.habit;
// shopping
obj.friend;
// kevin
上面示例中,运行结果this.value输出为undefined, 这不是全局value也不是foo对象中的value,这说明bind的this对象失效了,new的实现中生成一个新的对象,这个时候的this指向的是obj。
这里可以通过修改返回函数的原型来实现,代码如下:
// 第三版
Function.prototype.bind2 = function (context) {
const self = this;
const args = [...arguments].slice(1);
const fBound = function() {
const bindArgs = [...arguments];
// 注释1
return self.apply(this instanceof fBound? this : context, args.concat(bindArgs
));
}
// 注释2
fBound.prototype = this.prototype;
return fBound;
}
注释1:
- 当作为构造函数时,this指向实例,此时this instance of fBound 结果为true,可以让实例获得来自绑定函数的值,即上例中实例会具有habit属性
- 当作为普通函数时,this指向window,此时结果为false,集那个绑定函数的this指向context
注释2:
修改返回函数的prototype为绑定函数的prototype,实例久可以继承绑定函数的原型中的值,即上面例子中obj可以获取到bar原型上的friend.
模拟实现第四步
上面实现中 fBound.prototype = this.prototype有一个缺点,直接修改 fBound.prototype 的时候,也会直接修改 this.prototype。
我们来测试一下:
// 测试用例
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正确
// undefined
// Jack
// 20
obj.habit; // 返回正确
// shopping
obj.friend; // 返回正确
// kevin
obj.__proto__.friend = "Kitty"; // 修改原型
bar.prototype.friend; // 返回错误,这里被修改了
// Kitty
解决方案是用一个空对象作为中介,把fBound.prototype赋值为空对象的实例(原型式继承)。
const fNOP = function() {}; // 创建一个空对象
fNOP.prototype = this.prototype; // 空对象的原型指向绑定函数的原型
fBound.prototype = new fNOP(); // 空对象的实例赋值给 fBound.prototype
这里可以直接使用ES5的Object.create()方法生成一个新对象
fBound.prototype = Object.create(this.prototype)
第四版目前ok了,代码如下:
Function.prototype.myBind = function(context) {
const self = this;
const fNOP = function () {};
const args = [...arguments].slice(1);
const fBound = function() {
const bindArgs = [...arguments];
return self.apply(this instanceof fNOP? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
模拟实现第五步(最终版)
到这里其实已经差不多了,但有一个问题是调用 bind 的不是函数,这时候需要抛出异常。
Function.prototype.myBind = function(context) {
if(typeof this !== 'function') {
throw new Error('caller must be a function!')
}
const self = this;
const fNOP = function () {};
const args = [...arguments].slice(1);
const fBound = function() {
const bindArgs = [...arguments];
return self.apply(this instanceof fNOP? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}