面试中可能会问到js中关于call、apply、bind的问题,例如
- 怎么利用call、apply来求一个数组中最大或者最小值
- 如何利用call、apply来做继承
- apply、call、bind的区别和主要应用场景
- .....
首先,要明白这三个函数的存在意义?答案是改变函数执行时的上下文,再具体一点就是改变函数运行时的this指向。
call/apply/bind的核心理念:借用方法
借用网上的例子:
生活中:平时没时间做饭的我,周末想给孩子炖个乌鸡汤尝尝。但是没有适合的锅,而我又不想出去买。所以就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一举两得。
程序中:A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?当然是借用 A 对象的方法啦,既达到了目的,又节省了内存。
这就是call/apply/bind的核心理念:借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存。
1、基本语法
1.1 call():Function.call(thisArg, param1, param2, ...)
- 调用 call 的对象,必须是个函数 Function。
- call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
- 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
1.2 apply():Function.apply(thisArg, [param1,param2,...])
- 它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
- 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
1.3 bind():Function.bind(thisArg, param1, param2, ...)
- bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。
- 不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行,而 apply 和 call 则是立即调用。
2、call、apply、bind基本使用介绍
let obj = {name: 'tony'};
function Child(name){
this.name = name;
}
Child.prototype = {
constructor: Child,
showName: function(){
console.log(this.name);
}
}
var child = new Child('thomas');
child.showName(); // thomas
// call,apply,bind使用
child.showName.call(obj);// tony
child.showName.apply(obj);// tony
let bind = child.showName.bind(obj); // 返回一个函数
bind(); // tony
我们拿别人的showName方法,并动态改变其上下文帮自己输出了信息,说到底就是实现了复用
3、应用场景
3.1 判断数据类型
借用Object.prototype.toString用来判断几乎所有类型的数据,它返回的是一个表示对象内部 [[Class]] 属性的字符串,这个属性是对象在JavaScript内部使用的类型标识
// 定义一个函数isType,用于判断给定的数据data是否属于指定的类型type
function isType(data, type) {
// 定义一个对象typeObj,包含JavaScript中各种数据类型对应的字符串表示及其简化名称
const typeObj = {
'[object String]': 'string', // 字符串类型
'[object Number]': 'number', // 数字类型
'[object Boolean]': 'boolean', // 布尔类型
'[object Null]': 'null', // null类型
'[object Undefined]': 'undefined', // undefined类型
'[object Object]': 'object', // 普通对象类型
'[object Array]': 'array', // 数组类型
'[object Function]': 'function', // 函数类型
'[object Date]': 'date', // 日期类型
'[object RegExp]': 'regExp', // 正则表达式类型
'[object Map]': 'map', // Map类型
'[object Set]': 'set', // Set类型
'[object HTMLDivElement]': 'dom', // DOM元素类型(以HTMLDivElement为例)
'[object WeakMap]': 'weakMap', // WeakMap类型
'[object Window]': 'window', // Window对象类型
'[object Error]': 'error', // Error对象类型
'[object Arguments]': 'arguments', // arguments对象类型(注意:在ES6及更高版本中,arguments对象已被废弃)
}
// 使用Object.prototype.toString.call(data)获取data的内部类型字符串
let name = Object.prototype.toString.call(data)
// 在typeObj中查找与data类型字符串相匹配的简化类型名称,如果未找到则默认为'未知类型'
let typeName = typeObj[name] || '未知类型'
// 返回typeName是否与传入的type参数相等,即判断data是否属于指定的类型
return typeName === type
}
// 测试函数isType
console.log(
isType({}, 'object'), // true,因为空对象{}的类型是'object'
isType([], 'array'), // true,因为空数组[]的类型是'array'
isType(new Date(), 'object'), // false,因为Date对象的类型是'date',不是'object'
isType(new Date(), 'date'), // true,因为Date对象的类型是'date'
)
3.2 将类数组转化为数组,以借用方法
数组,它的特征有:可以通过角标调用,如 array[0];具有长度属性length;可以通过 for 循环或forEach方法,进行遍历。
那么,类数组呢?顾名思义,就是具备与数组特征类似的对象。比如,下面的这个对象,就是一个类数组:let arrayLike = { 0: 1, 1: 2, 2: 3, length: 3},但是类数组无法使用 forEach、splice、push 等数组原型链上的方法
<div class="div1">1</div>
<div class="div1">2</div>
<div class="div1">3</div>
let div = document.getElementsByTagName('div');
console.log(div); // HTMLCollection(3) [div.div1, div.div1, div.div1] 里面包含length属性
let arr2 = Array.prototype.slice.call(div);
console.log(arr2); // 数组 [div.div1, div.div1, div.div1]
3.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
3.4 数组拼接,添加
let arr1 = [1,2,3];
let arr2 = [4,5,6];
//利用数组的concat方法:返回一个新的数组
let arr3 = arr1.concat(arr2);
console.log(arr3); // [1, 2, 3, 4, 5, 6]
console.log(arr1); // [1, 2, 3] 不变
console.log(arr2); // [4, 5, 6] 不变
// 用 apply方法
[].push.apply(arr1,arr2); // 给arr1添加arr2
console.log(arr1); // [1, 2, 3, 4, 5, 6]
console.log(arr2); // 不变
3.5 利用call和apply做继承
function Animal(name){
this.name = name;
this.showName = function(){
console.log(this.name);
}
}
function Cat(name){
Animal.call(this, name);// 使用this对象代替Animal对象,那么Cat中就有Animal的所有属性和方法,就能直接调用Animal的方法以及属性
}
var cat = new Cat("TONY");
cat.showName(); //TONY
3.6 保存函数参数
请问下面的这道经典面试题会输出什么?
for (var i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i)
}, i * 1000);
}
会依次输出:6 6 6 6 6,如何改进呢?下面使用了bind方法解决问题
for (var i = 1; i <= 5; i++) {
// 缓存参数
setTimeout(function (i) {
console.log('bind', i) // 依次输出:1 2 3 4 5
}.bind(null, i), i * 1000);
}
4、手写实现call、apply、bind
4.1 实现call
Function.prototype.myCall = function (context, ...arr) {
debugger
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; // 返回函数执行结果
};
function name(params) {
console.log(params)
}
function a() {
name.myCall(a,'333')
}
a();//333
4.2 实现apply
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; // 返回函数执行结果
};
function name(a,b,c) {
console.log(a,b,c)
}
function a() {
name.myApply(a,[2,3,4])
}
a();//2 3 4
4.3 实现bind
Function.prototype.myBind = function (objThis, ...params) {
const thisFn = this; // 存储源函数以及上方的params(函数参数)
// 对返回的函数 secondParams 二次传参
let fToBind = function (...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的指向并传递参数,返回执行结果
};
if (thisFn.prototype) {
// 复制源函数的prototype给fToBind 一些情况下函数没有prototype,比如箭头函数
fToBind.prototype = Object.create(thisFn.prototype);
}
return fToBind; // 返回拷贝的函数
};
function name(a,b,c) {
console.log(a,b,c)
}
var a = name.myBind(null,[4,4,4]);
a();//[4, 4, 4] undefined undefined