浅显的理解 this 指向
在 JavaScript 中,this 的指向是一个较为复杂但关键的概念,它与函数的执行上下文密切相关,决定了函数内部 this 所引用的对象。通过以下示例代码可以观察不同调用方式下 this 的指向情况:
function self() {
console.log('self-this', this);
}
const selfObj = {
self: self,
name: "selfObj"
}
selfObj.deepSelf = {
self: self,
name: "deepSelf"
}
self(); // 输出: self-this Window
selfObj.self(); // self-this {name: 'selfObj', deepSelf: {…}, self: ƒ}
selfObj.deepSelf.self(); // self-this {name: 'deepSelf', self: ƒ}
对上面示例代码输出描述:
- 在独立函数调用时,非严格模式下
this默认绑定到全局对象(浏览器中为window,Node.js 中为global),在严格模式下会绑定为undefined; - 函数作为对象的方法被调用时,
this指向调用该方法的对象 - 在对象嵌套结构中作为内层对象的方法调用时,this 指向对应的内层对象
从上述代码运行结果可知,普通函数的 this 指向取决于其执行时的上下文。基于此,若要改变函数的 this 指向,我们可以采用让目标对象新增属性并赋值为当前函数,再通过该对象去调用函数的方式来实现,这也为我们手写 call、apply 和 bind 方法提供了思路。
由于这三个方法是供函数调用以改变 this 指向的,所以我们需要将它们挂载到 Function.prototype 上,这样所有的函数都能够使用这些方法。
以下是查看在自定义方法内如何获取当前函数的示例代码:
Function.prototype.likeCall=function(){
console.log("likeCall-this", this);
}
function selfCall() {}
selfCall.likeCall(); // likeCall-this ƒ selfCall() {}
在具体函数调用 likeCall 方法时,likeCall 方法内部的 this 就是当前函数
手写 call 的实现
call方法允许我们显式地指定函数内部this的指向,并传入相应的参数来调用函数,参数是以逗号分隔逐个传递的(参数平铺的形式)。以下是call方法的手写实现代码及详细解析:
Function.prototype.likeCall = function (ctx, ...args) {
// 处理 ctx 为 null 或 undefined 的情况,将 ctx 指向全局对象(利用 globalThis 关键字,它会根据当前运行环境自动获取全局对象,在浏览器中是 window,在 Node.js 中是 global)
// Object()处理 ctx 原始类型时返回改原始类型的包装对象
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
// 使用 Symbol 创建一个唯一的属性名,避免与目标对象原有的属性名冲突
const key = Symbol();
// 通过 Object.defineProperty 定义该属性,将函数(即当前的 this)赋值给该属性,同时设置为不可枚举,这样在外部直接打印 ctx 对象时,该属性不会显示出来,避免造成干扰
Object.defineProperty(ctx, key, { value: this, enumerable: false });
try {
// 通过新增的属性来调用函数,并传入参数,获取函数的执行结果
const result = ctx[key](...(args || []));
return result;
} finally {
// 无论函数执行是否成功,最后都要删除新增的属性,保证目标对象的原始状态不受影响
delete ctx[key];
}
}
手写 aplly 的实现
apply方法与call方法类似,都用于改变函数的this指向并调用函数,但参数传递方式有所不同,apply方法将参数整合为一个数组进行统一传递”。以下是apply方法的手写实现代码及解析:
Function.prototype.likeApply = function (ctx, args) {
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
const key = Symbol();
Object.defineProperty(ctx, key, { value: this, enumerable: false });
try {
// 调用函数,注意这里需要将参数数组(类数组)展开传递给函数(通过...args 语法)
const result = ctx[key](...(args || []));
return result;
} finally {
// 调用完成后删除新增的属性
delete ctx[key];
}
}
手写 bind 实现
bind方法与call、apply的最大不同在于它不会立即调用函数,而是返回一个新的函数,这个新函数在被调用时,其内部的this指向已经被绑定到指定的对象上,并且可以继续传入新的参数。 此外,还有一个重要的特性,当使用new关键字来调用bind返回的新函数时,它的行为会发生改变,此时会返回原始函数(最初调用bind的那个函数)的实例化对象,这一机制符合 JavaScript 中构造函数结合new操作符创建实例的语义规则 以下是bind方法的手写实现代码及解析:
Function.prototype.likeBind = function (ctx, ...args) {
const self = this;
return function (...args2) {
if(new.target){
return new self(...(args || []), ...(args2 || []));
}
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
const key = Symbol();
Object.defineProperty(ctx, key, { value: self, enumerable: false });
try {
// 调用原始函数,合并传入的两组参数(第一次调用 bind 时传入的参数和后续新函数调用时传入的参数)
const result = ctx[key](...(args || []), ...(args2 || []));
return result;
} finally {
// 调用完成后删除新增的属性
delete ctx[key];
}
}
}
感谢阅读,敬请斧正!