JavaScript專題之模擬實現call和apply

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

概述

JS 函數 call 和 apply 用來手動改變 this 的指向,call 和 apply 惟一的區別就在於函數參數的傳遞方式不一樣,call 是以逗號的形式,apply 是以數組的形式:java

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。es6

首先觀察 call 和 apply 有什麼特色?數組

  1. 被函數調用(函數也是對象),至關於 call 和 apply 是函數的屬性
  2. 若是沒有傳入須要 this 指向對象,那麼 this 指向全局對象
  3. 函數執行了
  4. 最後都改變了 this 的指向

1、初步實現

基於 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 還涉及到傳參的問題,因此咱們進入到下一環節。app

2、eval 方式

在給對象臨時一個函數,並執行時,傳入的參數是除了 context 其他的參數。那麼咱們能夠截取 arguments 參數數組的第一個後,將剩餘的參數傳入臨時數組。函數

在前面我有講過函數 arguments 類數組對象的特色,arguments 是不支持數組的大多數方法, 可是支持for 循環來遍歷數組。post

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 的方式,將參數添加子函數的做用域中。ui

eval() 函數可計算某個字符串,並執行其中的的 JavaScript 代碼this

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。 大功告成!

3、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男
複製代碼

4、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 系列文章

  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
相關文章
相關標籤/搜索