前言
上一节我向大家介绍了Object与原型之间的关系,知道了原型就是为了实现继承才产生的。如果你还没有看过,建议先看看那篇文章,效果会更好。
彻底搞懂原型(1)—— Object是什么? - 掘金 (juejin.cn)
再次声明为了描述时更加清晰,我这里只将__proto__这个非标准属性称为原型。至于prototype,我就直接叫prototype属性。
好,那么常说的原型链又是怎么一回事呢。
原型链
刚开始学习原型链那会儿,我看到不少作者在讲述原型链时喜欢用图形来解释。又是__proto__,又是prototype,一直往上攀升,看的似懂非懂。我就是想知道在代码里原型链它是怎么样实现的。
首先要明白原型链是由Javascript来帮我们调用的,不是我们主动调用,它是一种隐式操作。所以讲述原型链就是在讲一种逻辑。用图形来表达逻辑确实是一个好的方法,但这也是基于对原型链很清楚的基础上。那么我从纯代码的角度来谈谈原型链。
上一节我们说到一个公式
Object.prototype=对象.__proto__
那么先创建一个对象,好好的看一下这个公式
let foo = {
name: "aaa",
};
console.log(foo);
观察一下foo.__proto__
与Object.prototype
我们发现两者是差不多的。
比较一下两者是否真的相等
console.log(foo.__proto__ === Object.prototype);
结果它们的的确确是相等的。
好了,那么以下公式也应该成立
foo.__proto__ === Object.prototype
foo.__proto__.__proto__ === Object.prototype.__proto__
foo.__proto__.__proto__ === Object.prototype.__proto__ === null
结论:
从代码层面看,原型链就是,以对象的__proto__作为起点, 对象不断的调用__proto__,直到它等于null为止。
要明白几点:
- 只有对象有__proto__属性,所以是对象在不断调用__proto__。函数它也是对象,所以它也有原型链。
- 对象不断的调用__proto__的过程是Javascript隐式调用的,不是我们主动实现的,我刚刚只是模拟一下这个过程。
原型链的作用与好处
刚刚模拟的原型链太短了,还看不出作用来。我让这个过程再长一点。
先定义一个构造函数:
function User() {}
上一节说过,函数在prototype属性上放一些方法,可以交给它所创建的对象的原型
function User() {}
User.prototype.getName = function () {
console.log("name");
};
const u1 = new User();
u1.__proto__===User.prototype
u1.__proto__.__proto__===User.prototype.__proto__===Object.prototype
u1.__proto__.__proto__.__proto__===User.prototype.__proto__.__proto__===Object.prototype.__proto__===null
现在调用u1.getName(),系统就会根据u1的原型链上去找这个方法。u1的原型等于User的prototype属性,User.prototype属性中正好有getName方法,然后执行该函数,原型链查找就停止了。如果没找到就再调用原型往上层查找,找到了Object.prototype。而JavaScript在底层为Object.prototype.__proto__赋值null以防一直循环下去。那么原型链的尽头就是null,一直到null都没找到的话就会报错。如果有方法同名呢?根据原型链的执行顺序,找到了就停止了,这样就遮蔽了上层同名的方法。
原型链的作用还是要实现继承嘛。 好处在于一层继承一层,每一层都可以定义自己独特的方法。下一层的同名方法遮蔽上一层的同名方法。而且创建的对象,它通过原型链就可以继承这些方法了,而不是每创建一个对象,就要为对象开辟的大量空间放入方法,这样就节省了空间。
注1:关于原型链节省空间的好处,展开说一下:还是刚刚的例子,不过把getName方法放入User内。
function User() {
this.getName = function () {
console.log("name");
};
}
// User.prototype.getName = function () {
// console.log("name");
// };
const u1 = new User();
const u2 = new User();
console.log("u1", u1);
console.log("u2", u2);
可以看到每个创建的User的对象都包含了getName方法,如果有100个方法呢,每个对象都要开辟空间放入100方法,太浪费空间了。我们再来看看使用原型链后的效果。
我们只在User.prototype中定义一次方法,每个创建的对象通过原型链取到方法,比上面那个方法大大节省了空间。
所以一般对象共用的方法,都放在函数的prototype属性中。
注2:有些朋友不清楚Javascript中函数与构造函数的区别。
一般函数是封装了代码来实现某个功能的。如果函数用new关键字来创建对象,这样操作以后,就给函数换了个称呼叫构造函数,因为该函数目的是创建对象的。并且约定函数名首字母大写表示,以示与一般函数的区别。
不管是叫函数还是构造函数其实是一个东西,函数在不同用途上的不同叫法。
instanceof操作符
提到原型链就不得不说说instanceof操作符,它是对原型链直接的运用。
使用格式 obj instanceof constructor
含义是:constructor(指构造函数)。constructor.prototype 是否在对象obj的原型链上, 从代码层面解释一下
第一次:
obj.__proto__ === constructor.prototype
如果不相等再到第二层:
obj.__proto__.__proto__ === constructor.prototype
第三层:
obj.__proto__.__proto__.__proto__ === constructor.prototype
...
有两种情况会结束查找:
1 如果有哪一层匹配上了,就返回true,原型链停止查找
2 原型链的顶层就是Object.prototype.__proto__===null,如果直到结果为null了还没有匹配上,那么就返回false,原型链停止查找
举例:
function a(){}
const aa=new a()
console.log(aa instanceof a); //true
console.log(aa instanceof Object); //true
aa instanceof a 表示:
第一层:
aa.__proto__ === a.prototype
因为第一层等式成立,所以原型链停止查找了,返回true
aa instanceof Object 表示:
第一层:
aa.__proto__ === a.prototype !== Object.prototype
等式不成立进入第二层:
aa.__proto__.__proto__ === a.prototype.__proto__ === Object.prototype
第二层等式成立,原型链停止查找了,返回true
a.prototype是否出现在aa._proto__.__proto__.__proto__这个过程中
又因为aa.__proto__===a.prototype,说明a.prototype出现在aa._proto__的原型链上
所以就返回true
所以有的时候,我们简单把instanceof用作为aa是否是a的实例。这种说法或者做法,是不完全正确的。
小结一下例子: obj instanceof constructor 表示的意思就是拿着constructor(构造函数)的prototype属性在 obj(对象)的原型链上匹配,匹配上了就返回true,一直匹配不上就返回false。 以上例来说 aa instanceof a 就表示拿着a.prototype在aa的原型链上匹配。 aa instanceof Object 就表示拿着Object.prototype在aa的原型链上匹配。 在这个过程中构造函数的prototype属性的值是不变化的,一直变化的是对象的原型。
注1:有时候会说,instanceof是判断用来判断对象是构造函数的实例。从以上示例能看出来,这样说不完全正确。 因为,对象是构造函数的实例,这是一种父子关系,在整个原型链上可能隔着好几条链呢!比如aa instanceof Object不就是祖孙关系嘛。
注2: instanceof与typeof操作符都是来判断数据类型的,那么什么时候用typeof,什么时候用instanceof呢?
type:
typeof在判断基本数据类型时能准确返回类型结果,且是小写的。但是有特例,判断null时会返回object。
console.log(typeof 1); // number
console.log(typeof null); // object
在判断引用类型时一般返回object,但是也有特例。判断函数时会返回function
console.log(typeof {}); // object
console.log(typeof Object); // function
instanceof:
instanceof可以用来判断更具体的引用类型,但是它返回的是boolean值,所以在条件判断语句中挺好用的。
console.log([] instanceof Array); // true
Object.prototype.toString.call(XXX) 除了以上两种外,还能利用Object.toString()方法彻底的区分各种类型。比如说想区分对象与数组与数字
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(1)); // [object Number]
基本类型的隐式装包与拆包
这里和大家分享一下,不太常说的知识点。
一般声明一个基本类型后,就可以直接使用该类型的方法。
let foo = "a";
let bar = foo.toUpperCase();
console.log(bar); // A
本身foo上,我们没有声明其它方法,但是它能调用 toUpperCase()
方法。说明foo原型链上应该有toUpperCase()
方法。而foo的构造函数是String函数。那些字符串的方法正是定义在String.prototype上。
很奇怪,通过调用instanceof操作符的结果竟然是false。那foo是怎么拿到toUpperCase()方法呢?
答案就是js引擎在底层做了处理。
当调用字符串的方法时,JavaScript 引擎会自动将字符串字面量转换为相应的字符串对象。
这种自动转换的过程是由 JavaScript 引擎进行的,它会在访问字符串字面量的方法时临时创建一个对应的字符串对象,然后调用该方法。这个过程被称为自动装包(也称隐式装包,自动装箱,隐式装箱...)
一旦方法执行完成,临时创建的字符串对象就会被销毁。这个过程被称为自动拆包。
这种行为的设计是为了方便开发人员在处理字符串时能够使用字符串的方法,同时仍然保持字符串字面量的简洁性和效率。
手写instanceof函数
理解了instanceof的原理就可以写出一个与之功能相同的函数。
思路:
那么这个函数应该有两个参数,一个对象,一个构造函数
要准备对象的原型和构造函数的prototype属性
将构造函数的prototype属性与对象的原型匹配,如果相等就返回true
如果不相等就将构造函数的prototype属性与对象的原型的原型匹配。如果相等就返回true,如果不相等再继续攀升原型链。
如果对象的原型链一直到null都没有匹配到,那么就返回false
根据这个思路,我这里用两种方法来实现。
方法1:使用循环
function myInstance(obj, constructor) {
let obj__proto__ = obj.__proto__;
const constructorPortotype = constructor.prototype;
while (obj__proto__ !== null) {
if (obj__proto__ === constructorPortotype) {
return true;
} else {
obj__proto__ = obj__proto__.__proto__;
}
}
return false;
}
function a() {}
const aa = new a();
console.log(myInstance({}, Object)); // true
console.log(myInstance(aa, a)); // true
console.log(myInstance(aa, Object)); // true
方法2:使用递归
function myInstance2(obj, constructor) {
let obj__proto__ = obj.__proto__;
const constructorPortotype = constructor.prototype;
if (obj__proto__ === null) {
return false;
} else if (obj__proto__ === constructorPortotype) {
return true;
} else {
return myInstance2(obj__proto__, constructor);
}
}
function b() {}
const bb = new b();
console.log(myInstance2({}, Object));
console.log(myInstance2(bb, b));
console.log(myInstance2(bb, Object));
总结
这一节向大家介绍了原型链的原理,理解了instanceof操作,以及手写实现了instanceof功能的函数。
码字不易,如果觉得还行,麻烦点个赞,谢谢!