面向对象的三大特征、封装、继承、多肽,
js
中同样有这三种特征,js
是一门弱语言,俗称解释性语言,通常来说比起高级语言,他没有严格的类型约束,为了让代码写得更健壮,维护性更强,因此有了ts
约束,而继承
是能让代码更加通用,让你的代码更加的抽象。
往往在项目中都会看到有用class
,或者OOP
思想去组织业务代码,本篇只做项目中常用到的继承以及对不同继承方式的回顾,也是再次加深对继承的一些理解,希望你在项目中有些帮助和思考。
正文开始...
构造函数
我们通过构造函数构建对象
function Animal(name) {
this.name = name;
this.getName = function () {
return this.name;
};
}
const tigger = new Animal('tigger');
const cat = new Animal('cat');
我们通过new 构造函数()
方式新建了两个对象tigger
、cat
,其实我们会发现,相当于有多少对象,我就要实例化多少个对象。并且实例化的对象都相互独立,互不影响。现在我想trigger
与cat
拥有同样的属性或者方法呢?
可以利用原型链prototype
共享方法,
...
Animal.prototype.say = function() {
console.log('hello,'+this.name);
}
cat.say(); // hello, cat
tigger.say(); // hello,trigger
当使用new Animal('cat')
或者new Animal('tigger')
,你会发现同样的事情,我们实例化了多次,因为这样做,tigger
与cat
并不相等,那么如何可减少内存开销呢。
我们可以利用单件模式
一个全局变量去处理,举个例子
let instance;
function Animal(name) {
this.name = name;
this.getName = function () {
return this.name;
};
if (!instance) {
instance = this;
}
this.getInstance = function () {
return instance;
};
}
const cat = new Animal('cat').getInstance();
const trigger = new Animal('trigger').getInstance();
console.log(cat === trigger); // true
或者在构造函数上绑定一个静态属性,这样比定义全局变量要好得多,推荐下面这种方式
function Animal(name) {
this.name = name;
this.getName = function () {
return this.name;
};
if (!Animal.instance) {
Animal.instance = this;
}
return Animal.instance;
}
const cat = new Animal('cat');
const trigger = new Animal('trigger');
console.log(cat === trigger); // true
但是这样我们会发现const trigger = new Animal('trigger')
实际上无论实例化多少个,都只会返回首次实例化的对象,对于不同场景还是得特殊处理。
自定义一个数组,完全继承数组
所有特性
function MyArray() {
this.ret = [];
}
MyArray.prototype = new Array();
// 指定构造函数
MyArray.prototype.constructor = MyArray;
var mine = new MyArray();
console.log(mine instanceof Array); // true
// 以上等价
MyArray.prototype.isPrototypeOf(mine); // true
constructor
查找对象的构造函数
function Print() {}
const print = new Print();
console.log(Print.prototype.constructor === print.constructor); // true
console.log(Object.getPrototypeOf(print) === Print.prototype); // true
判断print
的构造函数是不是Print
...
print instanceof Print // true
也可以用这个来代替
...
Print.prototype.isPrototypeOf(print); // true
原型继承法
所有对象共享一个原型对象,基于构建器工作模式,将父类的prototype
直接赋值给子类的prototype
// 父构造函数
function Parent() {
this.ParentName = 'parent';
}
Parent.prototype.cname = '123';
Parent.prototype.getName = function () {
console.log(this.cname); // 666
};
// 子构造函数
function Child() {
this.childname = 'childname';
// this.ParentName = '888';
}
Child.prototype = Parent.prototype;
// Child.prototype.cname = '666'; 会修改父类的cname
const c = new Child();
console.log(c.ParentName, c.childname, c.cname);
// undefined childname 123
从打印里我们可以看出,子类可以访问父类prototype
上的属性或者prototype
方法,但是父类自身属性
或者自身方法
不能访问,但是,我们注意到如果子类prototype
属性有父类相同的prototype
属性名时,此时子类会覆盖父类prototype
的属性。子类自身属性与父类自身属性同名时,此时子类访问就会有值,访问的是自身属性,c.ParentName
打印就会是888
于此同时子类prototype
修改会同时修改父类的prototype
临时构造器
现在我有一个需求,子类只继承父类的prototype
,不需要继承父类自身本身的属性,举个栗子佐证下
function extends(Child, Parent) {
const F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
// 将Child的构造函数指定成Child
Child.prototype.constructor = Child;
}
function Parent() {
this.parentName = '123'
}
Parent.prototype.age = 18;
function Child() {
this.childName = 'childname'
}
// Child.prototype.age = 666; // 并不会修改父类age属性
extend(Child, Parent);
const c = new Child();
console.log(c.age, c.childName, c.parentName)
// 18, childname,undefined
我们可以发现实际上利用extends
方法,利用了一个中间的F
构造函数,通过F.prototype = Parent.prototype
,然后将Child.prototype = new F()
,与上面原型继承不同的是,修改子类prototype
与父类相同的属性时,并不会修改父类prototype
的属性。本质上就是借鸡生蛋,借用了F
的prototype
,不直接修改父类的prototype
原型属性拷贝继承
将父类的prototype
属性值拷贝给子类
function extends(Child, Parent) {
const c_proto = Child.prototype;
const p_proto = Parent.prototype;
for (let key in p_proto) {
c_proto[key] = p_proto[key]
}
}
function Child () {
this.name = 'child'
}
function Parent() {
this.name = 'parent'
}
Parent.prototype.money = 100;
extends(Child, Parent);
const c = new Child();
console.log(c.money, c.name) // 100, child
注意,只会继承父类prototype
属性,父类自身属性并不会继承,因此这种与临时构造器
功能上如出一辙,子类并不能修改父类自身的属性。
寄生继承
function extends2(target) {
const F = function () {};
F.prototype = target;
return new F();
}
function Parent() {
this.name = 'parent';
}
Parent.prototype.age = 100;
const child = extends2(Parent.prototype);
const parent = new Parent();
// child.__protototype__.age = 88;
console.log(child.age, child.name); // 100, undefined
console.log(parent.age, parent.name); // 100, parent
这种继承本质上仍然是用利用父类的prototype
赋值给了一个中间构造函数F
的prototype
,他的弊端是并不能访问父类的自身属性与自身方法, 但是child.__protototype__.age
会修改父类的prototype
上的同名属性。
构造函数继承,利用 call 继承【构造器继承】
// 父构造函数
function Parent() {
this.name = 'parent';
this.say = function () {
console.log('hello,' + this.name);
};
}
Parent.prototype.age = 10;
// 子构造函数
function Child() {
Parent.call(this);
}
const c = new Child();
console.log(c.name); // parent
console.log(c.age); // undefined
console.log(c.say()); // hello parent
我们注意到c.age
返回的是undefined
,因为age
不是构造函数本身的属性或者方法,在构造函数prototype
的方法或者属性无法访问,如果我需要访问呢?
function Parent() {
this.name = 'parent';
this.say = function () {
console.log('hello,' + this.name);
};
}
Parent.prototype.age = 10;
function Child() {
Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.age = 888;
const c = new Child();
const p = new Parent();
console.log(c.age); // 888
console.log(c.name); // parent
console.log(p.age); // 10
我们就加了一行代码实现了Child.prototype = Object.create(Parent.prototype)
,这种方式子类与父类的耦合非常低,子类修改与父类同名prototype
的属性并不会影响父类。
原型链继承
实际上还有一种更简单的继承,让子类的prototype
等于父类的实例
,也称为原型链
继承
function Parent() {
this.name = 'parent';
this.say = function () {
console.log('hello,' + this.name);
};
}
function Child() {}
Child.prototype = new Parent();
const c = new Child();
const p = new Parent();
console.log(c.name); // 'parent'
多重继承
function A() {
this.a = 11;
}
function B() {
this.b = 22;
}
function C() {
A.call(this);
B.call(this);
}
C.prototype = Object.create(A.prototype);
C.prototype.constructor = C;
// 合并B的prototype
Object.assigin(C.prototype, B.prototype);
const c = new C();
extends 继承
class Parent {
constructor() {
this.name = 'Maic';
}
getName() {
return this.name;
}
}
class Child extends Parent {
constructor() {
super();
this.age = 10;
}
}
const c = new Child();
console.log(c.name); // Maic
console.log(c.getName()); // Maic
console.log(c.age); // 10
注意constructor
中有super()
调用
构造函数的变体,es6 的 class
// utils.js
class Utils {
static instance = null;
formateDate() {}
formateUrl() {
console.log('formateUrl');
}
static getInstance() {
if (!this.instance) {
this.instance = new Utils();
}
return this.instance;
}
}
export default Utils.getInstance();
引入utils.js
import Utils from './utils';
console.log(Utils.formateUrl());
总结
1、obj instanceof A
判断一个对象的构造函数(A 是否是 obj 的构造函数),如果是则返回true
、不是返回false
2、A.prototype.isPrototypeOf(obj)
判断构造函数A
是不是obj
实例对象的构造函数
3、常用的几种继承、原型继承法
、临时构造器
、原型属性拷贝继承
、寄生继承
、构造器继承【call】
、原型链继承
、extends继承
4、call
父类构造函数在子类构造函数调用call
实现继承,父类除了了自身属性和自身方法能被继承访问,父类原型的方法子类无法访问
5、Child.prototype = Object.create(Parent.prototype)
实现继承父类