晚上敲代码的时候遇到 call() 对 call() 究竟是如何改变 this 对象的指向的来了劲。
事情的起因还得从今晚想自定义实现 new 关键字的功能。结合构造函数创建一个实例对象,欲想创建实例对象,需要改变其 this 指向,自然就想到了三兄弟 call()、bind() 和 apply(),但它们又是怎么改变 this 指向的呢?深层次的原因就要从引用数据类型在堆栈中是什么样子的。
下面是一段代码,我把我的想法全部写在了注释上面,如果有什么不同的见解可以跟我交流。
/**
* newInstance(Fn, ...args)
* 作用:创建 Fn 构造函数的实例对象
* @param Fn 传递构造函数
* @param args 传递参数
*/
function newInstance(Fn, ...args) {
// 创建一个新的空对象,因为我们就是要实例化一个对象
const obj = {};
// 修改 Fn 函数内部 this 的指向为 obj,这里用自己模拟的 call() 方法
// result 获取到函数执行结果 ---> 若 Fn 没有返回值则 result 为 undefined
const result = call(Fn, obj, ...args);
// 改变对象原型指向的原型对象【原型对象是对象其作用是用来共享方法】
// 其实 __proto__ 一般是不能这么使用的,它充当的更多是一个引路人的角色
obj.__proto__ = Fn.prototype;
// 如果构造函数有返回值且返回的是一个引用数据类型则返回引用数据类型
// 若没有返回值以及返回的是基本数据类型则返回结果就是新创建的对象
return result instanceof Object ? result : obj;
}
// 重新回顾下 call() 的作用:改变函数内部 this 指向的对象 + 执行函数并返回函数执行结果
function call(Fn, obj, ...args) {
// 通过中间属性改变 Fn 函数内部 this 对象的指向
obj.temp = Fn;
// 执行函数获取执行结果(谁调用 this 就指向谁,所以这里指向 obj)
// 并且这里使得 obj 对象拥有了 name 属性和 age 属性
// 为什么这样做原来的 obj 对象就拥有了 name 属性和 age 属性呢?
/*
其实这就要理解到引用数据类型【复杂数据类型】的存储机制了,
当 obj 在 newInstance() 被创建出来之后,将会在栈内存开辟一块空间,
在栈内存开辟的这块空间含有 obj 这个变量名以及一个地址,这个地址指向堆内存的 {}
现在 obj 传递给了 call() 就相当于在 call() 里面创建了一个同为 obj 的变量,
然后把这个传递进来的 obj 对象赋值给了这个同名的 obj 变量,
此时这个同名 obj 也在栈内存开辟了一块空间,存放着变量名和地址,其地址也指向了堆内存的 {}
如果这时我们修改了这个同名的 obj 对象里的值,其实也就响应的修改了 newInstance() 中 obj 的值
因为他们指向的都是同一个地址!
---------------------------------------------------------------------------------------
为了方便理解现在暂时不叫他 obj ,我们定义一个变量名为 obj2:
let obj2 = obj;
然后给 obj2 添加一个 A 属性
obj2.A = "123";
之后我们再 newInstance() 方法里头打印输出 obj.A console.log(obj.A);
我们可以看到输出的结果为 123
--------------------------------------------------------------------------------------------
除此之外,也正是利用了这一点,才完成了改变调用 Fn 函数的 this 指向,其实这个 this 指向的就是 obj,
只不过我们将其返回,看到的好像是改变了,其实没有变,因为就是调用的是 obj 自己的方法,this 当然指向的是 obj 自己。
*/
const result = obj.temp(...args);
// 去除 temp 属性
delete obj.temp;
// 返回执行结果
return result;
}