国庆七天乐,手撕代码更快乐?

354 阅读5分钟

各位高富帅,白富美们,国庆快乐!

已经是假期第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就不在话下了,callapply的区别只是参数的参数的类型而已;

这里有个小tips: 如何记住callapply的区别:applyarray的首字母都是a,所以传数组,那相对的,call也就记住了。

既然call, apply都提到了,那bind肯定少不了,它们三者的作用都是一样的,不同点就在于bind是返回一个函数。

手撕代码第二节 bind

根据MDN的polilyfill,bind可以分两种情况

  1. 直接调用
  2. 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之前,有两个问题要解决:

  1. 为啥要考虑new调用
  2. 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;
}

在这里,关键点就是在于

  1. 保存原构造函数的原型链
  2. 判断是否为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调用的,由于fnAfterBindnew操作符实例化时,就是相当于fnAfterBind作为构造函数,而fnAfterBind的原型继承自 fNOP 既最终还是继承自_bind函数的调用者,因此new (funA.bind(objA)); => new funA();

而正常bind情况则是self.call(context, ...args);

以上bind手撕完毕;

总结

其实,手撕代码的目的并不是手撕代码,手撕代码只是一个知识理解运用的过程,在手撕过程中融会贯通,知其原理,其最终的目的都是更好的理解该知识点。

简而言之

  1. callapply其实就是 隐形地把该函数放入绑定对象中,以对象的方式调用,即a.call(b, param1) => b.a(param1);
  2. bind稍微复杂一些,要分情况讨论,new情况要注意判断new的调用以及该调用者的原型如何保存下来,否则就是一个简单的闭包函数,返回一个保存原this绑定的call调用而已。

补充:

写作不易,如果觉得对您有帮助,请不吝贵手点个赞,感谢!如有问题,欢迎指出错误。