摘要:之前文章詳細介紹了的使用,不了解的查看進階期。不同的引擎有不同的限制,核心限制在,有些引擎會拋出異常,有些不拋出異常但丟失多余參數。存儲的對象能動態增多和減少,并且可以存儲任何值。這邊采用方法來實現,拼成一個函數。
之前文章詳細介紹了 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,參數數組使用場景
下面列舉一些常用用法:
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, ...]
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 方法。
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
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,鏈接 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應用程序變得越來越強大,尤其一些新增加的功能例如:音頻視頻編輯,訪問WebSockets的原始數據等,很明顯有些時候如果使用JavaScript代碼可以快速方便地通過類型化數組來操作原始的二進制數據,這將會非常有幫助。
一句話就是,可以更快的操作復雜數據。
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"]
在子構造函數中,通過調用父構造函數的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()主要有以下兩點
1、call()改變了this的指向
2、函數 bar 執行了
如果在調用call()的時候把函數 bar()添加到foo()對象中,即如下
var foo = { value: 1, bar: function() { console.log(this.value); } }; foo.bar(); // 1
這個改動就可以實現:改變了this的指向并且執行了函數bar。
但是這樣寫是有副作用的,即給foo額外添加了一個屬性,怎么解決呢?
解決方法很簡單,用 delete 刪掉就好了。
所以只要實現下面3步就可以模擬實現了。
1、將函數設置為對象的屬性:foo.fn = bar
2、執行函數:foo.fn()
3、刪除函數: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模擬實現匯總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; }
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; }思考題
call 和 apply 的模擬實現有沒有問題?歡迎思考評論。
PS: 上期思考題留到下一期講解,下一期介紹重點介紹 bind 原理及實現
參考JavaScript深入之call和apply的模擬實現進階系列目錄MDN之Array.prototype.push()
MDN之Function.prototype.apply()
MDN之Array.prototype.slice()
MDN之Array.isArray()
JavaScript常用八種繼承方案
深入淺出 妙用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源碼解析
交流進階系列文章匯總如下,內有優質前端資料,覺得不錯點個star。
https://github.com/yygmind/blog
我是木易楊,網易高級前端工程師,跟著我每周重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100737.html
摘要:返回的綁定函數也能使用操作符創建對象這種行為就像把原函數當成構造器,提供的值被忽略,同時調用時的參數被提供給模擬函數。 bind() bind() 方法會創建一個新函數,當這個新函數被調用時,它的 this 值是傳遞給 bind() 的第一個參數,傳入bind方法的第二個以及以后的參數加上綁定函數運行時本身的參數按照順序作為原函數的參數來調用原函數。bind返回的綁定函數也能使用 n...
摘要:使用指定的參數調用構造函數,并將綁定到新創建的對象。由構造函數返回的對象就是表達式的結果。情況返回以外的基本類型實例中只能訪問到構造函數中的屬性,和情況完全相反,結果相當于沒有返回值。 定義 new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象的實例。 ——(來自于MDN) 舉個栗子 function Car(color) { this.color = co...
摘要:引言上一節介紹了高階函數的定義,并結合實例說明了使用高階函數和不使用高階函數的情況。我們期望函數輸出,但是實際上調用柯里化函數時,所以調用時就已經執行并輸出了,而不是理想中的返回閉包函數,所以后續調用將會報錯。引言 上一節介紹了高階函數的定義,并結合實例說明了使用高階函數和不使用高階函數的情況。后面幾部分將結合實際應用場景介紹高階函數的應用,本節先來聊聊函數柯里化,通過介紹其定義、比較常見的...
摘要:箭頭函數的尋值行為與普通變量相同,在作用域中逐級尋找。題目這次通過構造函數來創建一個對象,并執行相同的個方法。 我們知道this綁定規則一共有5種情況: 1、默認綁定(嚴格/非嚴格模式) 2、隱式綁定 3、顯式綁定 4、new綁定 5、箭頭函數綁定 其實大部分情況下可以用一句話來概括,this總是指向調用該函數的對象。 但是對于箭頭函數并不是這樣,是根據外層(函數或者全局)作用域(...
摘要:正在暑假中的課多周刊第期我們的微信公眾號,更多精彩內容皆在微信公眾號,歡迎關注。若有幫助,請把課多周刊推薦給你的朋友,你的支持是我們最大的動力。原理微信熱更新方案漲知識了,熱更新是以后的標配。 正在暑假中的《課多周刊》(第1期) 我們的微信公眾號:fed-talk,更多精彩內容皆在微信公眾號,歡迎關注。 若有幫助,請把 課多周刊 推薦給你的朋友,你的支持是我們最大的動力。 遠上寒山石徑...
閱讀 1155·2023-04-25 17:28
閱讀 3531·2021-10-14 09:43
閱讀 3954·2021-10-09 10:02
閱讀 1942·2019-08-30 14:04
閱讀 3128·2019-08-30 13:09
閱讀 3269·2019-08-30 12:53
閱讀 2896·2019-08-29 17:11
閱讀 1822·2019-08-29 16:58