call、apply、bind区别与使用
1.call、apply、bind基本介绍
语法:
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
返回值:
call/apply :fun执行的结果
bind :返回fun的拷贝,并拥有指定的this值和初始参数。
参数:
thisArg(可选)
- fun的this指向thisArg对象
- 非严格模式下:thisArg指定为null、undefined、fun中的他会死指向window对象
- 严格模式下,fun的this为undefined
- 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。
param1, param2(可选):传给fun的参数
- 如果param不传或者为null、undefined,则表示不需要传入任何参数
- apply第二个参数是数组,数组内的值为传递给fun的参数
** 调用call、apply、bind的必须是函数 **
作用:
改变函数执行时的this指向,目前所有关于他们的运用,都是基于这一点来进行的。
call/apply与bind的区别
**** 执行 ****
- call/apply改变了函数的this上下文后马上执行该函数
- bind则是返回改变了上下文后的函数,不执行该函数
**** 返回值****
- call/apply :fun执行的结果
- bind :返回fun的拷贝,并拥有指定的this值和初始参数。
2.call、apply、bind的核心理念:借用方法
借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存。
3.call/apply的应用场景
1). 判断数据类型
function isType(data, type) {
const typeObj = {
'[object String]': 'string',
'[object Number]': 'number',
'[object Boolean]': 'boolean',
'[object Null]': 'null',
'[object Undefined]': 'undefined',
'[object Object]': 'object',
'[object Array]': 'array',
'[object Function]': 'function',
'[object Date]': 'date', // Object.prototype.toString.call(new Date())
'[object RegExp]': 'regExp',
'[object Map]': 'map',
'[object Set]': 'set',
'[object HTMLDivElement]': 'dom', // document.querySelector('#app')
'[object WeakMap]': 'weakMap',
'[object Window]': 'window', // Object.prototype.toString.call(window)
'[object Error]': 'error', // new Error('1')
'[object Arguments]': 'arguments',
}
let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型
let typeName = typeObj[name] || '未知类型' // 匹配数据类型
return typeName === type // 判断该数据类型是否为传入的类型
}
console.log(
isType({}, 'object'), // true
isType([], 'array'), // true
isType(new Date(), 'object'), // false
isType(new Date(), 'date'), // true
)
2).类数组借用数组的方法
var arrayLike = {
0: 'OB',
1: 'Koro1',
length: 2
}
Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2');
console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}
3).apply获取数组中最值
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
4.call、apply该用哪个?
- 参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply。
- 考虑可读性:参数数量不多就用call,参数数量比较多的话,把参数整合成数组,使用apply。
- 参数集合已经是一个数组的情况,用apply,比如上文的获取数组最大值/最小值。
5.bind的应用场景
1).保存函数参数
for (var i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i) // 依次输出:6 6 6 6 6
}, i * 1000);
}
上述代码每各1秒输出一次6,因为受js运行机制影响,setTimeout异步执行,属于宏任务,等定时器开始执行的时候,i的值已经6。
让输出结果为1,2,3,4,5的方法如下:
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function () {
console.log('闭包:', i); // 依次输出:1 2 3 4 5
}, i * 1000);
}(i));
}
for (var i = 1; i <= 5; i++) {
// 缓存参数
setTimeout(function (i) {
console.log('bind', i) // 依次输出:1 2 3 4 5
}.bind(null, i), i * 1000);
}
实际上这里也用了闭包,我们知道bind会返回一个函数,这个函数也是闭包。
for (let i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i) // 依次输出:1 2 3 4 5
}, i * 1000);
}
2).回调函数this丢失问题
this.pageClass = new Page(this.handleMessage.bind(this)) // 绑定回调函数的this指向
or
this.pageClass = new Page(() => this.handleMessage()) // 箭头函数绑定this指向
6.手写call/apply、bind
#1.手写call思路
- 根据call的规则设置上下文对象,也就是this的指向。
- 通过设置context的属性,将函数的this指向隐式绑定到context上
- 通过隐式绑定执行函数并传递参数。
- 删除临时属性,返回函数执行结果
Function.prototype.myCall = function (context, ...arr) {
if (context === null || context === undefined) {
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
context = window
} else {
context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
delete context[specialPrototype]; // 删除上下文对象的属性
return result; // 返回函数执行结果
};
**** 拓展:****
- Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。
- 每个Symbol()返回的symbol值都是唯一的。
- 一个symbol值能够作为对象属性的标识符。
- _symbol 是一种基本数据类型 _
- 可以使用symbol来作为对象属性名。
- Symbol类型的key是不能通过
Object.keys()
或者for...in
来枚举的,它未被包含在对象自身的属性名集合(property names)之中。
获取symbol方式定义的对象属性方法:
// 使用Object的API
Object.getOwnPropertySymbols(obj) // [Symbol(name)]
// 使用新增的反射API
Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
#2.手写apply思路:
- 传递给函数的参数处理,不太一样,其他部分跟call一样。
- apply接受第二个参数为类数组对象, 这里用了JavaScript权威指南中判断是否为类数组对象的方法。
Function.prototype.myApply = function (context) {
if (context === null || context === undefined) {
context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
} else {
context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
// JavaScript权威指南判断是否为类数组对象
function isArrayLike(o) {
if (o && // o不是null、undefined等
typeof o === 'object' && // o是对象
isFinite(o.length) && // o.length是有限数值
o.length >= 0 && // o.length为非负值
o.length === Math.floor(o.length) && // o.length是整数
o.length < 4294967296) // o.length < 2^32
return true
else
return false
}
const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
context[specialPrototype] = this; // 隐式绑定this指向到context上
let args = arguments[1]; // 获取参数数组
let result
// 处理传进来的第二个参数
if (args) {
// 是否传递第二个参数
if (!Array.isArray(args) && !isArrayLike(args)) {
throw new TypeError('myApply 第二个参数不为数组并且不为类数组对象抛出错误');
} else {
args = Array.from(args) // 转为数组
result = context[specialPrototype](...args); // 执行函数并展开数组,传递函数参数
}
} else {
result = context[specialPrototype](); // 执行函数
}
delete context[specialPrototype]; // 删除上下文对象的属性
return result; // 返回函数执行结果
};
#3.手写bind思路
1.拷贝源函数:
- 通过变量储存源函数
- 使用Object.create复制源函数的prototype给fToBind
2.返回拷贝的函数
3.调用拷贝的函数:
- new调用判断:通过instanceof判断函数是否通过new调用,来决定绑定的context
- 绑定this+传递参数
- 返回源函数的执行结果
Function.prototype.myBind = function (objThis, ...params) {
const thisFn = this; // 存储源函数以及上方的params(函数参数)
// 对返回的函数 secondParams 二次传参
let fToBind = function (...secondParams) {
console.log('secondParams',secondParams,...secondParams)
const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用
const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上
return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果
};
fToBind.prototype = Object.create(thisFn.prototype); // 复制源函数的prototype给fToBind
return fToBind; // 返回拷贝的函数
};