前言
在上一节中我们了解了原型链的相关知识。那么这一节来谈谈原型链的一些具体应用和对象的非标准属性__proto__。
原型链的应用
在构造函数的rototype属性中定义方法
将方法定义在构造函数的prototype的属性上,创建的对象通过原型链就能使用该方法,这大大节省了空间。详细的解释与例子在上一节也有提到,这里就不再赘述了。
彻底搞懂原型(2)------原型链是什么 - 掘金 (juejin.cn)
in操作符与hasOwnPrototype
它们都是用来检测对象上是否有某个属性。不同的是in操作符还会在原型链上查找。而hasOwnPrototype只会在当前的对象上查找属性。
js
let a = {};
a.__proto__.name = "aaa";
console.log("name" in a); // true
console.log(a.hasOwnProperty("name")); // false
所以vscode,for-in快捷语句中会配上hasOwnPrototype明确表示只在当前对象上查找属性。
js
for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
const element = object[key];
}
}
如果细心一点的,可能会发现。Object.prototype上才有hasOwnProperty方法呀,为什么vscode会直接来个Object.hasOwnProperty,看上去hasOwnProperty就好像是一个Object的静态方法了。

这肯定是从Object原型链上找到的。盘一下这个原型链
js
Object.__proto__===Function.prototype
Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype
通过原型链找到Object.prototype,所以找到了hasOwnProperty的方法。
注:但是注意Object.prototype中有一个方法在调用时不能省去prototype。
toString(),因为在Function.prototype中正好有一个同名方法,而它又在原型链下层,遮蔽了上层的Object.prototype的toString()

call方法
之前听向军老师说过,call方法就是当自己以及自己的原型链上没有某种方法,但是别的类型的原型链上有,那么就借用一下。这个说法挺有趣,通俗易懂。
它的实现原理就是把某个方法,临时挂在某个对象上。然后用obj点的方式调用一下,当用obj.的方式调用方法,就正好改变了该方法的this指向,this就指向了obj。调用后再从对象上删除该方法。确实就像向别人借东西,用完后就还回去了。
所以call方法的第一个参数,就是该参数要去借别人的方法,所以方法挂在这个对象上,也是绑定this的对象。因为就是该对象用点的方式调用了方法。
之前使用过:
js
Object.prototype.toString.call({})
Object.prototype.hasOwnProperty.call(obj,key)
其实它们都是一个道理,将Object.prototype.toString,Object.prototype.hasOwnProperty方法分别挂在{},obj上,并调用。
举例:
js
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
const lis = document.getElementsByTagName("li");
console.log(lis);
</script>
</body>


可以看到它是没有forEach方法的,一旦使用就会报错。但是Array构造函数有啊。Array构造函数创建的数组,通过原型链就能拿到Array.prototype.forEach方法。那借用一下。
js
Array.prototype.forEach.call(lis, (res) => {
console.log(res);
});

手写call函数
写一个mycall函数来实现call方法的功能 思路:
- 参数有两个。需要一个对象,需要一个方法。
- 对象要添加该方法
- 执行方法
- 删除对象上的方法
js
function mycall(obj, fn, ...args) {
obj.getway = fn;
let res = obj.getway(...args);
delete obj.getway;
return res;
}
mycall(lis, Array.prototype.forEach, (res) => {
console.log("res", res);
});

就是这么简单,我们似乎就实现了call方法的功能。该方法还不够好,没有检验参数类型,临时key值用了一个getway的自定义的名字。再改进改进。
js
function mycall2(fn, obj, ...args) {
if (typeof fn !== "function") {
throw new TypeError("第一个参数必须是一个函数");
}
// 检查 context 是否为对象,如果未提供,则设置为全局对象(浏览器中为 window)
if (typeof obj !== "object" || typeof obj === null) {
obj = typeof window === "undefined" ? global : window;
}
// 用Symbol创建唯一的键以避免与现有属性冲突,可以把Symbol看作对字符串类型的扩展,它是不会重复的字符串
const keyTemp = Symbol("keyTemp");
obj[keyTemp] = fn;
const result = obj[keyTemp](...args);
delete obj[keyTemp];
return result;
}
mycall2(Array.prototype.forEach, lis, (res) => {
console.log("res", res);
});

原型的标准用法
我们之前一直用__proto__来表示对象的原型,这是浏览器厂商定义的非标准属性,我们可以用__proto__来获取和设置原型。
那么标准的获取对象原型的方式就是使用
Object.getPrototypeOf(obj)获取对象原型
Object.setPrototypeOf(obj,xxx)来设置原型

js
let a = {};
let b = {};
a.__proto__ = b; //非标准设置a对象的原型
Object.setPrototypeOf(a, b); //标准设置a对象的原型
之前我们用__proto__来写原型链,现在换成setPrototypeOf看看效果
原型标准形式写instanceof功能
js
function myInstance(obj, contructor) {
let obj__proto__ = Object.getPrototypeOf(obj);
const constructorPortotype = contructor.prototype;
if (obj__proto__ === null) {
return false;
} else if (obj__proto__ === constructorPortotype) {
return true;
} else {
return myInstance(obj__proto__, contructor);
}
}
那么今后尽量使用标准方法来获取原型
__proto__
究竟是什么
__proto__
是对象的访问器,getter与setter

它实现的代码:
js
let a = {};
Object.defineProperty(a, "__myproto__", {
get() {
return Object.getPrototypeOf(this);
},
set(value) {
// 原型的值要是对象类型
if (typeof value === "object" && value !== null) {
Object.setPrototypeOf(this, value);
}
},
});
console.log(a);

当我们获取a对象的原型时,我们就走get方法,当设置a对象的原型时,就走set方法
总结
这一节,说明了原型链的三个具体应用,分别是在
- 构造函数的prototype属性上定义方法以节省空间
- 对象的遍历两种方法:in操作符会攀升原型链,hasOwnProperty不会
- call方法就是将借用的方法挂在第一个参数上并手写了mycall函数
还说了对象原型的标准方法:
- 获取原型用Object.getPrototypeOf()
- 设置原型用Object.setPrototypeOf()
最后还使用对象原型的标准方法说明了非标准原型__proto__是对象的访问器getter与setter