javascript的call apply和new原理剖析 [手寫]

weixin_34253539發表於2019-02-25

今天公司沒那麼忙 閒來無事 就手動實現下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上

相關文章