【進階3-3期】深度解析 call 和 apply 原理舔亭、使用場景及實現(xiàn)

之前文章詳細介紹了 this 的使用蹋艺,不了解的查看【進階3-1期】。

call() 和 apply()

call() 方法調(diào)用一個函數(shù), 其具有一個指定的 this 值和分別地提供的參數(shù)(參數(shù)的列表)剥悟。

call()apply()的區(qū)別在于,call()方法接受的是若干個參數(shù)的列表曼库,而apply()方法接受的是一個包含多個參數(shù)的數(shù)組

舉個例子:

var func = function(arg1, arg2) {
     ...
};

func.call(this, arg1, arg2); // 使用 call区岗,參數(shù)列表
func.apply(this, [arg1, arg2]) // 使用 apply,參數(shù)數(shù)組

使用場景

下面列舉一些常用用法:

1毁枯、合并兩個數(shù)組
var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];

// 將第二個數(shù)組融合進第一個數(shù)組
// 相當于 vegetables.push('celery', 'beetroot');
Array.prototype.push.apply(vegetables, moreVegs);
// 4

vegetables;
// ['parsnip', 'potato', 'celery', 'beetroot']

當?shù)诙€數(shù)組(如示例中的 moreVegs )太大時不要使用這個方法來合并數(shù)組慈缔,因為一個函數(shù)能夠接受的參數(shù)個數(shù)是有限制的。不同的引擎有不同的限制种玛,JS核心限制在 65535藐鹤,有些引擎會拋出異常,有些不拋出異常但丟失多余參數(shù)赂韵。

如何解決呢娱节?方法就是將參數(shù)數(shù)組切塊后循環(huán)傳入目標方法

function concatOfArray(arr1, arr2) {
    var QUANTUM = 32768;
    for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
        Array.prototype.push.apply(
            arr1, 
            arr2.slice(i, Math.min(i + QUANTUM, len) )
        );
    }
    return arr1;
}

// 驗證代碼
var arr1 = [-3, -2, -1];
var arr2 = [];
for(var i = 0; i < 1000000; i++) {
    arr2.push(i);
}

Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded

concatOfArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
2、獲取數(shù)組中的最大值和最小值
var numbers = [5, 458 , 120 , -215 ]; 
Math.max.apply(Math, numbers);   //458    
Math.max.call(Math, 5, 458 , 120 , -215); //458

// ES6
Math.max.call(Math, ...numbers); // 458

為什么要這么用呢祭示,因為數(shù)組 numbers 本身沒有 max方法肄满,但是 Math有呀,所以這里就是借助 call / apply 使用 Math.max 方法绍移。

3悄窃、驗證是否是數(shù)組
function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// 直接使用 toString()
[1, 2, 3].toString();   // "1,2,3"
"123".toString();       // "123"
123.toString();         // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"

可以通過toString() 來獲取每個對象的類型,但是不同對象的 toString()有不同的實現(xiàn)蹂窖,所以通過 Object.prototype.toString() 來檢測轧抗,需要以 call() / apply() 的形式來調(diào)用,傳遞要檢查的對象作為第一個參數(shù)瞬测。

另一個驗證是否是數(shù)組的方法

var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){ 
    return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// 使用改造后的 toStr
toStr([1, 2, 3]);   // "[object Array]"
toStr("123");       // "[object String]"
toStr(123);         // "[object Number]"
toStr(Object(123)); // "[object Number]"

上面方法首先使用 Function.prototype.call函數(shù)指定一個 this 值横媚,然后 .bind 返回一個新的函數(shù),始終將 Object.prototype.toString 設(shè)置為傳入?yún)?shù)月趟。其實等價于 Object.prototype.toString.call() 灯蝴。

這里有一個前提toString()方法沒有被覆蓋

Object.prototype.toString = function() {
    return '';
}
isArray([1, 2, 3]);
// false
4、類數(shù)組對象(Array-like Object)使用數(shù)組方法
var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function

var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1"); // 505 不同環(huán)境下數(shù)據(jù)不同
// (505) ["h1", html.gr__hujiang_com, head, meta, ...] 

類數(shù)組對象有下面兩個特性

  • 1孝宗、具有:指向?qū)ο笤氐臄?shù)字索引下標和 length 屬性
  • 2穷躁、不具有:比如 pushshift因妇、 forEach 以及 indexOf等數(shù)組對象具有的方法

要說明的是问潭,類數(shù)組對象是一個對象猿诸。JS中存在一種名為類數(shù)組的對象結(jié)構(gòu),比如 arguments 對象狡忙,還有DOM API 返回的 NodeList 對象都屬于類數(shù)組對象梳虽,類數(shù)組對象不能使用 push/pop/shift/unshift 等數(shù)組方法,通過 Array.prototype.slice.call 轉(zhuǎn)換成真正的數(shù)組灾茁,就可以使用 Array下所有方法窜觉。

類數(shù)組對象轉(zhuǎn)數(shù)組的其他方法:

// 上面代碼等同于
var arr = [].slice.call(arguments);

ES6:
let arr = Array.from(arguments);
let arr = [...arguments];

Array.from() 可以將兩類對象轉(zhuǎn)為真正的數(shù)組:類數(shù)組對象和可遍歷(iterable)對象(包括ES6新增的數(shù)據(jù)結(jié)構(gòu) Set 和 Map)北专。

PS擴展一:為什么通過 Array.prototype.slice.call() 就可以把類數(shù)組對象轉(zhuǎn)換成數(shù)組禀挫?

其實很簡單,sliceArray-like 對象通過下標操作放進了新的 Array 里面逗余。

下面代碼是 MDN 關(guān)于 slice 的Polyfill特咆,鏈接 Array.prototype.slice()

Array.prototype.slice = function(begin, end) {
      end = (typeof end !== 'undefined') ? end : this.length;

      // For array like object we handle it ourselves.
      var i, cloned = [],
        size, len = this.length;

      // Handle negative value for "begin"
      var start = begin || 0;
      start = (start >= 0) ? start : Math.max(0, len + start);

      // Handle negative value for "end"
      var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
      if (end < 0) {
        upTo = len + end;
      }

      // Actual expected size of the slice
      size = upTo - start;

      if (size > 0) {
        cloned = new Array(size);
        if (this.charAt) {
          for (i = 0; i < size; i++) {
            cloned[i] = this.charAt(start + i);
          }
        } else {
          for (i = 0; i < size; i++) {
            cloned[i] = this[start + i];
          }
        }
      }

      return cloned;
    };
  }

PS擴展二:通過 Array.prototype.slice.call() 就足夠了嗎?存在什么問題录粱?

低版本IE下不支持通過Array.prototype.slice.call(args)將類數(shù)組對象轉(zhuǎn)換成數(shù)組,因為低版本IE(IE < 9)下的DOM對象是以 com 對象的形式實現(xiàn)的画拾,js對象與 com 對象不能進行轉(zhuǎn)換啥繁。

兼容寫法如下:

function toArray(nodes){
    try {
        // works in every browser except IE
        return Array.prototype.slice.call(nodes);
    } catch(err) {
        // Fails in IE < 9
        var arr = [],
            length = nodes.length;
        for(var i = 0; i < length; i++){
            // arr.push(nodes[i]); // 兩種都可以
            arr[i] = nodes[i];
        }
        return arr;
    }
}

PS 擴展三:為什么要有類數(shù)組對象呢?或者說類數(shù)組對象是為什么解決什么問題才出現(xiàn)的青抛?

JavaScript類型化數(shù)組是一種類似數(shù)組的對象旗闽,并提供了一種用于訪問原始二進制數(shù)據(jù)的機制。 Array存儲的對象能動態(tài)增多和減少蜜另,并且可以存儲任何JavaScript值适室。JavaScript引擎會做一些內(nèi)部優(yōu)化,以便對數(shù)組的操作可以很快举瑰。然而捣辆,隨著Web應(yīng)用程序變得越來越強大,尤其一些新增加的功能例如:音頻視頻編輯此迅,訪問WebSockets的原始數(shù)據(jù)等汽畴,很明顯有些時候如果使用JavaScript代碼可以快速方便地通過類型化數(shù)組來操作原始的二進制數(shù)據(jù),這將會非常有幫助耸序。

一句話就是忍些,可以更快的操作復(fù)雜數(shù)據(jù)。

5坎怪、調(diào)用父構(gòu)造函數(shù)實現(xiàn)繼承
function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代碼罢坝,繼承自SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]

var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]

在子構(gòu)造函數(shù)中,通過調(diào)用父構(gòu)造函數(shù)的call方法來實現(xiàn)繼承搅窿,于是SubType的每個實例都會將SuperType 中的屬性復(fù)制一份嘁酿。

缺點:

  • 只能繼承父類的實例屬性和方法隙券,不能繼承原型屬性/方法
  • 無法實現(xiàn)復(fù)用,每個子類都有父類實例函數(shù)的副本痹仙,影響性能

更多繼承方案查看我之前的文章是尔。JavaScript常用八種繼承方案

call的模擬實現(xiàn)

以下內(nèi)容參考自 JavaScript深入之call和apply的模擬實現(xiàn)

先看下面一個簡單的例子

var value = 1;
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

通過上面的介紹我們知道,call()主要有以下兩點

  • 1开仰、call()改變了this的指向
  • 2拟枚、函數(shù) bar 執(zhí)行了
模擬實現(xiàn)第一步

如果在調(diào)用call()的時候把函數(shù) bar()添加到foo()對象中,即如下

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value);
    }
};

foo.bar(); // 1

這個改動就可以實現(xiàn):改變了this的指向并且執(zhí)行了函數(shù)bar众弓。

但是這樣寫是有副作用的恩溅,即給foo額外添加了一個屬性,怎么解決呢谓娃?

解決方法很簡單脚乡,用 delete 刪掉就好了。

所以只要實現(xiàn)下面3步就可以模擬實現(xiàn)了滨达。

  • 1奶稠、將函數(shù)設(shè)置為對象的屬性:foo.fn = bar
  • 2、執(zhí)行函數(shù):foo.fn()
  • 3捡遍、刪除函數(shù):delete foo.fn

代碼實現(xiàn)如下:

// 第一版
Function.prototype.call2 = function(context) {
    // 首先要獲取調(diào)用call的函數(shù)锌订,用this可以獲取
    context.fn = this;      // foo.fn = bar
    context.fn();           // foo.fn()
    delete context.fn;      // delete foo.fn
}

// 測試一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call2(foo); // 1

完美!

模擬實現(xiàn)第二步

第一版有一個問題画株,那就是函數(shù) bar 不能接收參數(shù)辆飘,所以我們可以從 arguments中獲取參數(shù),取出第二個到最后一個參數(shù)放到數(shù)組中谓传,為什么要拋棄第一個參數(shù)呢蜈项,因為第一個參數(shù)是 this

類數(shù)組對象轉(zhuǎn)成數(shù)組的方法上面已經(jīng)介紹過了续挟,但是這邊使用ES3的方案來做紧卒。

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}

參數(shù)數(shù)組搞定了,接下來要做的就是執(zhí)行函數(shù) context.fn()庸推。

context.fn( args.join(',') ); // 這樣不行

上面直接調(diào)用肯定不行常侦,args.join(',')會返回一個字符串,并不會執(zhí)行贬媒。

這邊采用 eval方法來實現(xiàn)聋亡,拼成一個函數(shù)。

eval('context.fn(' + args +')')

上面代碼中args 會自動調(diào)用 args.toString() 方法际乘,因為'context.fn(' + args +')'本質(zhì)上是字符串拼接坡倔,會自動調(diào)用toString()方法,如下代碼:

var args = ["a1", "b2", "c3"];
console.log(args);
// ["a1", "b2", "c3"]

console.log(args.toString());
// a1,b2,c3

console.log("" + args);
// a1,b2,c3

所以說第二個版本就實現(xiàn)了,代碼如下:

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 測試一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call2(foo, 'kevin', 18); 
// kevin
// 18
// 1

完美W锼投蝉!

模擬實現(xiàn)第三步

還有2個細節(jié)需要注意:

  • 1、this 參數(shù)可以傳 null 或者 undefined征堪,此時 this 指向 window
  • 2瘩缆、函數(shù)是可以有返回值的

實現(xiàn)上面的兩點很簡單,代碼如下

// 第三版
Function.prototype.call2 = function (context) {
    context = context || window; // 實現(xiàn)細節(jié) 1
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result; // 實現(xiàn)細節(jié) 2
}

// 測試一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call2(null); // 2

console.log(bar.call2(obj, 'kevin', 18));
// 1
// {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

完美5柩痢S褂椤!

call和apply模擬實現(xiàn)匯總

call的模擬實現(xiàn)

ES3:

Function.prototype.call = function (context) {
    context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

ES6:

Function.prototype.call = function (context) {
  context = context || window;
  context.fn = this;

  let args = [...arguments].slice(1);
  let result = context.fn(...args);

  delete context.fn
  return result;
}
apply的模擬實現(xiàn)

ES3:

Function.prototype.apply = function (context, arr) {
    context = context || window;
    context.fn = this;

    var result;
    // 判斷是否存在第二個參數(shù)
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')');
    }

    delete context.fn
    return result;
}

ES6:

Function.prototype.apply = function (context, arr) {
    context = context || window;
    context.fn = this;
  
    let result;
    if (!arr) {
        result = context.fn();
    } else {
        result = context.fn(...arr);
    }
      
    delete context.fn
    return result;
}

思考題

callapply 的模擬實現(xiàn)有沒有問題谐算?歡迎思考評論熟尉。


PS: 上期思考題留到下一期講解,下一期介紹重點介紹 bind 原理及實現(xiàn)

參考

JavaScript深入之call和apply的模擬實現(xiàn)

MDN之Array.prototype.push()

MDN之Function.prototype.apply()

MDN之Array.prototype.slice()

MDN之Array.isArray()

JavaScript常用八種繼承方案

深入淺出 妙用Javascript中apply洲脂、call斤儿、bind

進階系列目錄

  • 【進階1期】 調(diào)用堆棧
  • 【進階2期】 作用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函數(shù)
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節(jié)流原理
  • 【進階12期】模塊化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網(wǎng)絡(luò)概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監(jiān)控
  • 【進階19期】跨域和安全
  • 【進階20期】性能優(yōu)化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff算法
  • 【進階23期】MVVM雙向綁定
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter源碼解析
  • 【進階28期】ReactRouter源碼解析

交流

進階系列文章匯總?cè)缦拢瑑?nèi)有優(yōu)質(zhì)前端資料恐锦,覺得不錯點個star往果。

https://github.com/yygmind/blog

我是木易楊,網(wǎng)易高級前端工程師一铅,跟著我每周重點攻克一個前端面試重難點棚放。接下來讓我?guī)阕哌M高級前端的世界,在進階的路上馅闽,共勉!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末馍迄,一起剝皮案震驚了整個濱河市福也,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌攀圈,老刑警劉巖暴凑,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赘来,居然都是意外死亡现喳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門犬辰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗦篱,“玉大人,你說我怎么就攤上這事幌缝【拇伲” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長浴栽。 經(jīng)常有香客問我荒叼,道長,這世上最難降的妖魔是什么典鸡? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任被廓,我火速辦了婚禮,結(jié)果婚禮上萝玷,老公的妹妹穿的比我還像新娘嫁乘。我一直安慰自己,他們只是感情好间护,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布亦渗。 她就那樣靜靜地躺著,像睡著了一般汁尺。 火紅的嫁衣襯著肌膚如雪法精。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天痴突,我揣著相機與錄音搂蜓,去河邊找鬼。 笑死辽装,一個胖子當著我的面吹牛帮碰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拾积,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼殉挽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拓巧?” 一聲冷哼從身側(cè)響起斯碌,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肛度,沒想到半個月后傻唾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡承耿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年冠骄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片加袋。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡凛辣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锁荔,到底是詐尸還是另有隱情蟀给,我是刑警寧澤蝙砌,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站跋理,受9級特大地震影響择克,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜前普,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一肚邢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拭卿,春花似錦骡湖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惠桃,卻和暖如春浦夷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辜王。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工劈狐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人呐馆。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓肥缔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親汹来。 傳聞我的和親對象是個殘疾皇子续膳,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,237評論 0 4
  • 參考《JavaScript設(shè)計模式與開發(fā)實踐》 this 跟別的語言大相徑庭的是闺阱,JavaScript的this總...
    16manman閱讀 573評論 0 0
  • 概要 64學(xué)時 3.5學(xué)分 章節(jié)安排 電子商務(wù)網(wǎng)站概況 HTML5+CSS3 JavaScript Node 電子...
    阿啊阿吖丁閱讀 9,208評論 0 3
  • ??引用類型的值(對象)是引用類型的一個實例酣溃。 ??在 ECMAscript 中,引用類型是一種數(shù)據(jù)結(jié)構(gòu)纪隙,用于將數(shù)...
    霜天曉閱讀 1,058評論 0 1
  • 宋運輝真心讓我佩服赊豌。 由于家庭成份問題,他比一般小孩更成熟绵咱,在其他孩子嬉戲的時候碘饼,他心里想的是他的未來,他們家的未...
    邂逅哆啦A夢閱讀 252評論 0 0