原型链和常见继承
什么是原型链:
原型指针:
- prototype: prototype属性,它是函数所独有的,它是从一个函数指向一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象; 这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象);
- __proto__: __proto__ 是原型链查询中实际用到的,它总是指向 prototype,换句话说就是指向构造函数的原型对象,它是对象独有的。注意,因为JS中,包括函数,万物皆对象,所有也有这个属性;
- constructor: 每个函数都有一个原型对象,该原型对象有一个constructor属性,指向创建对象的函数本身。此外,我们还可以使用constructor属性,所有的实例对象都可以访问constructor属性,constructor属性是创建实例对象的函数的引用。我们可以使用constructor属性验证实例的原型类型(与操作符instanceof非常类似)。由于constructor属性仅仅是原始构造函数的引用,因此我们可以使用该属性创建新的对象。
阅读下面代码:
// 让我们从一个函数里创建一个对象o,它自身拥有属性a和b的: let f = function() { this.a = 1; this.b = 2; } let o = new f(); // {a: 1, b: 2} // 在f函数的原型上定义属性 f.prototype.b = 3; f.prototype.c = 4; console.log(o); console.log(o.a, o.b, o.c, o.d);
- a是o的自身属性,该属性值为 1;
- b是o的自身属性,该属性值为 2,原型上b属性会发生"属性遮蔽",不会被访问到;
- c不是o的自身属性,就看它的原型链上有没有,c是o.[[Prototype]]的属性,值为 4;
- d不是o的自身属性,原型链上也没有这个属性,返回 undefined;
完整的原型链示意图:
所有的对象都是继承自Object,所以Person和Function的prototype的__proto__都指向Object的prototype。
常见的6种继承方式:
继承的父类
function Person(name) { this.name = name; this.read() = function() { console.log(this.name); } } Person.prototype.age = 10;
- 原型链继承
// 原型继承 function Student() {} Student.prototype = new Person() var student1 = new Student(); var student2 = new Student(); console.log(student1.name, student2.name); student1.name.push('tiger'); // 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会发生变化 console.log(student1.name, student2.name);
重点:让新实例的原型等于父类的实例。
特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
缺点:1、新实例无法向父类构造函数传参。2、继承单一。3、所有新实例都会共享父类实例的属性。
- 构造函数继承
// 构造函数继承 function Student() { Person.call(this); this.name = ['cat']; } var student1 = new Student(); console.log(student1.age)
重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行复制)
特点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。2、解决了原型链继承缺点。3、可以继承多个构造函数属性(call多个)。4、在子实例中可向父实例传参。
缺点:1、只能继承父类构造函数的属性。2、无法实现构造函数的复用。(每次用每次都要重新调用)3、每个新实例都有父类构造函数的副本,臃肿。
- 组合继承
// 组合继承 function Student() { Person.call(this); } Student.prototype = new Person(); var student1 = new Student(); console.log(student1); // 从下图可以看出,继承了两份相同的属性
重点:结合了两种模式的优点,传参和复用。
特点:1、可以继承父类原型上的属性,可以传参,可复用。2、每个新实例引入的构造函数属性是私有的。
缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
- 原型式继承
// 原型式继承 function content(obj) { // 封装一个容器,用来输出对象和承载继承的原型 function F() {}; F.prototype = obj; // 继承了传入的参数 return new F(); } var student = new Person(); // 拿到父类实例 var student1 = content(student); console.log(student1)
重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:类似于复制一个对象,用函数来包装。
缺点:1、所有实例都会继承原型上的属性。2、无法实现复用。(新实例属性都是后面添加的)
- 寄生式继承
// 寄生式继承 function content(obj) { function F() {}; F.prototype = obj; return new F(); } var Student = new Person();
// 原型式继承外,再套一个壳子传递参数 function subObject(obj) { var student = content(Student); return student; }
// 函数经过声明后,成为可添加属性的对象 var student1 = subObject(Student); console.log(student1)
重点:就是给原型式继承外面套了个壳子。
优点:没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点:没用到原型,无法复用。
- 寄生组合式继承
// 寄生组合继承
// 寄生 function content(obj) { function F() {}; F.prototype = obj; return new F(); } var con = content(Person.prototype);
// 组合 function Student() { Person.call(this); } Student.prototype = con; con.constructor = Student(); var student1 = new Student(); console.log(student1);
寄生:在函数内返回对象然后调用
组合:1、函数的原型等于另一个实例。2、在函数中用apply或者call引入另一个构造函数,可传参。