本文深入解析JavaScript中this关键字的四种指向规则,并通过50行核心代码逐步手写实现原生方法call、apply和bind。文章详细阐述了利用Symbol创建临时属性劫持this的机制,对比了三种方法在参数传递与返回值上的差异,并提供了完整的bind高阶实现,帮助开发者彻底掌握函数绑定的底层原理。
JavaScript中this指向的核心原理
在尝试手写实现call、apply和bind之前,有必要先回顾一下JavaScript中this关键字的工作机制。
我们可以将this形象地理解为一个指向当前执行上下文的指针。
理解this的核心原则只有一条:它始终指向最后调用该函数的对象,这是掌握其行为的关键所在。
通常情况下,this的指向分为以下四种场景:
当函数以普通方式直接调用时,在非严格模式下this指向全局对象window;若处于严格模式('use strict'),则this的值为undefined。
function test() {
console.log(this);
};
test();
指向window 输出下面的代码:
// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage, sessionStorage: Storage, webkitStorageInfo: DeprecatedStorageInfo…}
严格模式
'use strict';
function test() {
console.log(this);
};
test();
// undefined
当函数作为对象的方法被调用,即采用obj.fn()的形式时,this指向该对象obj。
var obj = {
name: 'segmentFault',
foo: function() {
console.log(this.name);
}
}
obj.foo();
// 'segmentFault'
这种调用方式也可以通过变量引用实现,例如将函数赋值给对象属性后通过该属性调用。
function test() {
console.log(this.name);
}
var obj = {
name: 'qiutc',
foo: test
}
obj.foo();
// 'qiutc'
当使用call或apply方法时,this的指向会被强制改变为指定的第一个参数。
function a(a,b,c) {
console.log(this.name);
console.log(a,b,c)
}
const b = {
name: "segmentFault"
}
a.call(b,1,2,3)
//输出 segmentFault和 1,2,3
function a(a,b,c) {
console.log(this.name);
console.log(a,b,c)
}
a.apply(b,[1,2,3])
//输出segmentFault和1,2,3
而bind方法的行为则有所不同:
function a() {
console.log(this.name);
}
const b = {
name: "segmentFault"
}
a.bind(b, 1, 2, 3)
此时控制台不会立即输出任何结果,因为bind并不会直接执行原函数,而是返回一个新的函数。这个新函数的this指向被绑定为bind的第一个参数。
function a() {
console.log(this.name);
}
const b = {
name: "segmentFault"
}
const c = a.bind(b, 1, 2, 3)
c()
//此时输出segmentFault
手写实现call与apply方法
接下来,我们开始手写实现call方法。
首先,我们在Function.prototype上定义一个名为myCall的方法。
Function.prototype.myCall = function (context, ...arg) {
const fn = Symbol('临时属性')
context[fn] = this
context[fn](...arg)
delete context[fn]
}
短短四行代码即可实现call的基本功能,其核心思路如下:
- 通过将函数作为对象的属性进行调用,利用对象属性访问机制使函数内部的this指向该对象。
- 每次调用时,动态生成一个唯一的Symbol属性名,调用完成后立即删除该属性,以保持对象干净。
- 这个Symbol属性名对应的值就是当前正在调用myCall的原函数。
- 在函数形参中使用...arg(剩余参数语法)可以将所有传入的参数收集到一个数组中,在函数内部通过该变量即可访问所有参数。
- 在调用context[fn](...arg)时,...arg用于展开数组,将数组元素作为独立参数依次传入函数。
为了清晰展示原理,本文暂不进行类型判断和边界错误处理,仅聚焦于核心逻辑的讲解。
随后,我们来实现apply方法。
同样在Function.prototype上定义myApply方法。
//实现自己的myApply
Function.prototype.myApply = function (context, arg) {
const fn = Symbol('临时属性')
context[fn] = this
context[fn](...arg)
delete context[fn]
}
const obj2 = {
a: 1
}
test.myApply(obj2, [2, 3, 4])
其实现逻辑与call类似,区别在于apply接收的第二个参数是一个数组。因此,在调用时只需使用...操作符将该数组展开即可传入函数。
深入解析与实现bind方法
接下来处理bind方法的实现。
bind与call、apply的本质区别在于:bind不会立即改变原函数的this指向并调用,而是返回一个新函数(该函数内部已绑定指定的this),且不会执行原函数;而call和apply会直接改变this并立即调用原函数。
bind在编写框架源码(如Koa等)时应用广泛。
//实现自己的myBind
Function.prototype.myBind = function (context, ...firstarg) {
const that = this
const bindFn = function (...secoundarg) {
return that.myCall(context, ...firstarg, ...secoundarg)
}
bindFn.prototype = Object.create(that.prototype)
return bindFn
}
var fnbind = test.myBind(obj, 2)
fnbind(3)
同理,我们在原型上定义myBind方法。
这里需要保存最初调用myBind时的函数上下文(this劫持)。
返回一个新的函数,该新函数内部通过调用我们实现的myCall方法,确保this指向已确定,并返回新函数。
学习应循序渐进,建议按照本文顺序自行封装一遍。当然,完整的bind实现还需判断是否通过new关键字调用,以处理原型链继承的情况。
以下是完整版本的bind实现代码。
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; // 返回拷贝的函数
};
以上就是本文的全部内容,希望这些解析能对您的学习有所帮助,也欢迎大家继续支持本站。