1.原型链的概念
原型链,顾名思义,其实就是通过原型组成的一条链,且是隐式原型__proto__组成的,用于访问查找一个对象的属性的。
通过上篇文章,可以知道任何函数都有一个属性prototype(显式原型),而任何实例对象也都有一个隐式原型__proto__,并且实例对象的隐式原型等同于其构造函数的显式原型。但是通过打印发现构造函数的原型对象prototype中也存在隐式原型,这说明构造函数的原型对象是一个实例对象,那它是谁的实例呢?是Object对象的实例。
原型链,别名是隐式原型链。是在访问一个对象的属性时,首先是在自身属性中查找,找到就返回,如果没有,则沿着__proto__这条链一直向上查找,找到返回,如果最终没有找到,则返回undefined.
原型链的作用:查找对象的属性(方法)。
1.1 代码体验
// 注意方法也是属性 <script> // 1. 创建一个构造函数,并为其实例添加属性test1 function Fn() { this.test1 = function() { console.log('test1()'); } }; // 2. 为其显式原型添加属性test2 Fn.prototype.test2 = function() { console.log('test2()'); }; // 3. 创建一个实例对象fn let fn = new Fn(); // 4. 实例可以调用test1、test2、toString,但是无法调用test3 fn.test1(); fn.test2(); console.log(fn.toString()); fn.test3(); </script>
分析:
① fn可以调用test1属性,是因为这是它自带的属性;
② fn可以调用test2属性,是因为通过自身的__proto__属性查找到的。(实例的__proto__属性和其构造函数的prototype的地址相同,指向同一个Object实例对象。而上述代码通过构造函数的prototype为其添加了属性test2,所以可以调用
③ fn可以调用toString属性,是因为__proto__属性指向的Object实例对象中的__proto__属性指向的是Object对象的原型对象,该原型对象中存有该属性。
④ fn无法调用test3属性,是因为一路查找,直至Object对象中的原型对象prototype中都未找到。
以上fn调用属性的过程就是一个原型链,通过__proto__一层一层往上查找。
<script> function Fn() { }; // 1. 函数的显示原型指向的对象:默认是空Object的实例对象(但Object本身不满足这一说法) // instanceof作用:测试它左边的对象是否是它右边的类的实例 console.log(Fn.prototype instanceof Object); // true console.log(Object.prototype instanceof Object); // false console.log(Function.prototype instanceof Object); // true // 2. 所有函数都是Function的实例(包含Function本身) // 说明所有函数的__proto__都是一样的 function Fun(){}; // 等同于 let Fun=new Function(); // 这就说明函数Fun是Function的实例 Function=new Function(); // 这说明Function本身也是Function的实例 console.log(Function.__proto__ === Function.prototype); // true,构造函数Function的原型对象===实例的隐式原型,证明上述结论的准确性 // 3. Object的原型对象是原型链尽头 console.log(Object.prototype.__proto__); // null </script>
由于Object的原型对象中的__proto__属性的值是null,所以说Object的原型对象是原型链的尽头,即从下往上查找,直到在Object的原型对象中也未查找到的属性,则为undefined。
1.2 原型链图解
2.原型链的属性问题
① 读取对象的属性值时:会自动到原型链中查找;
② 设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值;
③ 方法属性一般定义在原型中,属性一般通过构造函数定义在实例对象本身上;
<script> // 1. 定义一个构造函数 function Fn() { }; // 2. 为原型对象添加属性a Fn.prototype.a = '你好吗?'; // 3. 创建一个实例对象 fn1 let fn1 = new Fn(); // 读取属性时会自动到原型链中查找 console.log(fn1.a); // 你好吗? // 4. 创建一个实例对象 fn2 let fn2 = new Fn(); // 5. 为fn2 实例对象设置属性a,该属性是fn2实例对象的专有属性,所以fn1无法获取 // 设置对象的属性值时,不会查找原型链,而是只看当前对象本身是否存在该属性,如果没有则直接添加并设置值,如果有则覆盖之前的值 fn2.a = '我很好'; console.log(fn1.a, fn1); // 你好吗? console.log(fn2.a, fn2); // 我很好 // 6.1 属性一般通过构造函数直接定义在实例对象本身上 function Person(name, age) { this.name = name; this.age = age; }; // 6.2 方法一般定义在构造函数的原型中 Person.prototype.setName = function(name) { this.name = name; }; // 6.3 正因为这样的设置,每个实例对象都拥有了自身的属性,但是方法均来源于原型中 let p1 = new Person('张三', 22); p1.setName('Tom'); console.log(p1); let p2 = new Person('李四', 35); p2.setName('Bob'); console.log(p2); // 同一构造函数中所有实例对象的隐式原型都是指向该函数的显式原型prototype,所以相等 console.log(p1.__proto__ === p2.__proto__); // true </script>
3. 构造函数 / 原型 / 实例对象的关系
通过上图可以知道任何函数都是构造函数Function的实例对象,即所有函数都有一个隐式原型__proto__,指向的是Function的显示原型prototype。但是注意:Function是内置的构造函数,也是对象,都是继承了Object的所有属性和方法,所以Function的原型对象的__proto__指向的是Object的原型prototype。