ES6模擬 call 和 apply 和 bind

轉自 【進階3-3期】深度廣度解析 call 和 apply 原理、使用場景及實現

ES6本身實現

call

Function.prototype.call2 = function(context,...list) {
				context = context || window;
				context.fn = this;			 
				let result = context.fn(...list);
				delete context.fn
				return result;
			}

			var value = "JS";
			var foo = {
				value: "Node"
			};

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

			bar.call2(foo,1,2,3);
			bar.call2(null,4,5,6);
複製代碼

apply

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

			var value = "JS";
			var foo = {
				value: "Node"
			};

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

			bar.apply2(foo,[1,2,3]);
			bar.apply2(null,[4,5,6]);
複製代碼

bind1

Function.prototype.bind1 = function(context, ...arr1) {
				let func = this;
				return function(...arr2) {
					func.apply(context, [...arr1, ...arr2]);
					//   func.call(context, ...arr1, ...arr2);
				}
			}

			var value = "JS";
			var foo = {
				value: "Node"
			};

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

			bar.bind1(foo, 1, 2)(3, 4);
			bar.bind1(null, 5, 6)(7, 8);
			bar.bind1(null)(5, 6,7, 8);
複製代碼

bind2

Function.prototype.bind2 = function(context, ...arr1) {
				context = context || window;
				context.fn = this;			 
				return function(...arr2){
					return context.fn(...arr1,...arr2);
				};
			}
			var value = "JS";
			var foo = {
				value: "Node"
			};

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

			bar.bind2(foo, 1, 2)(3, 4);
			bar.bind2(null, 5, 6)(7, 8);
			bar.bind2(null)(5, 6,7, 8);
複製代碼

call() 和 apply()

call() 方法調用一個函數, 其具備一個指定的 this 值和分別地提供的參數(參數的列表)。html

call()apply()的區別在於,call()方法接受的是若干個參數的列表,而apply()方法接受的是一個包含多個參數的數組node

舉個例子:git

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

func.call(this, arg1, arg2); // 使用 call,參數列表
func.apply(this, [arg1, arg2]) // 使用 apply,參數數組複製代碼
複製代碼

經常使用用法

下面列舉一些經常使用用法:github

一、合併兩個數組
var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];

// 將第二個數組融合進第一個數組// 至關於 vegetables.push('celery', 'beetroot');Array.prototype.push.apply(vegetables, moreVegs);
// 4

vegetables;
// ['parsnip', 'potato', 'celery', 'beetroot']複製代碼
複製代碼

當第二個數組(如示例中的 moreVegs )太大時不要使用這個方法來合併數組,由於一個函數可以接受的參數個數是有限制的。不一樣的引擎有不一樣的限制,JS核心限制在 65535,有些引擎會拋出異常,有些不拋出異常但丟失多餘參數。數組

如何解決呢?方法就是將參數數組切塊後循環傳入目標方法bash

functionconcatOfArray(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, ...]複製代碼
複製代碼
二、獲取數組中的最大值和最小值
var numbers = [5, 458 , 120 , -215 ]; 
Math.max.apply(Math, numbers);   //458    Math.max.call(Math, 5, 458 , 120 , -215); //458// ES6Math.max.call(Math, ...numbers); // 458複製代碼
複製代碼

爲何要這麼用呢,由於數組 numbers 自己沒有 max方法,可是 Math有呀,因此這裏就是藉助 call / apply 使用 Math.max 方法。數據結構

三、驗證是不是數組
functionisArray(obj){ 
    returnObject.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 tokenNumber(123).toString(); // "123"Object(123).toString(); // "123"複製代碼
複製代碼

能夠經過toString() 來獲取每一個對象的類型,可是不一樣對象的 toString()有不一樣的實現,因此經過 Object.prototype.toString() 來檢測,須要以 call() / apply() 的形式來調用,傳遞要檢查的對象做爲第一個參數。app

另外一個驗證是不是數組的方法dom

var toStr = Function.prototype.call.bind(Object.prototype.toString);
functionisArray(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函數指定一個 this 值,而後 .bind 返回一個新的函數,始終將 Object.prototype.toString 設置爲傳入參數。其實等價於 Object.prototype.toString.call()函數

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

Object.prototype.toString = function() {
    return'';
}
isArray([1, 2, 3]);
// false複製代碼
複製代碼
四、類數組對象(Array-like Object)使用數組方法
var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a functionvar domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1"); // 505 不一樣環境下數據不一樣// (505) ["h1", html.gr__hujiang_com, head, meta, ...] 複製代碼
複製代碼

類數組對象有下面兩個特性

  • 一、具備:指向對象元素的數字索引下標和 length 屬性
  • 二、不具備:好比 pushshiftforEach 以及 indexOf等數組對象具備的方法

要說明的是,類數組對象是一個對象。JS中存在一種名爲類數組的對象結構,好比 arguments 對象,還有DOM API 返回的 NodeList 對象都屬於類數組對象,類數組對象不能使用 push/pop/shift/unshift 等數組方法,經過 Array.prototype.slice.call 轉換成真正的數組,就可使用 Array下全部方法。

類數組對象轉數組的其餘方法:

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

ES6:
let arr = Array.from(arguments);
let arr = [...arguments];
複製代碼
複製代碼

Array.from() 能夠將兩類對象轉爲真正的數組:類數組對象和可遍歷(iterable)對象(包括ES6新增的數據結構 Set 和 Map)。

PS擴展一:爲何經過 Array.prototype.slice.call() 就能夠把類數組對象轉換成數組?

其實很簡單,sliceArray-like 對象經過下標操做放進了新的 Array 裏面。

下面代碼是 MDN 關於 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 = newArray(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)將類數組對象轉換成數組,由於低版本IE(IE < 9)下的DOM對象是以 com 對象的形式實現的,js對象與 com 對象不能進行轉換。

兼容寫法以下:

functiontoArray(nodes){
    try {
        // works in every browser except IEreturnArray.prototype.slice.call(nodes);
    } catch(err) {
        // Fails in IE < 9var arr = [],
            length = nodes.length;
        for(var i = 0; i < length; i++){
            // arr.push(nodes[i]); // 兩種均可以
            arr[i] = nodes[i];
        }
        return arr;
    }
}
複製代碼
複製代碼

PS 擴展三:爲何要有類數組對象呢?或者說類數組對象是爲何解決什麼問題纔出現的?

JavaScript類型化數組是一種相似數組的對象,並提供了一種用於訪問原始二進制數據的機制。 Array存儲的對象能動態增多和減小,而且能夠存儲任何JavaScript值。JavaScript引擎會作一些內部優化,以便對數組的操做能夠很快。然而,隨着Web應用程序變得愈來愈強大,尤爲一些新增長的功能例如:音頻視頻編輯,訪問WebSockets的原始數據等,很明顯有些時候若是使用JavaScript代碼能夠快速方便地經過類型化數組來操做原始的二進制數據,這將會很是有幫助。

一句話就是,能夠更快的操做複雜數據。

五、調用父構造函數實現繼承
functionSuperType(){
    this.color=["red", "green", "blue"];
}
functionSubType(){
    // 核心代碼,繼承自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"]複製代碼
複製代碼

在子構造函數中,經過調用父構造函數的call方法來實現繼承,因而SubType的每一個實例都會將SuperType 中的屬性複製一份。

缺點:

  • 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
  • 沒法實現複用,每一個子類都有父類實例函數的副本,影響性能

更多繼承方案查看我以前的文章。JavaScript經常使用八種繼承方案

call的模擬實現

如下內容參考自 JavaScript深刻之call和apply的模擬實現

先看下面一個簡單的例子

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

functionbar() {
    console.log(this.value);
}

bar.call(foo); // 1複製代碼
複製代碼

經過上面的介紹咱們知道,call()主要有如下兩點

  • 一、call()改變了this的指向
  • 二、函數 bar 執行了
模擬實現第一步

若是在調用call()的時候把函數 bar()添加到foo()對象中,即以下

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

foo.bar(); // 1複製代碼
複製代碼

這個改動就能夠實現:改變了this的指向而且執行了函數bar

可是這樣寫是有反作用的,即給foo額外添加了一個屬性,怎麼解決呢?

解決方法很簡單,用 delete 刪掉就行了。

因此只要實現下面3步就能夠模擬實現了。

  • 一、將函數設置爲對象的屬性:foo.fn = bar
  • 二、執行函數:foo.fn()
  • 三、刪除函數:delete foo.fn

代碼實現以下:

// 初版Function.prototype.call2 = function(context) {
    // 首先要獲取調用call的函數,用this能夠獲取
    context.fn = this; 		// foo.fn = bar
    context.fn();			// foo.fn()delete context.fn;		// delete foo.fn
}

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

functionbar() {
    console.log(this.value);
}

bar.call2(foo); // 1複製代碼
複製代碼

完美!

模擬實現第二步

初版有一個問題,那就是函數 bar 不能接收參數,因此咱們能夠從 arguments中獲取參數,取出第二個到最後一個參數放到數組中,爲何要拋棄第一個參數呢,由於第一個參數是 this

類數組對象轉成數組的方法上面已經介紹過了,可是這邊使用ES3的方案來作。

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

參數數組搞定了,接下來要作的就是執行函數 context.fn()

context.fn( args.join(',') ); // 這樣不行復制代碼
複製代碼

上面直接調用確定不行,args.join(',')會返回一個字符串,並不會執行。

這邊採用 eval方法來實現,拼成一個函數。

eval('context.fn(' + args +')')
複製代碼
複製代碼

上面代碼中args 會自動調用 args.toString() 方法,由於'context.fn(' + args +')'本質上是字符串拼接,會自動調用toString()方法,以下代碼:

var args = ["a1", "b2", "c3"];
console.log(args);
// ["a1", "b2", "c3"]console.log(args.toString());
// a1,b2,c3console.log("" + args);
// a1,b2,c3複製代碼
複製代碼

因此說第二個版本就實現了,代碼以下:

// 第二版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
};

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

bar.call2(foo, 'kevin', 18); 
// kevin// 18// 1複製代碼
複製代碼

完美!!

模擬實現第三步

還有2個細節須要注意:

  • 一、this 參數能夠傳 null 或者 undefined,此時 this 指向 window
  • 二、函數是能夠有返回值的

實現上面的兩點很簡單,代碼以下

// 第三版Function.prototype.call2 = function (context) {
    context = context || window; // 實現細節 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; // 實現細節 2
}

// 測試一下var value = 2;

var obj = {
    value: 1
}

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

bar.call2(null); // 2console.log(bar.call2(obj, 'kevin', 18));
// 1// {//    value: 1,//    name: 'kevin',//    age: 18// }複製代碼
複製代碼

完美!!!

call和apply模擬實現彙總

call的模擬實現

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的模擬實現

ES3:

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

    var result;
    // 判斷是否存在第二個參數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;
}
複製代碼
複製代碼
相關文章
相關標籤/搜索