JavaScript继承发展
1.设计模式
1.工厂模式
- ECMAScript中没法创建类 因此在js中使用一种函数来封装以特定接口创建对象
function createPerson(name, age) {
let obj = {};
obj.name = name;
obj.age = age;
obj.sayName = function() {
console.log('my name is ' + this.name);
}
return obj;
}
let person1 = createPerson('ljh', 21);
let person2 = createPerson('ljhh', 22);
person1.sayName();// my name is ljh
person2.sayName();// my name is ljhh
- 工厂模式虽然解决了创建多个相似对象的问题 但没有解决对象识别问题
2.构造函数模式
- ECMAScript中的构造函数可以创建特定类型的对象 如Array的原生构造函数 在运行时会自动出现在执行环境 因此可自定义创建构造函数来定义对象类型的属性方法
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
}
}
let person1 = new Person('ljh', 21);//1.
let person2 = new Person('ljhh', 22);
person1.sayName();//ljh
person2.sayName();//ljhh
console.log(person1 instanceof Person);//true 2.
console.log(person1 instanceof Object);//true
Person('ljhhh', 23);//3.
window.sayName();//ljhhh
console.log(person1.sayName === person2.sayName);//false 4.1
//4.2
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;// 存外部函数引用
}
function sayName() {
console.log(this.name);
}
person1.sayName === person2.sayName;//true
-
1.new的功能是
- 创建一个新对象
- 将构造函数的作用域赋给新对象(使this可以指向这个新对象)
- 执行构造函数(为新对象添加构造函数的属性方法)
- 返回这个新对象
-
2.创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型
-
3.构造函数与其他函数没有不同 区别仅仅是在于new调用为它们创建了独一的作用域 使this指向不同 若直接调用也是可以的 不过this就是指向global全局对象了
-
4.构造函数模式的缺点:
-
1.每个实例的方法是不同的 无法做到继承里面的复用
-
2.若是要解决的话 需要在构造函数的内部将函数定义移到全局去 构造函数内声明一个属性保存外部方法的指针
但是这样会导致 如果一个类的方法很多的时候会创建很多的全局函数 就没有封装可言了
-
3.原型模式
- ECMAScript中每一个函数都有一个prototype属性 是一个指针 指向一个对象 原型对象的prototype为它的constructor 实例的__proto__为原型的constructor
function Person() {};
Person.prototype.name = 'ljh';
Person.prototype.age = 21;
Person.prototype.sayName = function() {
console.log(this.name);
}
let person1 = new Person();
let person2 = new Person();
console.log(person1.name);//ljh
person1.sayName();//ljh
person2.sayName();//ljh
person1.name = 'ljhh';
person1.hasOwnProperty('name');//true
console.log('name' in person1);//true
Object.keys(person1);//name
person1.sayName();//ljhh
person1.name = null;
person1.sayName();//null而不是ljh
delete person1.name;
person1.sayName();//ljh
person1.hasOwnProperty('name');//false
console.log('name' in person1);//true
Object.keys(Person.prototype);//name, age
-
因为所有属性方法都在Person的原型上定义的 所以每个实例的属性方法都会是相同的 当通过对person1的name赋值后 会发现person1的name改变了 其中的实质是:
-
当new出person1和person2时 这两个实例对象本身并没有属性name和sayName 当访问name属性时 js会沿原型链往上找
过程:person1是否有name -> Person是否有name -> Object是否有name
其中找到便返回 因此当我们给person1加上了name属性后 查找就不会到Person原型上了 这就是属性屏蔽
注意:若想将实例的属性删除需要用delete 这样才会重新查找到原型后面去 如果只是置为null 会返回null而不是查找原型
-
通过person1.hasOwnProperty(prop)可以查看属性是否是来自实例的属性
-
in操作符会监测对象原型和实例 有就返回true 原理是检测对象是否能访问到此属性
-
Object.keys(obj)可以获得这个对象上的所有可枚举的所有实例属性
-
-
改造写法
function Person() {};
Person.prototype = {
constructor: Person,
//记得这里要给constrctor重新赋值,因为这里直接对prototype进行了地址上的覆盖,导致之前的默认特性消失了 同时会导致constructor变为可枚举类型 因此需要用Object.defineProperty去设置为不可枚举 若是不重新赋值的话constructor会指向Object
name: 'ljh',
age: 21,
sayName: function() {
console.log(this.name);
}
}
- 原型模式的最大缺点
- 1.首先是所有实例初始都默认为同一个值 不方便
- 2.最重要的是引用属性会共用同一个地址
function Person() {};
Person.prototype = {
constructor: Person,
name: 'ljh',
age: 21,
parents; ['mpy', 'lad'];
sayName: function() {
console.log(this.name);
}
}
let p1 = new Person();
let p2 = new Person();
p1.parents;//mpy lad
p2.parents;//mpy lad
p1.push('111');
p1.parents;//mpy lad 111
p2.parents;//mpy lad 111
//这里p1和p2的parents这个引用是引用的相同地址 所以导致一个实例更改引起另一个实例也改变
4.构造函数模式与原型模式组合
- 1.使用构造函数模式定义实例属性
- 2.使用原型模式定义方法及共享的属性
function Person(name, age) {
this.name = name;
this.age = age;
this.parents = [1,2];
}
//在prototype上加入sayName属性 可以不用替换prototype 更加简便
Person.prototype.sayName = function() {
console.log(this.name);
}
let p1 = new Person('ljh', 21);
let p2 = new Person('ljhh', 22);
console.log(p1.parents);//12
console.log(p2.parents);//12
p1.parents.push(3);
console.log(p1.parents);//123
console.log(p2.parents);//12
- 实例属性在构造函数中定义
- 所有实例共享的方法sayName在原型中定义
- 修改p1的parents不会影响p2 因为引用的是不同的数组
5.动态原型模式(使共享方法只在原型声明一次)
- 最完善的形式
function Person(name, age) {
this.name = name;
this.age = age;
this.parents = [1,2];
//当生成实例时会检测到sayName已存在 则不会生成另一份sayName
//使原型方法可以复用
if(typeof this.sayName != 'function') {
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}
2.继承
1.原型链
- 每个构造函数都有一个原型对象。原型对象又包含一个指向构造函数的指针。而实例都包含一个指向原型内部的内部指针。若让原型对象指向另一个实例,则可以形成一个链形访问结构
- 本质:用一个新类型的实例重写原型对象
function SuperType() {
this.type = 'parent';
this.name = 'ljh';
this.par = [1,2];
}
SuperType.prototype.getSuper = function() {
return this.type;
}
function SubType() {
this.type = 'child';
this.name = 'ljhh';
this.par = [3,4];
}
SubType.prototype = new SuperType();
SubType.prototype.getSub = function() {
return this.type;
}
let sub1 = new SubType();
let sub2 = new SubType();
console.log(sub1.getSuper());//ljh
//能访问到getSuper是因为在sub实例上没有,但是在sub的原型的prototype上的SuperType实例上有这个方法,因此可以访问到
//给子类型添加方法或更改父类型方法时一定要在父类型实例对象覆盖子类型的prototype之后写,不然之后会被覆盖
sub1.par;//3,4
sub2.par;//3,4
sub1.par.push(5);//3,4,5
sub2.par;//3,4,5
//SubTyped的所有实例都会共享一个引用
- 缺点:子类的所有实例都会共享一个引用
2.借用构造函数
- 在子类型的构造函数中调用父类型的构造函数,通过call或apply改变上下文
function SuperType() {
this.name = 'ljh';
this.par = [1,2];
}
function SubType() {
SuperType.call(this, 'ljhh');
this.age = 21;
}
let s1 = new SubType();
let s2 = new SubType();
console.log(s1.par);//12
console.log(s2.par);//12
s1.par.push(3);
console.log(s1.par);//123
console.log(s2.par);//12
console.log(s1.name);//ljhh
console.log(s1.age);//21
- 这样不会导致引用相同是因为在新的subType对象上执行SuperType()函数中定义的所有对象初始化代码因此每个实例都有自己的par副本
- 相较于原型链模式,这个方式可以向父类构造函数中传递参数
- 缺点:方法都在构造函数中 无法复用
3.组合继承
- 使用原型链实现原型属性及方法的继承
- 使用借用构造函数模式实现对实例属性的继承 可保证在原型上定义方法实现方法复用,也保证了每个实例都有自己的属性
function SuperType() {
this.name = 'ljh';
this.par = [1,2];
}
SuperType.prototype.sayName = function() {
return this.name;
}
function SubType(name, age) {
SuperType.call(this, name);//1
this.age = age;
}
SubType.prototype = new SuperType();//2
SubType.prototype.sayAge = function() {
return this.age;
}
let s1 = new SubType('ljh', 21);
let s2 = new SubType('ljhh', 22);
console.log(s1.par);//12
console.log(s2.par);//12
s1.par.push(3);
console.log(s1.par);//123
console.log(s2.par);//12
console.log(s1.sayName());//ljh
console.log(s1.sayAge());//21
console.log(s2.sayName());//ljhh
console.log(s2.sayAge());//22
- 会调用两次父类构造函数 一次在call 一次在创建原型
4.原型式继承(Object.create的前身)
- 借助原型可以基于已有的对象创建新对象,同时不创建自定义类型
function create(obj) {
function Foo() {};
Foo.prototype = obj;
return new Foo();
}
const person = {
name: 'a',
par: [1,2]
}
let p1 = create(person);
p1.par.push(3);
let p2 = create(person);
p2.par.push(4);
person.par;//1 2 3 4
- 相当于对传入的对象进行了一次浅复制
5.寄生继承
- 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象
function createAnother(obj) {
let clone = Object.create(obj);
clone.say = function() {
return 'hello';
}
return clone;
}
6.寄生组合继承
- ES6使用的方式 至今最完美的继承
- 只调用一次父类构造函数
function Parent() {
this.name = [1]
}
Parent.prototype.reName = function() {
this.name.push(2)
}
function Child() {
Parent.call(this) // 生成子类的实例属性(但是不包括父对象的方法)
}
Child.prototype = Object.create(Parent.prototype) // 该方法会使用指定的原型对象及其属性去创建一个新的对象 但不会再次调用父类的构造函数
var child1 = new Child()
var child2 = new Child()
child1.reName()
console.log(child1.name, child2.name) //[ 'super4','super41' ] [ 'super4' ], 子类实例不会相互影响
console.log(child1.reName === child2.reName) //true, 共享了父类的方法