JavaScript 中的 call / apply / bind 全面解析 + Vue 实战指南
一、它们是什么?
call / apply / bind 都是 函数对象的方法,用于:
- 改变函数内部
this指向 - 控制函数执行方式
- 复用函数
- 控制执行时机
js
function show() {
console.log(this.name); // 打印当前 this 的 name 属性
}
const obj = { name: '张三' };
show(); // this = window/globalThis → 输出 undefined(严格模式下)
show.call(obj); // this = obj → 输出 '张三'
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: '张三' };
// 1️⃣ call:立即执行,参数逐个传入
greet.call(person, '你好', '!');
// this = person, 逐个传参
// 打印结果: "你好, 张三!"
// 2️⃣ apply:立即执行,参数数组传入
greet.apply(person, ['早上好', '你吃了吗']);
// this = person, 参数用数组
// 打印结果: "早上好, 张三你吃了吗"
// 3️⃣ bind:不执行,返回新函数
const boundGreet = greet.bind(person, '晚上好', '!!!');
// 返回一个新函数,但不执行
boundGreet();
// 执行后打印结果: "晚上好, 张三!!!"
二、区别总结
| 方法 | 是否立即执行 | 参数写法 | 典型用途 |
|---|---|---|---|
| call | ✔ 立即执行 | fn.call(obj, a, b) |
改 this、方法借用 |
| apply | ✔ 立即执行 | fn.apply(obj, [a, b]) |
改 this、数组参数 |
| bind | ❌ 不执行 | fn.bind(obj, a, b)(返回新函数) |
延迟执行、绑定 this |
三、为什么需要?
JavaScript 中 this 会丢失:
js
function show() {
console.log(this.name);
}
const obj = { name: '李四' };
// 直接调用
show(); // undefined
// 使用 call 改变 this
show.call(obj); // 李四
四、call ------ 逐个参数 + 立即执行
js
function sum(a, b) {
console.log(this.label, a + b); // 输出 this.label 与 a+b
}
const obj = { label: '结果:' };
// 使用 call 改变 this 并传参
sum.call(obj, 10, 20); //30
五、apply ------ 数组参数 + 立即执行
js
// 与 call 不同,参数必须传数组
sum.apply(obj, [10, 20]); //30
经典用途:
js
const arr = [5, 9, 1, 20];
// 求数组最大值
console.log(Math.max.apply(null, arr)); // 20
// 把 arguments 转数组
function test() {
const args = Array.prototype.slice.apply(arguments);
console.log(args);
}
test(1, 2, 3, 4); // [1,2,3,4]
六、bind ------ 不执行,返回新函数
js
function showMessage(msg) {
console.log(this.name, msg);
}
const obj = { name: '李四' };
// bind 不执行,而是返回新函数
const boundFn = showMessage.bind(obj, '你好!');
// 调用新函数
boundFn(); //李四 你好!
七、ES6 写法对比
1️⃣ apply → 扩展运算符
js
const arr = [5, 3, 9];
// 旧写法
console.log(Math.max.apply(null, arr)); // 9
// ES6 写法
console.log(Math.max(...arr)); // 9
2️⃣ arguments 转数组
js
function demo() {
// 旧写法
const arr1 = Array.prototype.slice.apply(arguments);
console.log('旧写法:', arr1); //旧写法: [1, 2, 3]
// ES6
const arr2 = [...arguments];
console.log('ES6:', arr2);//ES6: [1, 2, 3]
}
demo(1,2,3);
3️⃣ bind → 箭头函数固定 this
js
const obj2 = { name: '小明' };
// 箭头函数 this = 定义时作用域
setTimeout(() => console.log(obj2.name), 1000); //1s后打印小明
4️⃣ bind 柯里化 vs ES6
旧:
js
function add(a,b){ return a+b }
const add5 = add.bind(null, 5)
console.log(add5(10)) //15
ES6:
js
const add = a => b => a + b
console.log(add(5)(10)) //15
5️⃣ call → 可用对象方法替代
js
const obj3 = {
say() { console.log('Hi') }
}
obj3.say?.() //Hi
八、手写 call / apply / bind(完整版)
8.1 手写 call
js
Function.prototype.myCall = function (context, ...args) {
context = context ?? globalThis // null/undefined 默认 globalThis
context = Object(context) // 原始值自动装箱
const fnKey = Symbol('fn') // 避免覆盖属性
context[fnKey] = this // this = 当前函数
const result = context[fnKey](...args) // 执行函数
delete context[fnKey] // 删除临时属性
return result
}
// 测试
function say(a, b) {
console.log(this.name, a + b)
}
const obj = { name: '张三' }
say.myCall(obj, 1, 2) // 张三 3
8.2 手写 apply
js
Function.prototype.myApply = function (context, args) {
context = context ?? globalThis
context = Object(context)
const fnKey = Symbol('fn')
context[fnKey] = this
const result = args ? context[fnKey](...args) : context[fnKey]()
delete context[fnKey]
return result
}
// 测试
function sum(a, b) {
console.log(this.label, a + b)
}
const obj2 = { label:'结果:' }
sum.myApply(obj2, [10, 20])//结果: 30
8.3 手写 bind(基础版)
js
Function.prototype.myBind = function (context, ...presetArgs) {
const self = this
return function (...laterArgs) {
return self.apply(context, [...presetArgs, ...laterArgs])
}
}
// 测试
function show(a,b) {
console.log(this.name, a, b)
}
const obj3 = { name: '李四' }
const fn = show.myBind(obj3, 1)
fn(2) //李四 1 2
8.4 bind(高级版 --- 支持 new)
js
Function.prototype.myBind = function(context, ...presetArgs) {
const self = this
function boundFn(...laterArgs) {
if (this instanceof boundFn) { // new 优先级
return new self(...presetArgs, ...laterArgs)
}
return self.apply(context, [...presetArgs, ...laterArgs])
}
boundFn.prototype = Object.create(self.prototype) // 保留原型链
return boundFn
}
// 测试 new 场景
function Person(name){
this.name = name
}
const BindPerson = Person.myBind({ name:'AAA' })
const p = new BindPerson('张三')
console.log(p.name) // 张三
console.log(p instanceof Person) // true
console.log(p instanceof BindPerson) // true
九、Vue 实战部分(详细版)
9.1 回调中 this 丢失 → bind
js
export default {
data() {
return { count: 0 }
},
methods: {
handle() {
console.log('count:', this.count)
this.count++
},
test() {
setTimeout(this.handle.bind(this), 1000) //1秒后打印count: 0
// 不 bind 的话 this = window,count 会报错
}
}
}
9.2 防抖/节流绑定 this
js
methods: {
search() {
console.log('搜索关键词:', this.keyword)
},
debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
},
created() {
this.searchDebounced = this.debounce(this.search, 500).bind(this)
}
9.3 父组件调用子组件方法 → call/apply
js
// 父组件
this.$refs.child.say.call(this.$refs.child, 1,2)
this.$refs.child.say.apply(this.$refs.child, [1,2])
9.4 Vue3 setup
js
import { reactive } from 'vue'
function useLog() {
function log() {
console.log(this.msg)
}
return log
}
export default {
setup() {
const state = reactive({ msg: 'hello' })
const log = useLog().bind(state)
log() // hello
}
}
9.5 处理 $refs 类数组 → apply/ES6
js
mounted() {
const arr = Array.prototype.slice.apply(this.$refs.item)
console.log(Array.isArray(arr)) // true
const arr2 = [...this.$refs.item]
console.log(Array.isArray(arr2)) // true
}
9.6 事件监听解绑必须 bind
js
mounted() {
this.onScroll = this.handleScroll.bind(this)
window.addEventListener('scroll', this.onScroll)
},
beforeDestroy() {
window.removeEventListener('scroll', this.onScroll)
},
methods:{
handleScroll() {
console.log('滚动事件', this)
}
}
十、总结
✔ call → 改 this + 立即执行 + 逐个参数
✔ apply → 改 this + 立即执行 + 数组参数
✔ bind → 不执行 + 返回新函数(Vue 中最常用)
✔ ES6 可替代部分场景 → ... / Array.from / 箭头函数 / 柯里化
✔ 框架源码 & 高阶 JS 场景仍离不开原生 call/apply/bind