各位高富帅,白富美们,国庆快乐!
已经是假期第3天末尾了,各位都在家吃好喝好躺好了吗?作为一个programmer
,一连着吃喝躺玩了两天,这样的生活的确有些不习惯,这不,这放纵了两天,就又操作了起来,回顾起JS的基础知识了,给自己的脑袋活动一下,来一套前端健脑操?😀
手撕代码第一节 call
call
的定义根据MDN
如下:
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
看起来很官方并且比较不好理解,没关系,可以先看它是怎么使用的,在用中学,可能会理解得更加透彻;
function talk() {
console.log(this.name);
}
var personZhang = {
name: '张三'
}
talk(); // undefined;
talk.call(personZhang); // 张三
众所周知,talk
函数直接调用,this
就是指向window
,而call
可以将this
绑定到personZhang
-张三这个对象上;
其实,是不是可以换种理解方式? 这么理解
window = {
talk() {
console.log(this.name);
}
}
window.talk(); // undefined
var personZhang = {
name: '张三',
talk() {
console.log(this.name);
}
}
personZhang.talk(); // 张三
是不是call
相当于隐形地在personZhang
里面增加了一个talk
方法?
如果按照这种方式来理解,对于call
的手撕,是不是可以如下:
Function.prototype._call = function(context, ...args) {
if (typeof this !== 'function') throw Error('_call must be function!');
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context);
}
context.fn = this; // 这里 this 其实就是 call 方法, context 其实就是一个对象
// 这里其实就相当于把换入的context隐形的增加 context = {fn: _call};
var result = context.fn(...args);
delete context.fn; // 隐形增加的函数,这里删掉,保证是隐形的,不然破坏了原对象内部结构
return result;
}
其实总结一下call
函数,除开边界条件,就是只有一个操作:
把当前调用call
的函数当作是传入的第一个参数的方法
调用,如果说用表达式来体现
a.call(b, param1, param2) => b.a(param1, param2)
既然call
理解了,那么对于apply
就不在话下了,call
与apply
的区别只是参数的参数的类型而已;
这里有个小tips: 如何记住
call
与apply
的区别:apply
与array
的首字母都是a
,所以传数组,那相对的,call
也就记住了。
既然call
, apply
都提到了,那bind
肯定少不了,它们三者的作用都是一样的,不同点就在于bind
是返回一个函数。
手撕代码第二节 bind
根据MDN
的polilyfill,bind
可以分两种情况
- 直接调用
- new调用
先考虑直接调用的情况:
bind
返回一个函数,但是内部this
是绑定在传入的对象中,既然理解了call
,那就好办了
Function.prototype._bind = function(context, ...args) {
if (typeof this !== 'function') throw Error('_call must be function!');
var _this = this; // 这里 this 就是 调用 _bind 的函数,如 a._bind 那么指的就是a
var fnAfterBind = function() {
return _this.call(context, ...args);
}
return fnAfterBind;
}
这里就是返回一个函数,同时该函数的内部this
指向了绑定的context
对象;
以上这种是基础的,不考虑new
调用的方式,直接返回一个函数并在里面通过call
方法进行绑定就可以了,但若要考虑到new
的调用,就没有这么简单了。
考虑new
进行调用的情况:
在new
方式调用bind
之前,有两个问题要解决:
- 为啥要考虑
new
调用 new
一个对象究竟是做了什么
为什么要考虑new
调用?
有如下两种情况:
function personZhang() {
console.log(this.name)
}
personZhang.prototype.name = '张三';
var personLi = {
name: '李四'
}
// 第一种情况
var person = personZhang.bind(personLi);
person(); // 李四
// 第二种情况
var person1 = personZhang.bind(personLi);
new person1(); // 张三 personZhang {}
以上两种情况可以发现,即使用bind
绑定之后的函数,再用new
调用,bind
的绑定效果不起作用,这个是new
绑定的优先级 > 显式绑定 的优先级导致的。因此要考虑 new
调用
为此,new
一个对象,究竟是做了什么?
用代码来剖析一下new
究竟做了什么操作
Function.prototype._new = function(Constructor, ...args) {
var obj = {};
obj.__proto__ = Constructor.prototype;
var result = Constructor.call(obj, ...args);
// other code...
}
根据上面代码理解,其实就是创建一个对象,让该对象的原型指向构造函数的原型,并调用该构造函数,使this
绑定到该对象上!
由于要考虑new
的调用,所以上述的_bind
方法就要发生些许改造了
// new (funA.bind(objA))
Function.prototype.__bind = function(context, ...args) {
// 首先,bind肯定是被一个函数[function]调用;
if (typeof this !== 'function') throw Error('_bind must be function!');
// 当前函数
var self = this;
var fNOP = function() {}; // 这两行代码很重要
if (this.prototype) {
fNOP.prototype = this.prototype; // ①这里至关重要,通过闭包的方式,使fNOP的原型指向 funA 的prototype,就是为了判断其是否有 new 的调
// 即记录了 funA 的 Constructor.prototype, 上面 new 调用的原理
}
// bind返回一个函数[function]
var fnAfterBind = function() {
// ② 由于考虑到 new 的调用,因此,要取 ① 中的fNOP是否为当前 function 的原型,即 funA的Constructor.prototype这个东西!
return self.call(fNOP.prototype.isPrototypeOf(this) ? this : context, ...args);
}
fnAfterBind.prototype = new fNOP();
return fnAfterBind;
}
在这里,关键点就是在于
- 保存原构造函数的原型链
- 判断是否为
new
调用
如何保存原调用_bind
函数的调用者的原型链
var fNOP = function() {};
if (this.prototype) {
fNOP.prototype = this.prototype;
}
判断是否为new
调用
var fnAfterBind = function() {
return self.call(fNOP.prototype.isPrototypeOf(this) ? this : context, ...args);
}
fnAfterBind.prototype = new fNOP();
fNOP.prototype.isPrototypeOf(this)
就是判断当前是否为new
调用的,由于fnAfterBind
以new
操作符实例化时,就是相当于fnAfterBind
作为构造函数,而fnAfterBind
的原型继承自 fNOP
既最终还是继承自_bind
函数的调用者,因此new (funA.bind(objA)); => new funA();
而正常bind
情况则是self.call(context, ...args)
;
以上bind
手撕完毕;
总结
其实,手撕代码的目的并不是手撕代码,手撕代码只是一个知识理解运用的过程,在手撕过程中融会贯通,知其原理,其最终的目的都是更好的理解该知识点。
简而言之
call
与apply
其实就是 隐形地把该函数放入绑定对象中,以对象的方式调用,即a.call(b, param1) => b.a(param1)
;bind
稍微复杂一些,要分情况讨论,new
情况要注意判断new
的调用以及该调用者的原型如何保存下来,否则就是一个简单的闭包函数,返回一个保存原this
绑定的call
调用而已。
补充:
写作不易,如果觉得对您有帮助,请不吝贵手点个赞,感谢!如有问题,欢迎指出错误。