JavaScript專題之模擬實現call和apply

南波發表於2018-10-29

本文共 1320 字,讀完只需 5 分鐘

概述

JS 函式 call 和 apply 用來手動改變 this 的指向,call 和 apply 唯一的區別就在於函式引數的傳遞方式不同,call 是以逗號的形式,apply 是以陣列的形式:

let person1 = {
    name: "person1",
    say: function(age, sex) {
        console.log(this.name + ' age: ' + age + ' sex: ' + sex);
    }
}

let person2 = {
    name: "person"
}

person1.say.call(person2, 20, "男");

person1.say.apply(person2, [20, "男"]);
複製程式碼

本文就嘗試用其他方式來模擬實現 call 和 apply。

首先觀察 call 和 apply 有什麼特點?

  1. 被函式呼叫(函式也是物件),相當於 call 和 apply 是函式的屬性
  2. 如果沒有傳入需要 this 指向物件,那麼 this 指向全域性物件
  3. 函式執行了
  4. 最後都改變了 this 的指向

一、初步實現

基於 call 函式是呼叫函式的屬性的特點,call 的 this 指向呼叫函式,我們可以嘗試把呼叫函式的作為傳入的新物件的一個屬性,執行後,再刪除這個屬性就好了。

Function.prototype.newCall = function (context) {
    context.fn = this;  // this 指的是 say 函式
    context.fn();
    delete context.fn;
}

var person = {
    name: "jayChou"
};

var say = function() {
    console.log(this.name);
}

say.newCall(person);  // jayChou
複製程式碼

是不是就初步模擬實現了 call 函式呢,由於 call 還涉及到傳參的問題,所以我們進入到下一環節。

二、eval 方式

在給物件臨時一個函式,並執行時,傳入的引數是除了 context 其餘的引數。那麼我們可以擷取 arguments 引數陣列的第一個後,將剩餘的引數傳入臨時陣列。

在前面我有講過函式 arguments 類陣列物件的特點,arguments 是不支援陣列的大多數方法, 但是支援for 迴圈來遍歷陣列。

Function.prototype.newCall = function (context) {
    context.fn = this;
    
    let args = [];
    
    for(let i=1; i< arguments.length; i++) {
        args.push('arguments[' + i + ']');
    }
    // args => [arguments[1], arguments[2], arguments[3], ...]
    
    context.fn(args.join(','));  // ???
    delete context.fn;
}

var person = {
    name: "jayChou"
};

var say = function(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
}

say.newCall(person);
複製程式碼

上面傳遞引數的方式最後肯定是失敗的,我們可以嘗試 eval 的方式,將引數新增子函式的作用域中。

eval() 函式可計算某個字串,並執行其中的的 JavaScript 程式碼

Function.prototype.newCall = function (context) {
    context.fn = this;
    
    let args = [];
    
    for(var i=1; i< arguments.length; i++) {
        args.push('arguments[' + i + ']');
    }

    // args => [arguments[1], arguments[2], arguments[3], ...]
    
    eval('context.fn(' + args + ')');
    delete context.fn;
}

var person = {
    name: "jayChou"
};

function say(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
}

say.newCall(person, 18, '男');  // name: jayChou,age: 18, sex: 男
複製程式碼

成功啦!
實現了函式引數的傳遞,那麼函式返回值怎麼處理呢。而且,如果傳入的物件是 null,又該如何處理?所以還需要再做一些工作:

Function.prototype.newCall = function (context) {
    if (typeof context === 'object') {
        context = context || window
    } else {
        context = Object.create(null);
    }
    
    context.fn = this;
    
    let args = [];
    
    for(var i=1; i< arguments.length; i++) {
        args.push('arguments[' + i + ']');
    }

    // args => [arguments[1], arguments[2], arguments[3], ...]
    
    var result = eval('context.fn(' + args + ')');  // 處理返回值
    delete context.fn;
    return result;  // 返回返回值
}

var person = {
    name: "jayChou"
};

function say(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
    return age + sex;
}

var check = say.newCall(person, 18, '男');
console.log(check); // 18男
複製程式碼

判斷傳入物件的型別,如果為 null 就指向 window 物件。利用 eval 來執行字串程式碼,並返回字串程式碼執行的結果,就完成了模擬 call。 大功告成!

三、ES 6 實現

前面我們用的 eval 方式可以用 ES6 的解決還存在的一些問題,有沒有注意到,這段程式碼是有問題的。

context.fn = this;
複製程式碼

假如物件在被 call 呼叫前,已經有 fn 屬性怎麼辦?

ES6 中提供了一種新的基本資料型別,Symbol,表示獨一無二的值,另外,Symbol 作為屬性的時候,不能使用點運算子。所以再加上 ES 的 rest 剩餘引數替代 arguments 遍歷的工作就有:

Function.prototype.newCall = function (context,...params) {
    if (typeof context === 'object') {
        context = context || window
    } else {
        context = Object.create(null);
    }
    let fn = Symbol();
    context[fn] = this
    var result = context[fn](...params);
    
    delete context.fn;
    return result;
}

var person = {
    name: "jayChou"
};

function say(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
    return age + sex;
}

var check = say.newCall(person, 18, '男');
console.log(check); // 18男
複製程式碼

四、apply

apply 和 call 的實現原理,基本類似,區別在於 apply 的引數是以陣列的形式傳入。

Function.prototype.newApply = function (context, arr) {
    if (typeof context === 'object') {
        context = context || window
    } else {
        context = Object.create(null);
    }
    context.fn = this;

    var result;
    if (!arr) {  // 判斷函式引數是否為空
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0; i < arr.length; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')');
    }

    delete context.fn;
    return result;
}
複製程式碼

es6 實現

Function.prototype.newApply = function(context, parameter) {
  if (typeof context === 'object') {
    context = context || window
  } else {
    context = Object.create(null)
  }
  let fn = Symbol()
  context[fn] = this;
  var result = context[fn](...parameter);
  delete context[fn];
  return result;
}
複製程式碼

總結

本文通過原生 JS 的 ES5 的方法和 ES 6 的方法模擬實現了 call 和 apply 的原理,旨在深入瞭解這兩個方法的用法和區別,希望你能有所收穫。

歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。

JavaScript專題之模擬實現call和apply

掘金專欄 JavaScript 系列文章

  1. JavaScript之變數及作用域
  2. JavaScript之宣告提升
  3. JavaScript之執行上下文
  4. JavaScript之變數物件
  5. JavaScript原型與原型鏈
  6. JavaScript之作用域鏈
  7. JavaScript之閉包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值傳遞
  11. JavaScript之例題中徹底理解this
  12. JavaScript專題之模擬實現call和apply

相關文章