之前文章詳細介紹了 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 = ;
for (var i = , len = arr2.length; i < len; i += QUANTUM) {
Array.prototype.push.apply(
arr1,
arr2.slice(i, Math.min(i + QUANTUM, len) )
);
}
return arr1;
}
// 驗證代碼
var arr1 = [, , ];
var arr2 = [];
for(var i = ; i < ; 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 = [, , , ];
Math.max.apply(Math, numbers); //458
Math.max.call(Math, , , , ); //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([, , ]);
// true
// 直接使用 toString()
[, , ].toString(); // "1,2,3"
"123".toString(); // "123"
toString(); // SyntaxError: Invalid or unexpected token
Number().toString(); // "123"
Object().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([, , ]);
// true
// 使用改造後的 toStr
toStr([, , ]); // "[object Array]"
toStr("123"); // "[object String]"
toStr(); // "[object Number]"
toStr(Object()); // "[object Number]"
複制代碼
上面方法首先使用
Function.prototype.call
函數指定一個
this
值,然後
.bind
傳回一個新的函數,始終将
Object.prototype.toString
設定為傳入參數。其實等價于
Object.prototype.toString.call()
。
這裡有一個前提是
toString()
方法沒有被覆寫
Object.prototype.toString = function() {
return '';
}
isArray([, , ]);
// 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,連結 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 || ;
start = (start >= ) ? start : Math.max(, len + start);
// Handle negative value for "end"
var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
if (end < ) {
upTo = len + end;
}
// Actual expected size of the slice
size = upTo - start;
if (size > ) {
cloned = new Array(size);
if (this.charAt) {
for (i = ; i < size; i++) {
cloned[i] = this.charAt(start + i);
}
} else {
for (i = ; 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 = ; i < length; i++){
// arr.push(nodes[i]); // 兩種都可以
arr[i] = nodes[i];
}
return arr;
}
}
複制代碼
PS 擴充三:為什麼要有類數組對象呢?或者說類數組對象是為什麼解決什麼問題才出現的?
JavaScript類型化數組是一種類似數組的對象,并提供了一種用于通路原始二進制資料的機制。 Array
存儲的對象能動态增多和減少,并且可以存儲任何JavaScript值。JavaScript引擎會做一些内部優化,以便對數組的操作可以很快。然而,随着Web應用程式變得越來越強大,尤其一些新增加的功能例如:音頻視訊編輯,通路WebSockets的原始資料等,很明顯有些時候如果使用JavaScript代碼可以快速友善地通過類型化數組來操作原始的二進制資料,這将會非常有幫助。
一句話就是,可以更快的操作複雜資料。
5、調用父構造函數實作繼承
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 = ;
var foo = {
value:
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
複制代碼
通過上面的介紹我們知道,
call()
主要有以下兩點
- 1、
改變了this的指向call()
- 2、函數
執行了bar
模拟實作第一步
如果在調用
call()
的時候把函數
bar()
添加到
foo()
對象中,即如下
var foo = {
value: ,
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:
};
function bar() {
console.log(this.value);
}
bar.call2(foo); // 1
複制代碼
完美!
模拟實作第二步
第一版有一個問題,那就是函數
bar
不能接收參數,是以我們可以從
arguments
中擷取參數,取出第二個到最後一個參數放到數組中,為什麼要抛棄第一個參數呢,因為第一個參數是
this
。
類數組對象轉成數組的方法上面已經介紹過了,但是這邊使用ES3的方案來做。
var args = [];
for(var i = , 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 = , len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args +')');
delete context.fn;
}
// 測試一下
var foo = {
value:
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'kevin', );
// kevin
// 18
// 1
複制代碼
完美!!
模拟實作第三步
還有2個細節需要注意:
- 1、this 參數可以傳
或者null
,此時 this 指向 windowundefined
- 2、this 參數可以傳基本類型資料,原生的 call 會自動用 Object() 轉換
- 3、函數是可以有傳回值的
實作上面的三點很簡單,代碼如下
// 第三版
Function.prototype.call2 = function (context) {
context = context ? Object(context) : window; // 實作細節 1 和 2
context.fn = this;
var args = [];
for(var i = , len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result; // 實作細節 2
}
// 測試一下
var value = ;
var obj = {
value:
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
function foo() {
console.log(this);
}
bar.call2(null); // 2
foo.call2(); // Number {123, fn: ƒ}
bar.call2(obj, 'kevin', );
// 1
// {
// value: 1,
// name: 'kevin',
// age: 18
// }
複制代碼
完美!!!
call和apply模拟實作彙總
call的模拟實作
ES3:
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
context.fn = this;
var args = [];
for(var i = , 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 ? Object(context) : window;
context.fn = this;
let args = [...arguments].slice();
let result = context.fn(...args);
delete context.fn
return result;
}
複制代碼
apply的模拟實作
ES3:
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
var result;
// 判斷是否存在第二個參數
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = , 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 ? Object(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。
github.com/yygmind/blo…
我是木易楊,網易進階前端工程師,跟着我每周重點攻克一個前端面試重難點。接下來讓我帶你走進進階前端的世界,在進階的路上,共勉!