前端面试系列-JS篇-手写 apply、call和bind
引言
在 JavaScript 的世界里,有三位神奇的函数:apply、call和bind
。它们是 Function
对象的方法,用于改变函数的执行上下文。
本文将详细介绍它们的定义、实现、区别、应用场景等方面内容。
定义
apply、call和bind
都是函数对象的方法,它们的主要目的是改变函数的 this
指向 。在 JavaScript 中,this
的值是在函数调用时确定的,而不是在函数定义时。这三个方法允许我们在调用函数时,手动指定 this
的值。
apply
apply
方法接受两个参数:一个是要绑定的 this
值,另一个是参数数组。它会立即执行函数,并返回执行结果。
function.apply(thisArg, [argsArray])
call
call
方法与 apply
类似,但它接受的是一个参数列表,而不是一个参数数组。它同样会立即执行函数,并返回执行结果。
function.call(thisArg, arg1, arg2, ...)
bind
bind
方法接受一个要绑定的this值和一系列参数,然后返回一个新的函数。新函数在调用时,会使用指定的 this
值和参数。
function.bind(thisArg, arg1, arg2, ...)
区别
参数传递方式
apply和call
的主要区别在于参数传递方式。apply
接受一个参数数组,而 call
接受多个单独的参数。
返回值
apply和call
在执行后会立即返回函数执行的结果;而 bind
在执行后会返回一个新的函数,需要再次调用才能得到函数执行的结果。
是否立即执行
apply和call
在改变 this
指向的同时,会立即执行函数。而 bind
改变 this
指向后,会返回一个新函数,需要手动调用才会执行。
应用场景
改变函数内部的this指向
function greet() {
console.log(`Hello, my name is ${this.name}`);
}
const person = {
name: 'Alice',
};
greet.call(person); // 输出:Hello, my name is Alice
借用其他对象的方法
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
const student = new Student('Bob', 18, 'Grade 12');
console.log(student); // 输出:Student {name: "Bob", age: 18, grade: "Grade 12"}
函数柯里化
function add(a, b) {
return a + b;
}
const add5 = add.bind(null, 5);
console.log(add5(3)); // 输出:8
实现
apply和call
apply和call
的原理非常相似,它们都是通过改变函数内部 this
的指向来实现调用对象的更改。在 JavaScript 中,函数内部的 this
指向是在调用时确定的,而 apply和call
正是通过显式地指定 this
来改变调用对象。
bind
bind
的原理稍微复杂一些。它会创建一个新函数,这个新函数在被调用时,会将 bind
的第一个参数作为 this
,并携带其他参数调用原函数。这是通过闭包实现的,它让新函数持有原函数的引用和传入的 thisArg
。
让我们动动手指头,亲手实现一下 apply、call 和 bind
。
// 手写实现 apply
Function.prototype.myApply = function(context, args) {
// 检查调用 myApply 方法的 this 是否为函数
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
// 如果没有传入上下文对象,则默认为全局对象。
context = context || window;
// 定义一个唯一的 Symbol 属性名,避免与上下文对象中原有的属性名冲突
const fn = Symbol('fn');
// 将当前函数保存到上下文对象的 fn 属性中
context[fn] = this;
// 使用展开语法将 args 数组的元素作为参数,调用保存在上下文对象中的函数
const result = context[fn](...args);
// 删除上下文对象中保存的函数属性
delete context[fn];
// 返回函数调用的结果
return result;
};
// 手写实现 call
Function.prototype.myCall = function(context, ...args) {
// 检查调用 myCall 方法的 this 是否为函数
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
// 如果没有传入上下文对象,则默认为全局对象。
context = context || window;
// 定义一个唯一的 Symbol 属性名,避免与上下文对象中原有的属性名冲突
const fn = Symbol('fn');
// 将当前函数保存到上下文对象的 fn 属性中
context[fn] = this;
// 使用展开语法将 args 的元素作为参数,调用保存在上下文对象中的函数
const result = context[fn](...args);
// 删除上下文对象中保存的函数属性
delete context[fn];
// 返回函数调用的结果
return result;
};
以上是这两个方法的作用是模拟实现 call和apply
方法的调用过程,因为基本一样所以合并起来讲。
在函数中,首先检查调用方法的 this
是否为函数,如果不是函数则抛出类型错误。然后,它将传入的上下文对象赋值给一个变量,如果没有传入上下文对象,则默认使用全局对象。接着,它使用 Symbol
定义一个唯一的属性名,避免与上下文对象中原有的属性名冲突。它将当前函数保存到上下文对象的 fn
属性中,并使用展开语法将 args
的元素作为参数,调用保存在上下文对象中的函数。最后,它删除上下文对象中保存的函数属性,并返回函数调用的结果。
// 手写实现 bind
Function.prototype.myBind = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
const self = this;
return function F(...newArgs) {
if (this instanceof F) {
return new self(...args, ...newArgs);
}
return self.apply(context, args.concat(newArgs));
};
};
在函数中,首先检查调用 myBind
方法的 this
是否为函数,如果不是函数则抛出类型错误。然后,它将当前函数保存到一个变量中。接着,它返回一个新的函数,在新函数中根据情况绑定 this
的值并调用原函数。如果通过 new
操作符调用该函数,则将 this
绑定到新创建的实例对象上,并将绑定时的参数和调用时的新参数一起传递给原函数进行调用。否则,将原函数绑定到指定的上下文对象上,并将绑定时的参数和调用时的新参数一起传递给原函数进行调用。
可能有关于 new 操作符调用函数不好理解,没关系,我们看一下下面的这个例子
function Person(name, age) {
this.name = name;
this.age = age;
}
const obj = {};
const bindPerson = Person.bind(obj, 'Alice');
// 使用 new 操作符调用绑定后的函数
const alice = new bindPerson(25);
console.log(alice); // Person { name: 'Alice', age: 25 }
console.log(alice instanceof Person); // true
console.log(alice instanceof bindPerson); // false
在这个例子中,我们定义了一个 Person
构造函数,它接收两个参数 name
和 age
,并将它们分别赋值给新创建的对象的 name
和 age
属性。然后,我们创建了一个空对象 obj
,并使用 bind
方法将 Person
函数绑定到 obj
对象上,并传入一个参数 'Alice'
。这个绑定后的函数被赋值给了 bindPerson
变量。
接着,我们使用 new
操作符调用 bindPerson
,并传入一个参数 25
,这个参数将作为 Person
构造函数的第二个参数 age
。在这个调用过程中,bindPerson
函数内部的 this
被绑定到新创建的实例对象上,同时绑定时的参数 'Alice'
也被传递给了 Person
构造函数。最终,我们得到了一个新的 Person
实例对象 alice
,它的 name
属性为 'Alice'
,age
属性为 25
。
需要注意的是,虽然 alice
是通过 bindPerson
函数创建的,但它的原型链仍然指向 Person.prototype
,因为它是通过 new
操作符调用 Person
构造函数创建的实例对象。同时,alice
不是 bindPerson
的实例,因为 bindPerson
函数没有定义 constructor
属性,而是继承自 Function.prototype
。
测试
最后,让我们使用一个简单的例子来测试一下这些函数:
function greeting() {
return `${this.name} 说:${Array.prototype.slice.apply(arguments).join(' ')}`;
}
const alice = { name: 'Alice' };
const bob = { name: 'Bob' };
// 使用 apply
console.log(greeting.myApply(alice, ['Hi', '大家好'])); // Alice 说:Hi 大家好
// 使用 call
console.log(greeting.myCall(bob, 'Hello', '世界')); // Bob 说:Hello 世界
// 使用 bind
const greetingFromAlice = greeting.myBind(alice, '你好');
console.log(greetingFromAlice('朋友们')); // Alice 说:你好 朋友们
注意事项
在使用 apply、call和bind
时,需要注意以下几点:
-
如果传入的
this
值为null或undefined
,函数内部的this
将指向全局对象(在浏览器中为window
,在Node.js
中为global
) -
如果传入的
this
值为原始值(如数字、字符串或布尔值),函数内部的this
将指向该原始值的包装对象(如Number、String或Boolean
) -
非严格模式下,如果传入的
this
值为null
或undefined
,函数内部的this
将指向全局对象。在严格模式下,函数内部的this
将保持为null或undefined
-
使用
bind
时,需要注意内存泄漏问题。如果绑定的函数被长时间引用,可能导致内存泄漏。在不需要时,应该解除引用,以便垃圾回收
总结
apply、call和bind
是 JavaScript 中非常实用的三个函数。它们可以帮助我们改变函数内部的 this
指向,提高代码的复用性和灵活性。
可能你平时不会经常用到这三个方法,但在一些场景下它们将能为你解决很多问题,让我们在 JavaScript 的征途上更加游刃有余。此外,apply、call 和 bind
的概念也能帮助你更好地理解 JavaScript 中的函数式编程、柯里化等概念。跳出 JavaScript,你还会发现类似的方法和思想应用于其他编程语言中。
最后,祝大家变得更强!
转载自:https://juejin.cn/post/7241080533169635386