call
基础
var func=function(arg1,arg2){
//
}
func.call(this,arg1,arg2);
func.apply(this,[arg1,arg2]);
使用场景
1.合并两个数组
var a=[1,2]
var b=[3,4]
Array.prototype.push.apply(a,b)
方法对于第二个数组的长度有限制,因为一个函数接受的参数接受的参数个数是有限制的,不同引擎的限制不同,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;
}
2.获取数组中的最大值和最小值
var arr=[1,2,3,4,5]
Math.max.apply(Math,arr)
Math.max.call(Math,1,2,3,4)
Math.max.call(Math,...arr)
3.数据类型的校验
Object.prototype.toString.call({}).replace(/^\[object\s|]$/g,'')
可以通过toString()来获取每个对象的类型。不同对象的toString实现不同。前提是方法没有被覆盖
4.类数组对象(Array-like Object)使用数组的方法
var domNodeArray=Array.prototype.slice.call(domNodes)
类数组对象具有下面两个特性。
- 具有指向对象元素的数字索引下标和length属性。
- 不具有push,shift,forEach,indexOf等数组对象具有的方法。
类数组对象是一个对象,比如:JS的 arguments对象,DOM API返回的NodeList,
var array=Array.prototype.slice.call(arguments)
array=[].slice.call(arguments)
array=array.from(arguments)
array=[...arguments]
Array.from()可以将类数组对象和可遍历对象(包括Set和Map)转为真正的数组。
Q1.为什么通过Array.prototype.slice.call()就可以把类数组对象转换成数组。
A:slice将Array-like对象通过下标操作放进了新的Array里。
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;
};
}
Q2.通过Array.prototype.slice.call()存在的问题。
A. 对于版本小于9的IE浏览器会出问题,因为其DOM对象是以com对象的形式实现的,js对象与com对象不能进行转换。
Q3.为什么有类数组对象,类数组对象是为解决什么问题才出现的。
A. 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中的属性复制一份。
缺点:1. 只能继承父类的实例属性和方法,不能继承原型方法/属性。2.无法实现复用,每个子类都有父类实例函数的副本,影响性能。
模拟实现
需要注意的点
- 实现时需要先将方法添加到对向上,然后再删除,所以需要确保增加的fn的key是唯一的。
- 注意传入的参数this是可选的,需要给默认值。
- 在函数内对于不确定个数的参数可以用 arguments 来获取参数。
- 执行传入的函数,可以借助于eval为其传入参数。
```javascript
//需要确保fn的唯一
call
// es3
Function.prototype.call2 = function (context) {
context = context ? Object(context) : window;
var args = [];
for (var i = 1; i < arguments.length; i++) {
} context.fn = this; var result = eval(‘context.fn(‘ + args + ‘)’); delete context.fn; return result }args.push('arguments[' + i + ']');
//es6 Function.prototype.call2 = function (context, …arg) { context = context ? Object(context) : window; context.fn = this; const result = context.fn(…arg); delete context.fn; return result };
apply //es3 Function.prototype.apply2 = function (context, argsArray) { context = context ? Object(context) : window; context.fn = this; var result; if (!argsArray) { result = context.fn() } else { var arg = []; for (var i = 0; i < argsArray.length; i++) { arg.push(‘argsArray[‘ + i + ‘]’) } eval(‘context.fn(‘ + arg + ‘)’) } delete context.fn; return result };
//es6 Function.prototype.apply2 = function (context, argsArray) { context = context ? Object(context) : window; context.fn = this; const result = context.fn(…argsArray); delete context.fn; return result };
存在的问题,需要保证fn的唯一性
//es3 var fnFactory = function (context) { var unique_fun = ‘fn’; while (context.hasOwnProperty(unique_fun)) { unique_fun = “fn” + Math.random(); } return unique_fun };
Function.prototype.call2 = function (context) { context = context ? Object(context) : window; var fn = fnFactory(context); context[fn] = this; var args = []; for (var i = 0, len = arguments.length; i < len; i++) { args.push(‘arguments[‘ + i + ‘]’); } var result = eval(‘contextfn; delete context[fn] return result };
//es6 Function.prototype.call2 = function (context, …args) { context = context ? Object(context) : window; const fn = Symbol(); context[fn] = this; const result = contextfn; delete context[fn]; return result };
<a name="eae81273"></a>
#### in 和hasOwnproperty
in操作符会检查属性是否存在对象以及[[Prototype]]原型链中。<br />Object.hasOwnProperty(),只会检查是否出现在对象中,不会检查原型链。
> in检查的是属性名。
> 所有的普通对象都可以调用hasOwnProperty来访问。对于Object.create(null)创建的需要使用call。
<a name="qRFtd"></a>
# bind
> bind方法会返回一个新函数。这个新函数被调用时,this值是bind函数的第一个参数。bind()的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
**bind方法与call/apply最大的不同是前者返回一个绑定上下文的函数,后两者是直接执行了函数**<br />举例子
var value = 2;
var foo = { value: 1 };
function bar(name, age) { return { value: this.value, name: name, age: age } };
bar.call(foo, “Jack”, 20); // 直接执行了函数 // {value: 1, name: “Jack”, age: 20}
var bindFoo1 = bar.bind(foo, “Jack”, 20); // 返回一个函数 bindFoo1(); // {value: 1, name: “Jack”, age: 20}
var bindFoo2 = bar.bind(foo, “Jack”); // 返回一个函数 bindFoo2(20); // {value: 1, name: “Jack”, age: 20}
bind函数的四大特性:**指定this,返回一个函数,可以传入参数,柯里化**
<a name="Q6ei6"></a>
## 使用场景
**1.业务场景**
var nickname = “Kitty”; function Person(name){ this.nickname = name; this.distractedGreeting = function() {
setTimeout(function(){
console.log("Hello, my name is " + this.nickname);
}, 500);
}
}
var person = new Person(‘jawil’); person.distractedGreeting(); //Hello, my name is Kitty
**解决方案1**:缓存this值
var nickname = “Kitty”; function Person(name){ this.nickname = name; this.distractedGreeting = function() {
var self = this; // added
setTimeout(function(){
console.log("Hello, my name is " + self.nickname); // changed
}, 500);
}
}
var person = new Person(‘jawil’); person.distractedGreeting(); // Hello, my name is jawil
**解决方案2**:使用bind
var nickname = “Kitty”; function Person(name){ this.nickname = name; this.distractedGreeting = function() {
setTimeout(function(){
console.log("Hello, my name is " + this.nickname);
}.bind(this), 500);
}
}
var person = new Person(‘jawil’); person.distractedGreeting(); // Hello, my name is jawil
**2.验证是否是数组**
function isArray(obj){
return Object.prototype.toString.call(obj)===’[object Array]’
}
通过tostring来获取每个对象的类型,不同的对象有不同的实现,所以通过`Object.prototype.toString()`来检测,需要以call,apply的形式来调用。传递要检查的对象作为第一个参数。
var toStr=Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){ return toStr(obj)===’[object Array]’; }
首先使用Function.prototype.call函数指定一个this值,然后.bind返回一个新的参数,始终将Object.prototype.toString设置为参数传入,等价于Object.prototype.toString.call(),前提是toString()没有被覆盖
**3.柯里化(curry)**
> 只传递给函数一部分参数来调用它,让他返回一个函数去处理剩下的参数。
> 一次性定义柯里化函数
var add = function(x) { return function(y) { return x + y; }; };
var increment = add(1); var addTen = add(10);
increment(2); // 3
addTen(2); // 12
add(1)(2); // 3
定义一个add函数,接受一个参数返回新的函数,调用add之后,返回的函数就通过闭包的形式记住了第一次的参数,所以说bind本身也是闭包的一种使用场景。
<a name="Yjeq1"></a>
## 模拟实现
bind函数是在es5被引入,IE8下不支持,需要使用Polyfill来实现。<br />需要实现的特点,1.指定this, 2.返回一个函数,3.可以传入参数,4.柯里化。
**第一步**<br />使用call/apply指定this。使用return返回一个函数。实现1,2
Function.prototype.bind2 = function (context) { var self = this; return function () { return self.call(context) }
};
**模拟实现第二步**<br />使用arguments实现第三点
//es3 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1) return function () { var bindArgs = Array.prototype.slice.call(arguments) return self.apply(context, args.concat(bindArgs)) } };
//es6
Function.prototype.bind2 = function (context, …args) { var self = this; return function (…argsExec) { return self.apply(context, […args, …argsExec]) } };
**模拟实现第三步**<br />bind的一个特定,难点
> bind函数可以使用new操作符创建对象,这种行为想把原函数当成构造器,提供的this值被忽略,同时调用时的参数能被提供给模拟函数。
var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = ‘shopping’; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = ‘kevin’;
var bindFoo = bar.bind(foo, ‘Jack’); var obj = new bindFoo(20); // undefined // Jack // 20
obj.habit; // shopping
obj.friend; // kevin
上面例子中,运行结果this.value 输出为 undefined,这不是全局value 也不是foo对象中的value,这说明 bind 的 this 对象失效了,new 的实现中生成一个新的对象,这个时候的 this指向的是 obj。
通过修改返回函数的原型来实现
Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); // 注释1 return self.apply( this instanceof fBound ? this : context, args.concat(bindArgs) ); // 注释2 fBound.prototype = this.prototype; return fBound; } };
注释1:当作为构造函数时,this指向实例,可以让实例获得来自绑定函数的值<br />当作为普通函数时,将函数的this指向context<br />注释2:修改返回函数的prototype为绑定函数的prototype。实例可以继承绑定函数的原型的值。
**模拟实现第四步**<br />实现第三步的缺点fBound.prototype=this.prototype有个缺点,修改fBound.prototype也会直接修改this.prototype。
解决方案可以使用一个空的对象作为中介,把fBound赋值为空的对象的实例。或者直接使用Object.create()生成一个新对象。<br />不过bind()和Object.create()都是ES5的方法,IE<9不支持。所以Polyfill不能用Object.create()实现bind,原理是一样的。
Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP=function(){} var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply( this instanceof fBound ? this : context, args.concat(bindArgs) ); fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; } };
**模拟实现第五步**<br />处理调用bind的不是函数,抛出异常。
if(typeof this !==’function’){ throw Error(‘Function.prototype.bind - what is trying to be bound is not callable’) }
Function.prototype.bind2 = function (context) { if (typeof this !== ‘function’) { throw new Error(“Function.prototype.bind - what is trying to be bound is not callable”)
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {
};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(
this instanceof fBound ? this : context,
args.concat(bindArgs)
);
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
};
<a name="mL7pv"></a>
# 例题
<a name="yAm1Y"></a>
## 实现如下函数
```javascript
add(1); // 1
add(1)(2); // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10
解答:答疑函数会调用toString方法,只需要让函数返回一个闭包,并重写返回的函数的toString方法即可。
function add(a) {
function sum(b) {
a = a + b;
return sum
}
sum.toString = function () {
return a;
};
return sum
}
一个函数多次bind
function getName() {
return this.name;
}
const obj1 = {
name: "obj1"
}
const obj2 = {
name: "obj2"
}
var getNameBindOnce = getName.bind(obj1);
var getNameBindTwice = getNameBindOnce.bind(obj2);
console.log("getName", getName()) // undefined
console.log("getNameBindOnce", getNameBindOnce()) // obj1
console.log("getNameBindTwice", getNameBindTwice()) // obj1
// 一个函数多次bind,只有第一次生效了