特征
-
封装:把需要复用的代码 放入对象的方法中
-
继承:一个对象拥有另一个对象的所有成员(JS重点)
-
多态:一个对象在不同情况下的多种状态
继承(JS)
子对象 继承 父对象所有的成员
js中常用的三种继承方式
假设有两个对象,一个是父亲对象,一个是儿子对象,父亲有房子属性(或其他属性),儿子通过继承的方式获得父亲的房子属性(或其他属性)。
1. 混入式继承
遍历父对象成员添加给子对象(forin),一般用于继承一个子对象,如果子对象一多,代码冗余(有几个继承就要写几次forin循环了)。
//父对象
let father = {
house: {
address: 'xxxxx小区',
price: 'xxxx¥'
}
}
//子对象
let son = {
worker: '销售员' //本身的属性
}
/*
混入试继承:遍历父对象成员添加给子对象
*/
for (let key in father) {
son[key] = father[key];
}
console.log(son); //son中除了自己的属性(儿子的工作),还有了爸爸的房子(继承过来的)
2. 替换原型继承
将父对象作为子对象的原型(适用于多个继承),可以实现多个子对象继承,但是会覆盖子对象默认的原型。
//父对象
let father = {
house: {
address: 'xxxxx小区',
price: 'xxxx¥'
}
}
//第二种方式:
//替换原型继承:将父对象作为子对象的原型
function Son(work) {
this.work = work;
}
//子对象默认的原型
Son.prototype.hobby = function() {
console.log('乒乓球');
}
//将父对象作为子对象的原型
Son.prototype = father; //这里就会把Son中的默认原型hobby给替换成了father中的属性
let son1 = new Son('销售员');
let son2 = new Son('程序员');
console.log(son1.house, son2.house);
3.混合式继承
混入式 + 替换原型
*遍历父对象所有成员,添加给对象的原型
//第三种方式
//混合式继承 : 混入式+替换原型
function Son(work) {
this.work = work;
}
//子对象默认的原型
Son.prototype.hobby = function() {
console.log('乒乓球');
}
//遍历父对象所有成员,添加给子对象原型
for (let key in father) {
Son.prototype[key] = father[key];
}
let son1 = new Son('销售员');
let son2 = new Son('程序员');
console.log(son1.hobby(), son2.house);
原型链
原型链
每一个对象都有自己的原型,而原型也是对象,也会有自己的原型(链),以此类推形成一个链式结构---原型链。
原型
-
所有引用类型都有一个proto(隐式原型)属性,属性值是一个普通的对象
-
所有函数都有一个prototype(原型)属性,属性值是一个普通的对象
-
所有引用类型的proto属性指向它构造函数的prototype
对象访问原型链的规则
先看自己有没有,自己有优先访问自己的,没有再去从原型对象中去找,还没有就从原型对象的对象中去找,以此类推,到终点也没有找到的话,属性会返回undefined,方法就会报错。
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
构造函数:是用来创建对象的函数,本质上也是函数
原型对象:当我们每次创建一个函数的时候,函数对象都会有一个prototype属性,这个属性是一个指针,指向它的原型对象。
实例对象:通过构造函数创建实例对象
class Person {
//构造函数
constructor(name, age) {
this.name = name;
this.age = age;
}
}
//原型对象
Person.prototype.type = '灵长类'
Person.prototype.eat = function() {
console.log('杂食性');
}
//实例对象
let p1 = new Person('warm', 19);
//找实例对象p1的原型
console.log(p1.__proto__.constructor); //用的是谁的构造函数[class Person] p1.__proto__.constructor(p1的原型是什么) -->__proto__的意思是谁的原型 constructor原型是什么
console.log(p1.__proto__ == Person.prototype); //p1的原型是不是Person的原型 true
//检查p1的原型的原型
console.log(p1.__proto__.__proto__.constructor); //[Function: Object]
console.log(p1.__proto__.__proto__ === Object.prototype);
//检查p1的原型的原型的原型
console.log(p1.__proto__.__proto__.__proto__.constructor);//null不是没有了而是给个终点 不然这得找哪儿去了都 所以null代表原型链的终点
/* 关于下面输出的解释帮助理解 */
console.log(p1.name); //p1中有的属性
console.log(p1.type); //p1没有 但是他指向的原型(Person)里面有
p1.eat(); //原型有
p1.toString(); //这里虽然不打印 但是也不报错 原型链 虽然p1没有 p1的原型也没有 但是p1的原型里面还有一个原型(原型中还有原型)可能里面的原型有
p1.learn(); //p1没有,原型链里面也没有 报错
内置对象原型链
所有对象的原型链都会最终指向Object
小技巧:当你想使用某个内置对象的函数,但是你又记不太清这个函数名,这个时候你可以打印他的内置原型链,去查看原型中的函数。
Array
数组对象,具体看代码
let arr = [1, 2, 3, 4, 5, 6];
//1.查看arr对象的原型
console.log(arr.__proto__.constructor); //Array 证明什么的操作其实就是 new Array(1,2,3,4,5,6);
console.log(arr.__proto__ === Array.prototype); //true 两个都指向同一个原型链
//2.查看arr对象的原型的原型
console.log(arr.__proto__.__proto__.constructor); //Object
console.log(arr.__proto__.__proto__ === Object.prototype); //true
//看接下来分析
arr.reverse();
//Array只有一个属性 length 但是他的原型中有许多方法(revers()等)
//js中所有的方法都被写到原型中 不写到Array中是因为不需要每创建一次数组 执行一编这么多函数 直接写原型里面会省下很多事
Date
//日期对象
let date = new Date();
/*
注意 : js中有少部分的对象,无法通过log来打印的,一旦使用log会自动转成字符串显示
*这三种:日期对象Date 函数对象function Dom对象
如果想要看这三个对象的内存,则需要使用 console.dir()
*/
console.log(date); //浏览器页面时没有Prototype的
console.dir(date); //浏览器会有Prototype的
//小技巧 -- 当你忘记你需要用的那个方法的具体单词 你可以通过打印原型链 去查看你需要的那个方法
// console.dir(date);就是这个可以去浏览器查看原型中的方法
//1.查看date对象的原型
console.log(date.__proto__.constructor); //Date 证明什么的操作其实就是 new Array(1,2,3,4,5,6);
console.log(date.__proto__ === Date.prototype); //true 两个都指向同一个原型链
//2.查看date对象的原型的原型
console.log(date.__proto__.__proto__.constructor); //Object
console.log(date.__proto__.__proto__ === Object.prototype); //true
String
//两种方法 两者打印会有区别
//1.console.log(str.__proto)
let str1 = 'abc';
console.log(str1.__proto__); //这里只能调用原型才能打印出来
//2.console.log(str);
let str2 = new String('abc'); //str = 'abc'这两个功能是一样 但是你要想直接查原型链 建议使用 new String('abc');这种方式
console.log(str2);
//1.查看String对象的原型
console.log(str2.__proto__.constructor); //String
console.log(str2.__proto__ === String.prototype); //true 两个都指向同一个原型链
//2.查看String对象的原型的原型
console.log(str2.__proto__.__proto__.constructor); //Object
console.log(str2.__proto__.__proto__ === Object.prototype); //true
DOM对象原型链
DOM对象结构设计 : 一共是有七层的原型链结构
<body>
<div class="box">我是div</div>
<p class="pp">
我是标签p
</p>
<script>
let box = document.querySelector(".box");
let pp = document.querySelector(".pp");
console.log(box); //log打印不出来原型链哦
//一共七层层
console.dir(pp);
console.dir(box); //本身的原型
console.dir(pp.__proto__); //HTMLParagraphElement pp的第一层 之后的都是一样
console.log(box.__proto__); //HTMLDivElement div的第一层 之后的都是一样
console.log(box.__proto__.__proto__); //HTMLElement
console.log(box.__proto__.__proto__.__proto__); //Element
console.log(box.__proto__.__proto__.__proto__.__proto__); //Node
console.log(box.__proto__.__proto__.__proto__.__proto__.__proto__); //EventTarget
console.log(box.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__); //Object
console.log(box.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__); //终点null
</script>
</body>
函数也是对象类型
点语法证明(对象才有点语法)
console.log打印代码区的内容
console.dir打印对象区的内容
函数中其实有代码区和对象区一分为二,但是我们一般使用函数来存储代码。
//1.使用点语法证明(对象才有点语法)
function fn() {
console.log('这是一个函数');
}
fn.sex = '男';
fn.age = 19;
console.dir(fn); //log只能打印代码 所以要看属性dir打印 这里可以看到函数里面多了age,sex的属性 而且函数也有原型链
console.log(fn.sex);
console.log(fn.age);
查看函数的原型链(对象区)
//查看函数的原型链
console.log(fn.__proto__.constructor);
console.log(fn.__proto__ === Function.prototype);
//查看函数原型的原型
console.log(fn.__proto__.__proto__.constructor);
console.log(fn.__proto__.__proto__ === Object.prototype);
js中所有的函数都是被function构造函数创建的
常见的创建函数的方式(具名函数,匿名函数...)底层都是用Function创建的。
//2.js中所有的函数都是被function构造函数创建的
//其底层:
let fn2 = new Function('console.log(112233)'); //无参返回
let fn3 = new Function('a', 'b', 'return a+b') //有参返回
fn2();
console.log(fn3(1, 2));
instanceof运算符
关于MDN官网instanceof:instanceof - JavaScript | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof
-
与原型链相关的关键字:instanceof
-
instanceof运算符 语法: 对象 instanceof 构造函数
-
检测右边构造函数的原型在不在左边实例对象的原型链中 返回值为Boolean类型
let arr = [1, 2, 3, 4]; console.log(arr instanceof Array); //true console.log(arr instanceof Object); //true
ES6类与继承
class关键字--声明一个类函数
其实仔细看前面的一些构造函数的建造就是使用了class类中的构建。
类函数:其实就是构造函数
类函数的好处
-
把构造函数和原型包在一个大括号内部,阅读性会高
-
类函数必须使用new来调用,语法要求更有严谨性,不使用new直接报错
class必须要有一个函数叫constructor();
//ES5
//构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
//添加原型对象
Person.prototype.show = function() {
console.log('这是ES5的添加原型对象');
}
//ES6
class Person2 {
//构造函数
constructor(name, age) {
this.name = name;
this.age = age;
}
//添加原型对象
show() {
console.log('这就是新的ES6原型对象创建方法');
}
}
let p1 = new Person2('warm', 19);
console.log(p1);
show已经是原型对象之一了。
extend关键字--替换原型的方式继承
作用:继承
原理:替换原型的方式继承,但是不会覆盖原来的原型
语法:
class 子改造函数 父构造函数
//原理是替换原型方式继承 但是不会覆盖原来的原型
class Father {
constructor(housr) {
this.housr = housr;
}
show() {
console.log('father的show方法');
}
}
// console.log(f1);
class Son extends Father {
learn() {
console.log('学法律');
}
}
let s1 = new Son('xxxx小区');
console.log(s1);
看图:
不会覆盖是因为,他覆盖的其实是son原型的原型里面去覆盖了,他没有覆盖son的原型,而是覆盖了son原型的原型。
super关键字
作用:子类中调用父类的方法
原理:函数上下文调用
语法 super() super只能用于extends中
当子类继承父类,如果子类想要自己写构造函数,就必须先调用父类的super();
class Father {
constructor(housr) {
this.housr = housr;
}
show() {
console.log('father的show方法');
}
}
class Son extends Father {
constructor(name, age, hobby, housr) {
super(housr); //当子类继承父类,如果子类想要自己写构造函数,就必须先调用父类的super
this.name = name;
this.age = age;
this.hobby = hobby;
}
//原型
learn() {
console.log('学法律');
}
}