程序地带

细说 js 的7种继承方式


在这之前,先搞清楚下面这个问题:


function Father(){}
Father.prototype.name = "father";
Father.prototype.children = [];
const child1 = new Father();
console.log("get1 ==",child1); // Father {}
console.log("get ==",child1.name); // father
console.log("get ==",child1.children); // []
child1.name = "child1";
console.log("set ==",child1.name); // child1
child1.children.push("child2");
// child1.children = ["123"];
console.log("set ==",child1.children); // ["child2"]
console.log("get2 ==",child1) // Father {name: "child1"}

疑问:


(1)为什么访问 child1.name 的时候,值是原型上的 name 的值,而设置值之后,实例的原型上的name属性未被修改,反而自己生成了一个name属性?


(2) child1.children.push("child2")  与   child1.children = ["123"];  最终的结果为什么会不同?为什么 push 方法会导致原型上的 children 属性也会改变?


参考:


(1)《你不知道的js》第五章-原型——设置与遮蔽属性 


(2)“=”操作符为对象添加属性


简单来说就是:


(1)查询对象属性的时候,会从本体对象开始查找,如果有就返回本体上的属性,因为原型链上的被遮蔽了。如果没有就查原型链,直到原型链最高层null,找不到就返回 undefined。


(2)设置值的时候,如果该属性没有通过 Object.defineproperty 设置 setter 或者 writable 为 true,并且本体对象中没有该属性,并且是 ‘=’ 号赋值,那么会直接在本体对象中添加该属性。


(3)所以上面的 name 值,查询的时候是 原型上的 name值。而设置的时候,符合 (2)的条件,所以直接在 child1 中添加 name属性。如果 改成   child1.name++ ,结果也是本体对象中添加新属性。因为 这句代码等价于 child1.name = child1.name + 1; 。是隐形的等号赋值哦。


(4) child1.children.push("child2")  由于不是等号赋值,那么在 执行 child1.children 的时候,查询到 children 之后,没有 “=” 号赋值,而是 push ,所以操作的是 原型对象中的 children 属性(引用属性)。 而 child1.children = ["123"]  也符合(2)的条件。所以是本体对象新增该属性。


(5)可以简单的理解为如果本体对象上没有该属性, ‘=’ 号赋值之后,分配了新的内存地址,因此只有在本体上新增属性,才能保存赋的值。如果有该属性,就是简单的值替换。


 


上面的问题明白了之后,再来了解一下 js 的几种继承方式。文字有点多,一定要耐心看。几种继承方式是有关系的


1. 原型链继承


function Father(){
this.name = "father";
this.children = [];
this.age = 30;
}
Father.prototype.say = function(){
if(this.children.length){
console.log("我的孩子:"+this.children.join());
}else{
console.log("我是单身狗")
}
}
function CreateChild(name){
this.name = name || "未出生";
this.age = 0;
// this.children = [];
}
CreateChild.prototype = new Father(); // 这句很关键,让子类和父类链接起来,子类继承了父类所有的属性和方法,包括父类原型上的
let child1 = new CreateChild("张三");
let child2 = new CreateChild("张四");
child1.children.push("张小一")
console.log(child1)
console.log(child2.children)

结果:


 


特性:


(1)是通过覆盖构造函数的 原型 prototype 来实现的。


(2)不能给父类传参。


(3)子类继承父类所有的属性和方法,包括 原型 prototype 上的。


(4)这里的继承属性和方法指的是:构造的子实例,本身上是没有属性的(除非自己初始有),只有原型上继承的父类的属性和方法,调用属性或方法是根据原型链查找的。


(5)如果父类有引用类型,子类没有,那么其中一个子类继承来的引用类型修改后会影响所有的子类


说明:


(1)为什么访问 child2.children 的时候,也会出现 child1 的的 children 内容? 因为 访问的时候,child2 本身是没有children 这个属性的,只有在原型上去找,刚好找到父级存在这个属性,而这个属性又是引用属性,一处修改,所有引用的地方都更改了。所以最后结果也是 [‘张小一’];


(2)如果子类里面,自己定义有 children 属性。那么相当于 生成的实例 child1,child2 有些属性就是自己的构造函数上的,并不是继承来的,所以,如果放开  this.children = [] 的注释。你会看到 生成的实例中,父级的 children 属性未变化。


(3)如果在  CreateChild.prototype = new Father() 之后再给   CreateChild.prototype = xxx   赋值的话,结果又会不一样,原型直接被覆盖了。


(4)这里没必要去修复 CreateChild 的 constructor 的指向。因为在  CreateChild.prototype = new Father()    之后,原始 CreateChild 上的原型属性全都被覆盖了,去修复也没什么作用。


(5)那如果我又 想给父类传参 怎么办?借用构造函数继承 就能实现这个需求  


 


2. 借用构造函数继承。


function Father(name){
this.name = name;
this.children = ["张大大"];
this.age = 30;
}
Father.prototype.say = function(){
if(this.children.length){
console.log("我的孩子:"+this.children.join());
}else{
console.log("我是单身狗")
}
}
function CreateChild(name){
// this.children = ["张老大"]
Father.call(this,name);
// this.children = ["张老大"]
}
let child1 = new CreateChild("张三");
let child2 = new CreateChild("张四");
child1.children.push("张大一")
child2.children.push("张小二")
console.log(child1)
console.log(child2)

结果:



 特性:


(1)继承父类的原始属性和方法,不包括原型上的属性和方法。这里继承的属性和方法指的是:构造的实例,本身继承的是父类的属性和方法。原型上无任何变化。


(2)可以给父类传参,但是不能用于实例化(new)父类的时候传参。


(3)父类的引用属性是独立的, Father.call(this,name) 这段代码,相当于给 Father 方法的 this 绑定 为 CreateChild 函数中 this 的指向。然后给这个指向绑定属性和方法,有点 new 的味道。因为 new 的作用是:内部新增一个对象,让构造函数内部的 this 指向这个新对象,然后执行语句,为这个对象绑定属性和方法,最后返回这个对象。


(4)每次初始化子类都会执行一次 Father 父类,不能复用(一次执行,多次使用)。且子类未用到原型


说明:


(1)最重要,最核心的就是   Father.call(this,name)   这段代码,给子类绑定了属性和方法。


(2)如果放开 CreateChild 里面的第一个或者第二个赋值注释,都会因为代码执行的先后顺序 ,原始的数据会被覆盖。


(3)并不会继承父类原型上的属性和方法。因为 此时的 Father 只是当作普通函数执行,所以 prototype 原型上的属性和方法访问不了,因为 Father 并未使用构造函数的方式执行。


(4)如果我 既想继承父类原型上的属性和方法,又想给父类传参 怎么办?那么 组合继承 就能满足这个需求


 


3. 组合继承  


function Father(name){
this.name = name;
this.children = [];
this.age = 30;
}
Father.prototype.say = function(){
if(this.children.length){
console.log("我的孩子:"+this.children.join());
}else{
console.log("我是单身狗")
}
}
Father.prototype.hobbies = ["woman"];
function CreateChild(name){
Father.call(this,name)
}
CreateChild.prototype = new Father();
let child1 = new CreateChild("张三");
let child2 = new CreateChild("张四");
child1.children.push("张大一");
child1.hobbies.push("meet");
child2.children.push("张小二");
child2.hobbies.push("fruit")
console.log(child1)
console.log(child2)
console.log(child2.hobbies)

结果:



 特性:


(1)能继承父类的所有属性和方法,因为  Father.call(this,name)  这句代码。因此,构造实例本身就含有父类的属性和方法。


(2)能继承父类 prototype 原型上的属性和方法,因为  CreateChild.prototype = new Father();  这句代码   。因此,构造实例的原型上含有父类原型的属性和方法


(3)能给父类传参,但是不能用于实例化(new)父类的时候传参。


(4)父类原型上如果有引用属性,某一实例修改后,其它的实例也会受到影响。


(5)每创建一个实例,Father 函数会被执行一次。


说明:


(1)这种继承方式,是第一,二中方式的 组合,所以叫组合继承。囊括了这两种方式的优缺点。


 


4. 原型式继承


function extendChild(target){
function Fn(){};
Fn.prototype = target;
return new Fn();
}
function Father(){
this.name = "father";
this.children = [];
this.age = 30;
}
const FatherInstance = new Father();
const child1 = extendChild(FatherInstance);
const child2 = extendChild(FatherInstance);
child1.children.push("张大一");
child1.name = "child1";
child2.children = ["张小二"];
child2.name = "child2";
console.log(child1)
console.log(child2)

结果:



特性:


(1)通过覆盖一个函数的原型,实现构造的实例的原型上继承传入的对象。构造的实例本身是没有属性和方法的。


(2)如果父类有引用属性,那么一个构造实例改变后,其它的实例也会改变。


(3)每次新增实例,都需要执行一次 new Fn()。


(4)主要的功能就是:基于已有的对象,去创建新对象,继承已有对象的属性和方法。


说明:


(1)如果看了文章最初的第一个问题,就会明白child1和child2的name,还有child2的children 属性为什么会添加到本体属性上。


(2)细心的会发现,这种继承方式,和 Object.create 的 polify 一样一样的,是同样的原理,看mdn。


(3)我如果 想给所有实例添加 共同初始的 方法或者属性,而又不影响父类 怎么办? 寄生式继承 就能解决这个问题


 


5. 寄生式继承


function extendChild(target){
function Fn(){};
Fn.prototype = target;
return new Fn();
}
function Father(){
this.name = "father";
this.children = [];
this.age = 30;
}
const FatherInstance = new Father();
function createChild(target){
var target = extendChild(target)
target.name = "target";
return target;
}
const child1 = createChild(FatherInstance);
const child2 = createChild(FatherInstance);
child1.children.push("张大一");
child1.name = "child1";
child2.children = ["张小二"];
child2.name = "child2";
console.log(child1)
console.log(child2)

结果:



特性:


(1)在原型式继承上,多加了一个函数。


(2)可以实例化前,给所有实例添加公用的方法或属性。不会影响父级


说明:


(1)和原型式继承差不多,其它的没看出来有什么优缺点???


(2)组合继承挺好的,就是父类多调用了,而寄生式继承 只调用了一次,能不能把寄生式继承的优点和组合继承结合起来?所以 寄生组合式 就这么来了


 


6. 寄生组合式继承,这种继承方式是最优的继承方式


function Father(name){
this.name = name || "father";
this.children = [];
this.age = 30;
}
Father.prototype.say = function(){
console.log(this.name);
}
Father.prototype.hobbies = ["fruit"]
function Child(name){
Father.call(this,name)
// this.name = name || "未出生";
// this.age = 0;
}
function createChild(target){
var Fn = function(){};
Fn.prototype = target;
return new Fn();
}
function extendFn(Child,Father){
var instance = createChild(Father.prototype);
Child.prototype = instance;
}
extendFn(Child,Father)
let child1 = new Child("张三");
let child2 = new Child("张四");
child1.children.push("张大一")
child1.hobbies.push("123")
child1.name = "child1"
child2.children.push("张小二")
child2.name = "child2"
console.log(child1)
console.log(child2)

 结果:



 特性:


(1)和组合继承的特性一样。


(2)解决了 多次调用 父类的问题。


说明:


(1)createChild 方法 可以换成 Object.create。还可以省点代码,功能是一样的。


 


7. es6 class类的继承 extends


class Father{
constructor(name){
this.name = name || "father";
this.children = [];
}
hobbies = ["fruit"];
say(){
console.log(this.name)
}
}
class Child extends Father{
constructor(name) {
super(name);
}
}
let child1 = new Child("child1");
let child2 = new Child("child2");
child1.children.push("child1");
child1.hobbies.push("apple")
child2.hobbies = ["apple2"]
console.log(child1)
console.log(child2)
console.log(child2.say())

结果:



 特性:


(1)很方便用???


(2)子类会从父类继承所有的属性和方法,父类的引用属性不共享。


 


 


总结:


(1)原型链继承  —(优化)— >  借用构造函数继承 —(优化)—> 组合继承


(2)原型式继承  —(优化)— >  寄生式继承   —(优化)— >  寄生组合式继承


 


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/zjjDaily/p/14262962.html

随机推荐

C _ PAT B1009 说反话

题目描述给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出。输入格式:测试输入包含一个测试用例,在一行内给出总长度不超过80的字符串。字...

Just-Struggle 阅读(138)

bit索引 mysql_MySql索引详细介绍及正确使用方法

MySql索引详细介绍及正确使用方法1.前言:索引对查询的速度有着至关重要的影响,理解索引也是进行数据库性能调优的起点。索引是存储引擎用于快速查找记录的一种数据结构...

weixin_39728909 阅读(493)

Vesta修改单个原子或元素的颜色

两件事情:修改单个原子的颜色修改整个元素所有原子的颜色修改单个原子的颜色:Objects>点击S1后的颜色方块>在弹窗中选择目标颜色即可将S1这个原子的颜色改变...

Mr.tony_mouse 阅读(209)

datagridviewcheckboxcolumn 不可选_沈阳吊钩试抛丸机怎么选

沈阳吊钩试抛丸机怎么选r7vdwxg沈阳吊钩试抛丸机怎么选各项除尘设备运行费用指标清晰,运行费用成本指标纳入生产成本管理。四是排灰装置,排灰装置不仅仅是布袋除尘器的核心部分...

weixin_39617473 阅读(123)

datagridview 筛选_深圳自动化外观高精度筛选机哪家好

深圳自动化外观高精度筛选机哪家好tigmif6深圳自动化外观高精度筛选机哪家好的差异主要体现在盖板外边缘和通孔处,这些地方的倒边容易造成伪透光,进而导致边缘定位不准确&#x...

weixin_39765796 阅读(106)

bit索引 mysql_mysql索引知识点汇总

一.索引基础知识1.什么叫数据库索引?答:索引是对数据库中一列或者多列的值进行排序的一种数据结构。重点:对列的值进行排序的数据结构。使用索引可以快速访问数据库...

暗夜独舞春上雪 阅读(279)