目录
二、详解 call()、apply() 和 bind() 方法
三、call()、apply() 和 bind() 方法的核心理念
四、手写 call()、apply() 和 bind() 方法(中高级前端面试题)★★★★★
五、call()、apply() 和 bind() 方法的实际应用
1、如何选用 call()、apply() 和 bind() 方法?
(1)、借用 Object.prototype.toString() 方法判断数据类型
一、概述
- call()、apply() 和 bind() 方法都是用来改变函数执行时的上下文(this),可借助它们实现继承。
- 调用 call()、apply() 和 bind() 方法 的必须是个函数。
- apply() 有两个参数,第一个参数是对象,第二个参数是数组,fn.apply(thisobj, arg),此处 arg 是一个数组。用来改变函数的执行环境。
- call() 有 N 个参数,第一个参数都是对象,call 参数将依次传递给借用的方法作为参数,即 fn.call(thisobj, arg1, arg2)。用途和 apply() 完全一样,用来改变函数的执行环境。
- bind() 会返回执行上下文被改变的函数,而不会立即执行,前两者时立即执行,参数和 call() 相同。
二、详解 call()、apply() 和 bind() 方法
1、apply() 方法
apply() 方法接收2个参数:运行函数的作用域(也叫上下文执行环境) 和 一个参数数组。
apply() 方法,除了可以用来改变函数的作用域,还可以用于将数组转为函数的参数。
function sum(num1, num2){
return num1 + num2;
}
function fn1(num1, num2){
return sum.apply(this, arguments);
}
function fn2(num1, num2){
return sum.apply(this, [num1, num2]);
}
console.log(fn1(10, 10)); // 20
console.log(fn2(10, 10)); // 20
由于arguments是类数组,所以arguments也可以这样。
2、call() 方法
call() 和 apply() 方法唯一区别是参数不一样,call() 方法是 apply() 方法的语法糖。
call() 方法也接收2个参数:运行函数的作用域(也叫上下文执行环境) 和 一个参数列表
function sum(num1, num2){
return num1 + num2;
}
function fn(num1, num2){
return sum.call(this, num1, num2);
}
console.log(fn(10, 10)); // 20
call() 方法的另一种使用途径——将对象转为数组:
// 将对象转为数组
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
3、bind() 方法
bind() 方法与 call() 和 apply() 方法都可以改变上下文执行环境,不同的是,bind() 方法返回一个新函数,只有等这个新函数被调用才可以改变函数执行时的上下文(this),而 apply() 和 call() 方法是立即调用,立即就能改变 this 指向的。
bind() 方法接收2个参数:运行函数的作用域(也叫上下文执行环境) 和 一个参数列表
浏览器 IE8 及其以下版本不支持 bind() 方法。
var color = "red";
var obj = {
color: "blue"
}
function fn(){
console.log(this.color); // blue
}
var objColor = fn.bind(obj);
objColor();
三、call()、apply() 和 bind() 方法的核心理念
“A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?最好的方案当然是借用 A 对象的方法啦,既达到了目的,又节省了内存。”——这就是call()、apply() 和 bind() 方法的核心理念——借用方法。
四、手写 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; // 返回函数执行结果
};
2、手写 apply() 方法
- 传递给函数的参数处理,不太一样,其他部分跟call一样。
- apply() 方法接受第二个参数为类数组对象, 这里用了 JavaScrip t权威指南中判断是否为类数组对象的方法。
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() 方法
- 拷贝源函数:
- 通过变量储存源函数
- 使用 Object.create() 方法复制源函数的 prototype 属性给 fToBind
- 返回拷贝的函数
- 调用拷贝的函数:
- 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; // 返回拷贝的函数
};
五、call()、apply() 和 bind() 方法的实际应用
1、如何选用 call()、apply() 和 bind() 方法?
- 如果不需要关心具体有多少参数被传入函数,选用apply()。
- 如果确定函数可接收多少个参数,并且想一目了然表达形参和实参的对应关系,用call()。
- 如果我们想要将来再调用方法,不需立即得到函数返回结果,则使用bind()。
2、应用实例
(1)、借用 Object.prototype.toString() 方法判断数据类型
用 Object.prototype.toString() 方法来判断类型再合适不过,借用这个方法,我们几乎可以判断所有类型的数据:
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)、借用数组的方法,操作类数组
类数组因为不是真正的数组所有没有数组类型上自带的种种方法,所以我们需要去借用数组的方法。
比如借用数组的 push() 方法:
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() 方法获取数组的最大值和最小值
apply() 方法直接传递数组做要调用方法的参数,也省一步展开数组。
比如使用 Math.max() 和 Math.min() 方法来获取数组的最大值和最小值:
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
(4)、继承
ES5的继承,都是通过借用父类的构造函数,来实现父类的方法和属性的:
// 父类
function supFather(name) {
this.name = name;
this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
// 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
supFather.call(this, name);
this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
console.log(this.age, 'foo');
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}
(5)、保存函数参数(变量)
首先来看下一道经典的面试题:
for (var i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i); // 依次输出:6 6 6 6 6
}, 1000);
}
造成这个现象的原因是等到 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
}, 1000);
}(i));
}
②、bind() 方法保存变量
for (var i = 1; i <= 5; i++) {
// 缓存参数
setTimeout(function (i) {
console.log('bind', i) // 依次输出:1 2 3 4 5
}.bind(null, i), 1000);
}
③、用 let 声明变量,形成块级作用域,来间接达到保存变量的目的
for (let i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i); // 依次输出:1 2 3 4 5
}, 1000);
}
(6)、回调函数 this 丢失问题
问题再现:
class Page {
constructor(callBack) {
this.className = 'Page'
this.MessageCallBack = callBack //
this.MessageCallBack('发给注册页面的信息') // 执行PageA的回调函数
}
}
class PageA {
constructor() {
this.className = 'PageA'
this.pageClass = new Page(this.handleMessage) // 注册页面 传递回调函数 问题在这里
}
// 与页面通信回调
handleMessage(msg) {
console.log('处理通信', this.className, msg) // 'Page' this指向错误
}
}
new PageA()
上述代码中,this.pageClass = new Page(this.handleMessage) 这里 this 的指向发生了改变,怎么保持 this 指向一致呢?
两种方案:
①、 bind() 方法:
this.pageClass = new Page(this.handleMessage.bind(this)); // 绑定回调函数的this指向
②、 箭头函数:
this.pageClass = new Page(() => this.handleMessage()); // 箭头函数绑定this指向