【前端面试小册】JS-第21节:实现 call、apply、bind
一、call
1.1 概览
语法:func.call(context, arg1, arg2, ...)
作用:
- 接受多个参数,第一个参数
context,让调用函数的this指向context - 其余参数传递给函数调用
1.2 call 使用
使用 call
let obj = {
name: '愚公上岸说'
}
function demo(age) {
console.log(this.name); // 愚公上岸说
console.log(age); // 22
}
demo.call(obj, 22);
未使用 call
var name = '巴菲特'
function demo(age) {
console.log(this.name); // 巴菲特
console.log(age); // 22
}
demo(22)
// 没使用 call,this 默认指向 window
// this.name 等价于 window.name
// 注意 var 声明的全局变量才会挂在 window 上
// let 声明挂在 script 上
1.3 call 实现
实现思路
- 改变调用 call 的目标函数的 this 指向,并将其指向 call 的第一个参数
- 执行调用 call 的目标函数
- 删除临时属性,返回函数执行结果
实现代码
Function.prototype.myCall = function (context, ...args) {
// 处理 context 为 null 或 undefined 的情况
if (context === null || context === undefined) {
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为 window)
context = window;
} else {
// 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
context = Object(context);
}
// 利用 Symbol 唯一性,防止参数被覆盖
const temp = Symbol('temp');
// 函数的 this 指向隐式绑定到 context 上
context[temp] = this;
// 传参并执行函数
let result = context[temp](...args);
// 删除临时属性
delete context[temp];
return result;
};
关键点解析
1. context 处理:
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context);
}
原因:
null和undefined需要指向全局对象- 原始值需要转换为对象(如
Object(1)返回Number对象)
2. Symbol 的使用:
const temp = Symbol('temp');
context[temp] = this;
原因:
- 使用
Symbol确保属性名唯一,不会覆盖context上的原有属性 - 避免与
context上的属性名冲突
3. 执行函数:
let result = context[temp](...args);
原理:
- 将函数作为
context的方法调用 - 这样函数内部的
this就指向context
二、apply
2.1 概览
语法:func.apply(context, [argsArray])
作用:
- 第一个参数
context,让调用函数的this指向context - 第二个参数:数组或者类数组
注意:与 call 的参数类型区别,apply 的第二个参数是数组或类数组。
2.2 apply 使用
使用 apply
let obj = {
name: '愚公上岸说'
}
function demo(a, b) {
console.log(this.name); // 愚公上岸说
console.log(a, b); // 1, 2
}
demo.apply(obj, [1, 2]);
未使用 apply
let obj = {
name: '愚公上岸说'
}
var name = '巴菲特'
function demo(a, b) {
console.log(this.name); // 巴菲特
console.log(a, b); // 1, 2
}
demo.apply(null, [1, 2]);
// 没使用 apply,this 默认指向 window
// this.name 等价于 window.name
// 注意 var 声明的全局变量才会挂在 window 上
// let 声明挂在 script 上
// 注意是在浏览器环境
2.3 类数组复习
类数组的特点:
- 拥有
length属性 - 其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理)
- 不具有数组所具有的方法
类数组判断函数:
function isLikeArray(o) {
if (typeof o === 'object' &&
isFinite(o.length) &&
o.length >= 0 &&
o.length < 4294967296) {
// 4294967296: 2^32
// length 属性的值是一个 0 到 2^32-1 的整数
return true;
}
return false;
}
2.4 apply 完整实现
// 类数组判断
function isLikeArray(o) {
if (typeof o === 'object' &&
isFinite(o.length) &&
o.length >= 0 &&
o.length < 4294967296) {
return true;
}
return false;
}
// 实现
Function.prototype.myApply = function (context, args) {
// 处理 context
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context);
}
// 利用 Symbol 唯一性,防止参数被覆盖
const temp = Symbol('temp');
// 函数的 this 指向隐式绑定到 context 上
context[temp] = this;
let result;
if (args) {
// 判断参数是否数组或类数组
if (!Array.isArray(args) && !isLikeArray(args)) {
throw new TypeError('第二个参数必须是: 数组或类数组');
} else {
args = Array.from(args); // 转为数组
result = context[temp](...args); // 执行函数并展开数组,传递函数参数
}
} else {
result = context[temp](); // 执行函数
}
delete context[temp];
return result;
};
测试用例
function demo(a, b) {
console.log(this.name); // 愚公上岸说
console.log(a, b); // 1, 2
}
let obj = {
name: '愚公上岸说'
}
demo.myApply(obj, [1, 2]);
三、bind
3.1 概览
语法:fn.bind(context, arg1, arg2, ...)
作用:
bind()方法创建一个新的函数- 在
bind()被调用时,这个新函数的this被指定为bind()的第一个参数 - 而其余参数将作为新函数的参数,供调用时使用
3.2 bind 使用
function demo(a, b, c) {
console.log(this.name); // 愚公上岸说
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
}
let obj = {
name: '愚公上岸说'
}
let fn = demo.bind(obj, 1, 2);
fn(3);
3.3 bind 实现
Function.prototype.myBind = function (context, ...args) {
// 存储源函数以及上方的 args(函数参数)
const _this = this;
// 对返回的函数 args1 二次传参
let fn = function (...args1) {
// this 是否是 fn 的实例,判断返回的 fn 是否通过 new 调用
const isUseByNew = this instanceof fn;
// fn 作为构造函数调用时,_this 指向 fn 的实例,而 fn 的 this 也指向 fn 实例,所以 _this 指向 this 即可
// fn 作为普通函数调用时,_this 指向 context,这里 Object(context) 原因同上面 apply call
const _context = isUseByNew ? this : Object(context);
// 用 call 调用源函数绑定 this 的指向并传递参数,返回执行结果
return _this.call(_context, ...args, ...args1);
};
// 复制源函数的 prototype 给 fn
// 一些情况下函数没有 prototype,比如箭头函数
if (_this.prototype) {
fn.prototype = Object.create(_this.prototype);
}
return fn;
};
3.4 关键点解析
3.4.1 new 调用的处理
const isUseByNew = this instanceof fn;
const _context = isUseByNew ? this : Object(context);
原因:
- 当使用
new调用时,this指向新创建的实例 - 此时应该使用实例作为
this,而不是context
3.4.2 参数合并
return _this.call(_context, ...args, ...args1);
原理:
args是bind时传入的参数args1是调用返回函数时传入的参数- 使用展开运算符合并参数
3.4.3 prototype 复制
if (_this.prototype) {
fn.prototype = Object.create(_this.prototype);
}
原因:
- 保持原型链关系
- 使用
Object.create避免直接引用
3.5 测试用例
Demo 1:new 调用
function demo(a, b, c) {
console.log(this.name); // 成都
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
}
let obj = {
name: '愚公上岸说'
}
const fn = demo.myBind(obj, 1, 2);
fn.prototype.name = '成都';
new fn(3);
Demo 2:普通调用
function demo(a, b, c) {
console.log(this.name); // 愚公上岸说
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
}
let obj = {
name: '愚公上岸说'
}
const fn = demo.myBind(obj, 1, 2);
fn(3);
四、总结:call、apply、bind 区别
4.1 call 与 apply
唯一区别:传参写法不同
- call:从第 2 个以后的参数,都是传给 call 的调用函数
- apply:传给它的调用函数的参数就是第二个参数(数组或类数组)
func.call(context, arg1, arg2, arg3);
func.apply(context, [arg1, arg2, arg3]);
4.2 call、apply、bind 区别
| 特性 | call | apply | bind |
|---|---|---|---|
| 执行时机 | 立即执行 | 立即执行 | 返回新函数 |
| 参数形式 | 参数列表 | 数组或类数组 | 参数列表 |
| 返回值 | 函数执行结果 | 函数执行结果 | 新函数 |
核心区别:
- call、apply:改变调用函数的
this指向后,会立马执行他的调用函数 - bind:改变
this指向,生成一个新的函数,而不会立马执行他的调用函数
4.3 使用场景
call 的使用场景
// 1. 类数组转数组
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
const arr = Array.prototype.slice.call(arrayLike);
// 2. 获取数组最大值
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.call(null, ...numbers);
apply 的使用场景
// 1. 数组合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
// 2. 获取数组最大值
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
bind 的使用场景
// 1. 函数柯里化
function add(a, b) {
return a + b;
}
const add5 = add.bind(null, 5);
console.log(add5(3)); // 8
// 2. 事件处理
class Button {
constructor() {
this.clickCount = 0;
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.clickCount++;
}
}
五、面试要点总结
核心知识点
- call:立即执行,参数列表形式
- apply:立即执行,数组或类数组形式
- bind:返回新函数,不立即执行
- 实现原理:通过将函数作为对象方法调用来改变
this指向
常见面试题
Q1: call、apply、bind 的区别?
答:
call和apply立即执行,区别在于参数形式(列表 vs 数组)bind返回新函数,不立即执行- 都用于改变函数的
this指向
Q2: 如何实现 call?
答:将函数作为 context 的方法调用,使用 Symbol 避免属性冲突,执行后删除临时属性。
Q3: bind 如何处理 new 调用?
答:判断是否使用 new 调用,如果是,使用实例作为 this;否则使用 context。
实战建议
- ✅ 理解三者的区别和使用场景
- ✅ 掌握实现原理,能够手写实现
- ✅ 注意
new调用时的处理 - ✅ 理解
Symbol在实现中的作用
前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!
查看12道真题和解析
