之前文章詳細介紹了 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穷躁、不具有:比如
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應(yīng)用程序變得越來越強大,尤其一些新增加的功能例如:音頻視頻編輯此迅,訪問WebSockets的原始數(shù)據(jù)等汽畴,很明顯有些時候如果使用JavaScript代碼可以快速方便地通過類型化數(shù)組來操作原始的二進制數(shù)據(jù),這將會非常有幫助耸序。
一句話就是忍些,可以更快的操作復(fù)雜數(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
中的屬性復(fù)制一份嘁酿。
缺點:
- 只能繼承父類的實例屬性和方法隙券,不能繼承原型屬性/方法
- 無法實現(xiàn)復(fù)用,每個子類都有父類實例函數(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
完美W锼投蝉!
模擬實現(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
// }
完美5柩痢S褂椤!
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;
}
思考題
call
和 apply
的模擬實現(xiàn)有沒有問題谐算?歡迎思考評論熟尉。
PS: 上期思考題留到下一期講解,下一期介紹重點介紹 bind
原理及實現(xiàn)
參考
JavaScript深入之call和apply的模擬實現(xiàn)
進階系列目錄
- 【進階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往果。
我是木易楊,網(wǎng)易高級前端工程師一铅,跟著我每周重點攻克一個前端面試重難點棚放。接下來讓我?guī)阕哌M高級前端的世界,在進階的路上馅闽,共勉!