摘要:之前文章詳細介紹了的使用,不了解的查看進階期。不同的引擎有不同的限制,核心限制在,有些引擎會拋出異常,有些不拋出異常但丟失多余參數(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ù)組使用場景
下面列舉一些常用用法:
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, ...]
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 方法。
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
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、不具有:比如 push 、shift、 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ù)組?
其實很簡單,slice 將 Array-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ù)。
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í)行了
如果在調(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
完美!
第一版有一個問題,那就是函數(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
完美??!
還有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)匯總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; // 判斷是否存在第二個參數(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; }思考題
call 和 apply 的模擬實現(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
摘要:返回的綁定函數(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...
摘要:使用指定的參數(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...
摘要:引言上一節(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ù)柯里化,通過介紹其定義、比較常見的...
摘要:箭頭函數(shù)的尋值行為與普通變量相同,在作用域中逐級尋找。題目這次通過構(gòu)造函數(shù)來創(chuàng)建一個對象,并執(zhí)行相同的個方法。 我們知道this綁定規(guī)則一共有5種情況: 1、默認綁定(嚴格/非嚴格模式) 2、隱式綁定 3、顯式綁定 4、new綁定 5、箭頭函數(shù)綁定 其實大部分情況下可以用一句話來概括,this總是指向調(diào)用該函數(shù)的對象。 但是對于箭頭函數(shù)并不是這樣,是根據(jù)外層(函數(shù)或者全局)作用域(...
摘要:正在暑假中的課多周刊第期我們的微信公眾號,更多精彩內(nèi)容皆在微信公眾號,歡迎關(guān)注。若有幫助,請把課多周刊推薦給你的朋友,你的支持是我們最大的動力。原理微信熱更新方案漲知識了,熱更新是以后的標配。 正在暑假中的《課多周刊》(第1期) 我們的微信公眾號:fed-talk,更多精彩內(nèi)容皆在微信公眾號,歡迎關(guān)注。 若有幫助,請把 課多周刊 推薦給你的朋友,你的支持是我們最大的動力。 遠上寒山石徑...
閱讀 1166·2023-04-25 17:28
閱讀 3617·2021-10-14 09:43
閱讀 3978·2021-10-09 10:02
閱讀 1951·2019-08-30 14:04
閱讀 3142·2019-08-30 13:09
閱讀 3280·2019-08-30 12:53
閱讀 2907·2019-08-29 17:11
閱讀 1833·2019-08-29 16:58