文章目录
- 概要
- 继承的进化史
- 技术名词解释
- 原型链的作用
- 原型链继承
- 案列分析
- 源代码解析
- 效果图
- 预留问题
- 小结
概要
这阵子在整理JS的7种继承方式,发现很多文章跟视频,讲解后都不能让自己理解清晰,索性自己记录一下,希望个位发表需要修改的意见,共勉之。
部分文章总结出来的数据可能是错误的,网络上太多数据了,有些不严谨,当然我也可能是其中一人,如果有问题,欢迎提出来,共同进步。
继承的进化史
JS的7种并不是逐级进化的(个人觉得~),可以参考下图:
技术名词解释
- 构造函数:构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数。在其他面向的编程语言里面,构造函数是存在于类中的一个方法。虽然es6 中的class也是解决了旧的JS继承方式,但是无奈考官要提问,只能记住了 :) …
- 原型:原型就是一个对象,也叫原型对象,不同的人叫法不一样,很容易搞懵逼初学者,原型===原型对象。
- 原型链:就是实例对象和原型对象之间的链接,每一个对象都有原型,原型本身又是对象,原型又有原型,以此类推形成一个链式结构,称为原型链。
- prototype :子类构造函数 所指向的原型(实例化的父类)
Children.prototype = new Parent(); 这个是手动指向
。 - proto:子类实例对象 所指向的原型(实例化的父类)
这个是new 操作符里面的操作
。
原型链的作用
当你访问一个对象的属性(数据或者方法),如果该对象没有该属性,并且它还有原型的话,就会去原型对象找,原型对象找不到该数据,就再找原型对象的原型对象,直到世界尽头…(null)
原型链继承
原型链继承,主要是靠子类的prototype属性,指向父类的实例,通过原型链的方式,实现继承。JS中每个实例都会有原型。
这里为什么不是每个对象都会有原型呢?
let obj = {}; // 默认都有原型
console.dir(obj);
let obj2 = Object.create(null); // 通过Object.create() 创建无原型对象
console.dir(obj2);
案列分析
- 父类:Parent,要被继承的类,可以理解为人。
- 子类:Children,要继承父类的类,可以理解为不同的工种,但是还是一个人,所以继承了父类的一些基础属性。
- 实例:person1 ,person2,实例化的子类,一般实例化后数据就分开了,复合数据类型要注意…
父类的数据以及方法要给子类继承,通过了JS的原型链的原理,将子类的prototype,指向了实例化的父类。
Children.prototype = new Parent();
继承是实现了,但是打印对象发现结构不对,少了个构造函数,文章下方也有截图展示对比
Children.prototype.constructor = Children;
父类属性:
- name:公共属性,每个人都有名字
基础数据类型
- age:公共属性,每个人都有年龄
基础数据类型
- data:公共属性,个人的一些数据,可以理解为个人的收藏,有人喜欢红酒,有人喜欢首饰,这里的是
复合数据类型
,需要注意,有时候误操作会影响到其他实例,因为它存放的是一个地址。 - sayName:公共方法
- sayData:公共方法,用来打印等下误操作的数据问题,打印BUG的地方…
- sayAge:父类原型对象上的方法
主要是为了跟构造函数继承对比,构造函数的缺点就是无法使用原型链上的数据...
源代码解析
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>原型链继承解析</title>
</head>
<body>
<h3>原型链继承解析</h3>
<h4>由于要打印原型对象,在浏览器操作比较方便,就用了html文件。</h4>
<script>
// 父类
function Parent(name, age, dataA, dataB) {
// 基本数据类型
this.name = name;
this.age = age;
// 复合数据类型
this.data = { a: dataA ? dataA : "a", b: dataB ? dataB : "b", c: "c" };
// 父类内部方法
this.sayName = function () {
console.log("我的名字:", this.name);
};
// 全局的关键是看这个...原型链的bug在这里
this.sayData = function () {
// 这里还不能直接打印对象出来,因为是一个指针,总是指向最新的数据...
console.log("我的数据如下:");
console.dir(this.data.a);
console.dir(this.data.b);
console.dir(this.data.c);
};
}
// 父类原型链方法 (这个跟原型链继承只有半毛钱关系...)
Parent.prototype.sayAge = function () {
console.log("我的年龄:", this.age);
};
// 子类,不同工种
function Children(name, age, dataA, dataB, job) {
// 原型链继承无法通过super 传递给父类,只能通过覆盖的形式了
this.name = name;
this.age = age;
this.job = job;
this.data.a = dataA;
this.data.b = dataB;
this.sayJob = function () {
console.log("我的工作:", this.job);
};
}
// 原型链继承,构造函数的原型,是一个对象,才会在内存在分配空间,所以是需要实例化的。
// 这边只写了一个子类,如果是多个子类,其实就会实例化多个原型对象,一般数据不会相互影响,除了复合数据类型,这边不进行验证...
Children.prototype = new Parent();
// 不加这句结构不完整,可看文章末尾...
Children.prototype.constructor = Children;
console.log("===person1...");
let person1 = new Children(
"钟先生",
33,
"person1A",
"person1B",
"程序员"
);
// 基本数据类型
person1.sayName();
person1.sayAge();
person1.sayJob();
// 复合数据类型
person1.sayData();
console.log("===person2...");
let person2 = new Children(
"刘小姐",
18,
"person2A",
"person2B",
"清洁阿姨",
);
// 基本数据类型
person2.sayName();
person2.sayAge();
person2.sayJob();
// 复合数据类型
person2.sayData();
console.log("===重新打印person1,不用实例化,看看person1会不会被影响...");
// 基本数据类型
person1.sayName();
person1.sayAge();
person1.sayJob();
// 复合数据类型
person1.sayData();
console.log(
"===可以看出,基本数据类型不会被影响到,但是复合数据类型出事了..."
);
console.dir(person1);
</script>
</body>
</html>
效果图
这里可以看出来,复合类型数据被污染了
部分同学不知道哪里修改了数据,这边使用构造函数初始化的时候,修改了,可以去看父类的构造函数!
下图主要是为了跟构造函数继承对比
预留问题
这里面功能是实现了,但是Children的实例的第一个原型对象是不是少了一个构造函数?
好像少了构造函数这个标识也不受影响。。。
es6 出来的class类,算是比较完整的,这边截图对比一下原型链继承以及class继承。
所以需要手动添加
Children.prototype.constructor = Children;
小结
原型链可以实现继承,但是如果子类实例修改了父类的复合数据类型(对象,方法,数组),就影响到了其他实例。
对于JS继承不熟悉的同学,感觉可以先从class继承开始,了解其结构,因为那个是最简单实现,并且最标准的格式了,大概了解了JS继承逻辑、prototype、proto、[[prototype]]、constructor 在实例中的结构再逐步学习对比其他继承,当然如果不去面试的话,直接用class就好了…