【進階3-3期】深度廣度解析 call 和 apply 原理、使用場景及實現
本週的主題是 this全面解析 ,本計劃一共28期, 每期重點攻克一個面試重難點 ,如果你還不瞭解本進階計劃,文末點選檢視全部文章。
如果覺得本系列不錯,歡迎轉發,您的支援就是我堅持的最大動力。
之前文章詳細介紹了 this 的使用,不瞭解的檢視【進階3-1期】。
call() 和 apply()
call() 方法呼叫一個函式, 其具有一個指定的 this
值和分別地提供的引數( 引數的列表 )。
call()
和 apply()
的區別在於, call()
方法接受的是 若干個引數的列表 ,而 apply()
方法接受的是 一個包含多個引數的陣列
舉個例子:
var func = function(arg1, arg2) { ... }; func.call(this, arg1, arg2); // 使用 call,引數列表 func.apply(this, [arg1, arg2]) // 使用 apply,引數陣列 複製程式碼
常用用法
下面列舉一些常用用法:
1、合併兩個陣列
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,有些引擎會丟擲異常,有些不丟擲異常但丟失多餘引數。
如何解決呢?方法就是 將引數陣列切塊後迴圈傳入目標方法
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、獲取陣列中的最大值和最小值
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 複製程式碼
為什麼要這麼用呢,因為陣列 numbers
本身沒有 max
方法,但是 Math
有呀,所以這裡就是藉助 call / apply
使用 Math.max
方法。
3、驗證是否是陣列
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()
有不同的實現,所以通過 Object.prototype.toString()
來檢測,需要以 call() / apply()
的形式來呼叫,傳遞要檢查的物件作為第一個引數。
另一個 驗證是否是陣列 的方法
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
函式指定一個 this
值,然後 .bind
返回一個新的函式,始終將 Object.prototype.toString
設定為傳入引數。其實等價於 Object.prototype.toString.call()
。
這裡有一個 前提 是 toString()
方法 沒有被覆蓋
Object.prototype.toString = function() { return ''; } isArray([1, 2, 3]); // false 複製程式碼
4、類陣列物件(Array-like Object)使用陣列方法
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 不同環境下資料不同 // (505) ["h1", html.gr__hujiang_com, head, meta, ...] 複製程式碼
類陣列物件有下面兩個特性
- 1、具有:指向物件元素的數字索引下標和
length
屬性 - 2、不具有:比如
push
、shift
、forEach
以及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()
就可以把類陣列物件轉換成陣列?
其實很簡單, slice
將 Array-like
物件通過下標操作放進了新的 Array
裡面。
下面程式碼是 MDN 關於 slice
的Polyfill,連結 JavaScript%2FReference%2FGlobal_Objects%2FArray%2Fslice" rel="nofollow,noindex">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)
將類陣列物件轉換成陣列,因為低版本IE(IE < 9)下的 DOM
物件是以 com
物件的形式實現的,js物件與 com
物件不能進行轉換。
相容寫法如下:
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 擴充套件三:為什麼要有類陣列物件呢?或者說類陣列物件是為什麼解決什麼問題才出現的?
JavaScript型別化陣列是一種類似陣列的 物件 ,並提供了一種用於訪問原始二進位制資料的機制。 Array
儲存的物件能動態增多和減少,並且可以儲存任何JavaScript值。JavaScript引擎會做一些內部優化,以便對陣列的操作可以很快。然而,隨著Web應用程式變得越來越強大,尤其一些新增加的功能例如:音訊視訊編輯,訪問Socket/">WebSockets的原始資料等,很明顯有些時候如果使用JavaScript程式碼可以快速方便地通過型別化陣列來操作原始的二進位制資料,這將會非常有幫助。
一句話就是,可以更快的操作複雜資料。
5、呼叫父建構函式實現繼承
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 }; function bar() { console.log(this.value); } bar.call(foo); // 1 複製程式碼
通過上面的介紹我們知道, call()
主要有以下兩點
call() 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 }; function bar() { 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,c3 console.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 }; function bar(name, age) { console.log(name) console.log(age) console.log(this.value); } bar.call2(foo, 'kevin', 18); // kevin // 18 // 1 複製程式碼
完美!!
模擬實現第三步
還有2個細節需要注意:
- 1、this 引數可以傳
null
或者undefined
,此時 this 指向 window - 2、函式是可以有返回值的
實現上面的兩點很簡單,程式碼如下
// 第三版 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 } 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模擬實現彙總
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; } 複製程式碼
PS: 上期思考題留到下一期講解,下一期介紹重點介紹 bind
原理及實現
參考
MDN之Function.prototype.apply()
深入淺出 妙用Javascript中apply、call、bind
進階系列目錄
- 【進階1期】 呼叫堆疊
- 【進階2期】 作用域閉包
- 【進階3期】 this全面解析
- 【進階4期】 深淺拷貝原理
- 【進階5期】 原型Prototype
- 【進階6期】 高階函式
- 【進階7期】 事件機制
- 【進階8期】 Event Loop原理
- 【進階9期】 Promise原理
- 【進階10期】Async/Await原理
- 【進階11期】防抖/節流原理
- 【進階12期】模組化詳解
- 【進階13期】ES6重難點
- 【進階14期】計算機網路概述
- 【進階15期】瀏覽器渲染原理
- 【進階16期】webpack配置
- 【進階17期】webpack原理
- 【進階18期】前端監控
- 【進階19期】跨域和安全
- 【進階20期】效能優化
- 【進階21期】VirtualDom原理
- 【進階22期】Diff演算法
- 【進階23期】MVVM雙向繫結
- 【進階24期】Vuex原理
- 【進階25期】Redux原理
- 【進階26期】路由原理
- 【進階27期】VueRouter原始碼解析
- 【進階28期】ReactRouter原始碼解析
交流
進階系列文章彙總: github.com/yygmind/blo… ,覺得不錯歡迎點個star
我是木易楊,網易高階前端工程師,跟著我 每週重點攻克一個前端面試重難點 。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!
