成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

godlong_X / 578人閱讀

摘要:之前文章詳細介紹了的使用,不了解的查看進階期。不同的引擎有不同的限制,核心限制在,有些引擎會拋出異常,有些不拋出異常但丟失多余參數(shù)。存儲的對象能動態(tài)增多和減少,并且可以存儲任何值。這邊采用方法來實現(xiàn),拼成一個函數(shù)。

之前文章詳細介紹了 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應用程序變得越來越強大,尤其一些新增加的功能例如:音頻視頻編輯,訪問WebSockets的原始數(shù)據(jù)等,很明顯有些時候如果使用JavaScript代碼可以快速方便地通過類型化數(shù)組來操作原始的二進制數(shù)據(jù),這將會非常有幫助。

一句話就是,可以更快的操作復雜數(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 中的屬性復制一份。

缺點:

只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法

無法實現(xiàn)復用,每個子類都有父類實例函數(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

完美??!

模擬實現(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
// }

完美?。?!

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)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/100737.html

相關(guān)文章

  • 進階3-4深度解析bind原理使用場景模擬實現(xiàn)

    摘要:返回的綁定函數(shù)也能使用操作符創(chuàng)建對象這種行為就像把原函數(shù)當成構(gòu)造器,提供的值被忽略,同時調(diào)用時的參數(shù)被提供給模擬函數(shù)。 bind() bind() 方法會創(chuàng)建一個新函數(shù),當這個新函數(shù)被調(diào)用時,它的 this 值是傳遞給 bind() 的第一個參數(shù),傳入bind方法的第二個以及以后的參數(shù)加上綁定函數(shù)運行時本身的參數(shù)按照順序作為原函數(shù)的參數(shù)來調(diào)用原函數(shù)。bind返回的綁定函數(shù)也能使用 n...

    guyan0319 評論0 收藏0
  • 進階3-5深度解析 new 原理模擬實現(xiàn)

    摘要:使用指定的參數(shù)調(diào)用構(gòu)造函數(shù),并將綁定到新創(chuàng)建的對象。由構(gòu)造函數(shù)返回的對象就是表達式的結(jié)果。情況返回以外的基本類型實例中只能訪問到構(gòu)造函數(shù)中的屬性,和情況完全相反,結(jié)果相當于沒有返回值。 定義 new 運算符創(chuàng)建一個用戶定義的對象類型的實例或具有構(gòu)造函數(shù)的內(nèi)置對象的實例。 ——(來自于MDN) 舉個栗子 function Car(color) { this.color = co...

    Baaaan 評論0 收藏0
  • 進階 6-2 】深入高階函數(shù)應用之柯里化

    摘要:引言上一節(jié)介紹了高階函數(shù)的定義,并結(jié)合實例說明了使用高階函數(shù)和不使用高階函數(shù)的情況。我們期望函數(shù)輸出,但是實際上調(diào)用柯里化函數(shù)時,所以調(diào)用時就已經(jīng)執(zhí)行并輸出了,而不是理想中的返回閉包函數(shù),所以后續(xù)調(diào)用將會報錯。引言 上一節(jié)介紹了高階函數(shù)的定義,并結(jié)合實例說明了使用高階函數(shù)和不使用高階函數(shù)的情況。后面幾部分將結(jié)合實際應用場景介紹高階函數(shù)的應用,本節(jié)先來聊聊函數(shù)柯里化,通過介紹其定義、比較常見的...

    stackvoid 評論0 收藏0
  • 進階3-2】JavaScript深入之重新認識箭頭函數(shù)的this

    摘要:箭頭函數(shù)的尋值行為與普通變量相同,在作用域中逐級尋找。題目這次通過構(gòu)造函數(shù)來創(chuàng)建一個對象,并執(zhí)行相同的個方法。 我們知道this綁定規(guī)則一共有5種情況: 1、默認綁定(嚴格/非嚴格模式) 2、隱式綁定 3、顯式綁定 4、new綁定 5、箭頭函數(shù)綁定 其實大部分情況下可以用一句話來概括,this總是指向調(diào)用該函數(shù)的對象。 但是對于箭頭函數(shù)并不是這樣,是根據(jù)外層(函數(shù)或者全局)作用域(...

    Rainie 評論0 收藏0
  • 正在暑假中的《課多周刊》(第1)

    摘要:正在暑假中的課多周刊第期我們的微信公眾號,更多精彩內(nèi)容皆在微信公眾號,歡迎關(guān)注。若有幫助,請把課多周刊推薦給你的朋友,你的支持是我們最大的動力。原理微信熱更新方案漲知識了,熱更新是以后的標配。 正在暑假中的《課多周刊》(第1期) 我們的微信公眾號:fed-talk,更多精彩內(nèi)容皆在微信公眾號,歡迎關(guān)注。 若有幫助,請把 課多周刊 推薦給你的朋友,你的支持是我們最大的動力。 遠上寒山石徑...

    liukai90 評論0 收藏0

發(fā)表評論

0條評論

godlong_X

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<