自己动手用原生实现 bind/call/apply

大家好!!!注册一年多的第一篇博客。

自我介绍: 本人非计算机专业出身,转行进入前端半年时间,写的东西可能观赏性不强,一起进步吧道友们。。。

接下来的一段时间, 我都会不定期整理自己理解的js知识点, 欢迎各路道友吐槽。

 

进入正题......  (针对新手,老司机不要嘲笑我)

 

首先, bind/call/apply 这改变this指向的三兄弟我们都很熟悉了, 还有其他改变this指向的方法这里就不多说了哈,要不就跑题了。。。。

你需要知道的前提 : 这三货都是函数原型(Function.prototype)上的方法, 所以只有函数可以调用。

              小白 : “原型是啥?”   每一个构造函数(其实就是普通函数,函数名首字母大写而已)都有一个prototype原型对象,当你引用某个实例对象上的属性时, 会先查找该对象本身有没有该属性,有就用,没有就会去构造这个实例的构造函数的原型上去找,还没有的话就会沿着构造函数的原型对象的__proto__属性往上找......haha, 好像又跑题了, 关于原型链和继承可以自行百度

 

真的进入正题。。。。。。

 

它们不是函数上的方法吗, 我们先定义个函数, 后面来改变这个函数的this指向

function out(age, sex, type) {
    console.log(age, sex, this.age, this.sex, type);
}

分别输出传入的参数age, sex, type  和   this 上的age , sex, 

然后定义个对象, 后面让this指向该对象,  没错,此对象描述的就是我

let  my = {
    age : 16,
    sex : 'handsome man'
};

先试试bind, 先会用然后再来实现, 由于bind可以传两次参数,你可以这样传

const bindFunction = out.bind(my,50); //this 指向my 不会直接执行 返回一个明确this指向的函数
bindFunction('women','bind'); // 传参执行 

还可以这样传,想怎么传就怎么传,只要确保.bind后的第一个参数是你要改变this指向的对象, (如果什么都不传,this指向window)

const bindFunction = out.bind(my); //this 指向my 不会直接执行 返回一个明确this指向的函数
bindFunction('50','women','bind'); // 传参执行 

执行后输出 :

50 "women" 16 "handsome man" "bind"

总结一下我们要模拟的bind

1.  函数A调用bind返回一个可执行的函数B, 调用bind时可传参 可不穿参, 不穿参时this指向window

2.  调用B可继续传参, 两次传参合并

3.   new B() 的构造函数依然是A, 而不是B,并且因为new操作符改变this指向的优先级最高,所以如果使用了new操作符,this指向不应该变,就是该是啥是啥(划重点)

上代码 :

 

Function.prototype.myBind = function (target) { // bind 是 function 上的 方法  this 指向 target
    if (typeof  this !== 'function') {
        throw new TypeError('not a function')
    };
    const _this = this, // 保存一下this
        args = Array.prototype.slice.call(arguments, 1),// arguments是类数组,没有 slice 方法 保存第一次传入的参数
        temp = function () {}; // 定义一个中间函数
    const f = function () {
        return _this.apply(this instanceof  temp ? this : (target || window), args.concat([].slice.call(arguments))); // 合并两次传的参数
    };
    temp.prototype = _this.prototype; // 让temp 的原型 等于 this的原型
    f.prototype = new temp(); //f 继承 temp 圣杯继承可以了解下
    return f;
};

 

先正常执行一下myBind:

  const bind1 = this.out.myBind(my, 50);
    // const bind2 = new bind1('women','myBind');
  const bind3 = bind1('women','myBind');

结果是:

50 "women" 16 "handsome man" "myBind"

OK , 和bind的输出结果一样

再使用new操作符:

  const bind1 = this.out.myBind(my, 50);
    const bind2 = new bind1('women','myBind');
    // const bind3 = bind1('women','myBind');

结果是这样:此时this不再指向my, 而是指向new 出来的一个空对象,所以this上没有age和sex两个属性

50 "women" undefined undefined "myBind"

 

到此bind 我们已经完美模拟出来了  -------- 是不是有点小激动 ----------细心的道友会发现myBind 里 还借用了call 和apply!!! 这还不行啊

 

🆗接下来我们继续实现myCall 和 myApply

 

思考:

模拟bind是用call和apply来改变this指向的,

那么我们模拟call和apply要用什么来改变this指向呢?        给大家1s    思考......................................................

 

没错, 就是优先级最低的点(.)操作符, 还记得谁调用this就指向谁吗, 就是A.eat(),eat中的this就指向A,

 

基于此我们来实现myCall:

Function.prototype.myCall = function (target) {
target.fn = this; // 给target加一个属性fn = this
const args = [];
for (let i = 1; i < arguments.length; i ++ ) {
args.push(arguments[i]); // 遍历把参数push进数组
}
const result = target.fn(...args); // ...是把数组的中括号去掉, 这样传进去的就是参数列表了
delete target.fn; // 最后把fn属性删掉
return result;
};

这样就把this指向target了

同理myBind;

Function.prototype.myApply = function (target) {
target.fn = this;// 给target加一个属性fn = this
var result;
if(arguments[1]) {
result = target.fn(...arguments[1]);// ...是把数组的中括号去掉, 这样传进去的就是参数列表了
} else {
result = target.fn();
}
delete target.fn;// 最后把fn属性删掉
return result;
};

执行结果:

 

 

 这样理论上来说Call和Apply就已经模拟出来了, 但是我还留下了一个坑....................................................

 

有老司机可以看出来吗?       一秒钟时间抢答 -----------------------------------------------------------

 

没错, 我在myCall和myApply 里用了ES6的语法 : ...操作符扒括号(let 和 const 请忽略,用var也可以), 那我们可以完全用es5 来解决 如何把数组里的参数变成参数列表传进去吗?

 

 

道友们 有思路吗 ?  给大家个提示: 可以用ES3.0 的 eval()  配合 join() 方法实现 ,       eval是魔鬼大家请慎用

 

好记性不如烂笔头,大家回去自己动手试试吧 !

再演示一遍完整demo

 

posted @ 2019-03-25 19:29  初心,你好吗  阅读(955)  评论(11编辑  收藏  举报