call,apply,bind三者区别以及实现原理
作用
在执行函数的时候,如果需要保证函数内部this不被污染或者说需要使函数内部this指向到指定对象的时候,都会按情况分别使用到call、apply、bind方法来实现需求
bind与call、apply的区别
bind不会立即调用,他会返回一个函数,函数内部的this指向与bind执行时的第一个参数,而传入bind的第二个及以后的参数作为原函数的参数来调用原函数。call和apply直接返回函数的返回值。
call和apply的区别
call和apply第一个参数,都是设置函数内this要指向的目标。call第二个参数以及后面的参数,是数组解构后,数组的元素。apply第二个参数是一个数组。
call的实现
Function.prototype.call = function(context) {
// 基础类型转包装类型
if(context === undefined || context === null) {
context = window
}else if(typeof context === 'string'){
context = new String(context)
}else if(typeof context === 'number'){
context = new Number(context)
}else if(typeof context === 'boolean'){
context = new Boolean(context)
}
// 将原函数绑定到目标对象的fn属性上
context.fn = this
// 获取第二参数以及之后参数
const args = [...arguments].slice(1)
const fnValue = context.fn(args)
// 解绑删除被绑定的函数
delete context.fn
// 返回结果
return fnValue
}
基础类型转包装类型目的:
context.fn = this,普通类型无法绑定属性,包装类型可以- 符合
ECMAScript规范
apply的实现
Function.prototype.apply = function(context,arr){
if(context === undefined || context === null) {
context = window
}else if(typeof context === 'string'){
context = new String(context)
}else if(typeof context === 'number'){
context = new Number(context)
}else if(typeof context === 'boolean'){
context = new Boolean(context)
}
if(typeof arr !== 'Object' || typeof arr !== undefined || typeof arr !== null ){
throw new TypeError('CreateListFromArrayLike called on non-object')
}
arr = Array.isarray(arr) && arr || []
context.fn = this
const fnValue = context.fn(...arr)
delete context.fn
return fnValue
}
bind的实现
Function.prototype.bind = function(context){
const ofn = this
const args = [...arguments].slice(1)
function O(){}
function fn(){
ofn.apply(this instanceof O ? this : context, args.concat(Array.from(arguments)))
}
//建立原型链继承关系
O.prototype = this.prototype
fn.prototype = new O()
return fn
}
this instanceof O ? this : context这句话的作用
区分普通调用和 new 调用
1. 普通调用(非 new)
const obj = { name: "Alice" };
function greet() {
console.log(this.name);
}
const boundGreet = greet.bind(obj);
boundGreet(); // 普通调用
在这种情况下:
this instanceof O返回false- 执行
ofn.apply(context, args) - this 指向 bind 时传入的 context(即
obj)
2. new 调用
function Person(name) {
this.name = name;
}
const BoundPerson = Person.bind({});
const instance = new BoundPerson("Bob"); // new 调用
在这种情况下:
this instanceof O返回true- 执行
ofn.apply(this, args) - this 指向新创建的实例对象(即
instance)
再详细一点
当调用 new fn() 时,this 指向谁?
答案:指向新创建的实例对象
执行过程分析:
-
new fn()执行时:- 创建一个新对象,其原型指向
fn.prototype - 将这个新对象作为
this上下文执行fn函数体
- 创建一个新对象,其原型指向
-
fn函数体内的判断:javascript
ofn.apply(this instanceof O ? this : context, ...)-
this instanceof O:检查this是否是O的实例 -
由于
fn.prototype = new O(),所以通过new fn()创建的实例:- 其
[[Prototype]]指向new O() new O()的[[Prototype]]指向O.prototype(即原函数的prototype)
- 其
-
因此
this instanceof O返回true
-
-
结果:
ofn.apply(this, ...)执行原函数,this指向新创建的实例- 保持了
new操作符的正常行为
现代实现方法
补充一段call的现代实现方法,更简洁
Function.prototype.call = function(context) {
// 使用 Object() 统一处理基础类型转换
context = context ? Object(context) : window
const fn = Symbol('fn') // 使用 Symbol 避免属性冲突
context[fn] = this
const result = context[fn](...arguments)
delete context[fn]
return result
}
call,apply,bind的应用场景
1. call 的应用场景
类数组对象转数组
// 将类数组对象转换为真正的数组
function example() {
const args = Array.prototype.call(arguments);
// 或更现代的写法:const args = Array.from(arguments);
return args;
}
继承中的构造函数调用
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name); // 继承父类属性
this.age = age;
}
const child = new Child('小明', 10);
调用对象的方法
const obj1 = {
name: 'Alice',
greet: function() {
console.log(`Hello, ${this.name}`);
}
};
const obj2 = {
name: 'Bob'
};
obj1.greet.call(obj2); // "Hello, Bob"
2. apply 的应用场景
数组参数传递
// 求数组最大值
const numbers = [5, 2, 8, 1, 9];
const max = Math.max.apply(null, numbers); // 9
// 数组合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2); // arr1 = [1,2,3,4,5,6]
函数柯里化
function add(a, b, c) {
return a + b + c;
}
function partialAdd(a) {
return function(b, c) {
return add.apply(this, [a, b, c]);
}
}
const add5 = partialAdd(5);
console.log(add5(3, 2)); // 10
3. bind 的应用场景
事件处理函数
class Button {
constructor() {
this.text = 'Click me';
this.handleClick = this.handleClick.bind(this); // 绑定this
}
handleClick() {
console.log(this.text); // 确保this指向Button实例
}
}
const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);
设置定时器
class Timer {
constructor() {
this.message = 'Timeout!';
}
start() {
// 使用bind确保setTimeout中的this正确
setTimeout(this.showMessage.bind(this), 1000);
}
showMessage() {
console.log(this.message);
}
}
预设参数
function multiply(a, b) {
return a * b;
}
// 创建固定第一个参数的函数
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
console.log(double(8)); // 16
// 创建固定多个参数的函数
const triplePlus = multiply.bind(null, 3, 4);
console.log(triplePlus()); // 12
实际项目中的综合应用
React 类组件中的方法绑定
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// 方法1:在构造函数中bind
this.handleClick = this.handleClick.bind(this);
}
// 方法2:使用箭头函数(自动绑定this)
handleClick = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.handleClick}>点击</button>;
}
}
函数借用
// 借用数组方法处理类数组对象
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
// 使用数组的forEach方法
Array.prototype.forEach.call(arrayLike, item => {
console.log(item); // a, b, c
});
// 使用数组的map方法
const result = Array.prototype.map.call(arrayLike, item => item.toUpperCase());
console.log(result); // ['A', 'B', 'C']
性能优化场景
// 在循环中避免重复创建函数
function processItems(items, processor) {
const boundProcessor = processor.bind(null, 'prefix');
for (let i = 0; i < items.length; i++) {
boundProcessor(items[i]); // 只绑定一次,重复使用
}
}