2

今天公司没那么忙 闲来无事 就手动实现下js的call,apply和new的原理吧~
本篇不长 废话不多 分为3步:

  • 手写call方法
  • 手写apply方法
  • 手写new方法

我们知道 call可以改变this指向,同时也可以传递参数。即 call的第一个参数为改变后的this,剩余参数则是正常的函数参数。并且,调用call和apply后相当于改变this并立马执行函数。ok,上代码~


let str = 'str';

let obj = {
    name:'123'
};

function fn(){
    console.log(this);
}

fn.call(str); // 打印 String {"str"}
fn.call(obj); // 打印 {name: "123"}
手写call方法

注意观看顺序1,2,3,4,5...

/* Function原型上拓展call方法*/

Function.prototype.myCall = function(context){
    /** 1 如果没传上下文conntext 就取window为this 
     * 此处Object() 主要考虑到如果是String类型
    */
    context = context?Object(context):window;
    /** 2 改变this指向
     * this就是原函数 */
    context.fn = this;
    /** 3 取参数 注意从第二个开始取
     * 因为第一个参数是上下文context 也就是this
    */
    let args = [];
    for(let i = 1; i < arguments.length; i++){
        /** 4 这里传递的上字符串 因为待会要配合eval()使用 */
        args.push('arguments['+ i +']');
    }
    /** 5 把参数传递进去 eval()方法可以让字符串执行 */
    let r = eval('context.fn('+ args +')');
    /** 6 删除原context.fn */
    delete context.fn;
    /** 7 返回r */
    return r;
}


let str = 'str';

let obj = {
    name:'123'
};

function fn(){
    console.log(this);
}

fn.myCall(str); // 打印 String {"str"}
fn.myCall(obj); // 打印 {name: "123"}

?. 接下来是apply方法 ,其实apply方法最简单 因为apply和call方法的唯一区别就是apply第一个参数后面的参数是数组形式 仅此而已。 所以 我们在call方法上进行稍微改造就好:

手写apply方法

注意观看顺序1,2,3,4,5...

/* Function原型上拓展apply方法*/

Function.prototype.myApply = function(context,args){
    /** 1 如果没传上下文conntext 就取window为this 
     * 此处Object() 主要考虑到如果是String类型
    */
    context = context?Object(context):window;
    /** 2 改变this指向
     * this就是原函数 */
    context.fn = this;
    /** 3 把参数传递进去 eval()可以让字符串执行 */
    let r = eval('context.fn('+ args +')');
    /** 6 删除原context.fn */
    delete context.fn;
    /** 7 返回r */
    return r;
}

let str = 'str';

let obj = {
    name:'123'
};

function fn(){
    console.log(this);
}

fn.apply(str);  // 打印 String {"str"}
fn.apply(obj); // 打印 {name: "123"}

fn.myApply(str);  // 打印 String {"str"}
fn.myApply(obj); // 打印 {name: "123"}

可以看到 apply的实现方法比call简单许多 这是因为call需要处理其他参数的情况,而apply的参数本身就是一个数组 直接传递进去 执行就好~

ok.call和apply我们都手动实现了,接下来就是new了~

首先是原new


function Animal(type){
    this.type = type;
}

Animal.prototype.eat = function(){
    console.log('eat-meat');
}

let tiger = new Animal('tiger');

console.log(tiger.type); // 打印 tiger
console.log(tiger.eat()); // 打印 eat-meat
手写new方法

注意观看顺序1,2,3,4,5...


function myNew(){
    /** 1 我们都new第一个参数为构造函数
     * shift:把数组的第一个元素从其中删除,并返回第一个元素的值
     * 此时Constructor就是构造函数
     */
    let Constructor = Array.prototype.shift.call(arguments);
    /** 2 将要返回的实例 */
    let obj = {};
    /** 3 实例obj和构造函数Constructor指向同一个原型对象 */
    obj.__proto__ = Constructor.prototype;
    /** 4 拿到实例执行的结果 观察结果是不是一个引用类型
     * 改变Constructor的this指向为将要返回的实例obj 并传递参数 
     * 这里一定要用apply 不要用call 因为apply传递的是一个参数数组
    */
    let r = Constructor.apply(obj,arguments);
    return r instanceof Constructor? r : obj;
}

function Animal(type){
    this.type = type;
}

Animal.prototype.eat = function(){
    console.log('eat-meat');
}

let tiger = new Animal('tiger');
let tiger1 = new myNew(Animal,'tiger');

console.log(tiger.type); // 打印 tiger
console.log(tiger.eat()); // 打印 eat-meat

console.log(tiger1.type); // 打印 tiger
console.log(tiger1.eat()); // 打印 eat-meat

好.至此 call apply和new的原理我们都手写出来了. 其实还有一个bind方法,不过bind方法略麻烦,对于bind 我会单独写一篇文章.

代码在git上


Funky_Tiger
443 声望33 粉丝

刷题,交流,offer,内推,涨薪,相亲,前端资源共享...