持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情
1、经典继承
最开始的继承方式:
function Father() {
this.colors = ['red','blue'];
}
function Son() {}
Son.prototype = new Father();
let obj = new Son();
console.log(obj);
我们发现,这个对象身上没有任何属性,正常,但是它的原型链上的属性继承的是Father身上的属性,所以有colors。
继承步骤:
- new Father()生成一个对象,这个对象有有一个属性:colors。
- 将这个对象赋值给Son.prototype(赋值的是地址)
- Son.prototype这个对象被改写了
- 原始Son.prototype是一个这样的对象{constructor:Son(),[[prototype]]:Object.prototype}
- 现在Son.prototype是这样的{colors:['red','blue'],[[prototype]]:Father.prototype}
这就相当于创建了
Son.prototype.colors属性。最终所有Son的实例都会共享这个colors属性。
但是这有一个问题,原型中的引用值被共享并不安全:
function Father() {
this.colors = ['red','blue'];
}
function Son() {}
Son.prototype = new Father();
let obj1 = new Son();
let obj2 = new Son();
obj1.colors.push('black');
console.log(obj1.colors); // ['red','blue','black']
console.log(obj2.colors); // ['red','blue','black']
同时,Son.prototype丢失了constructor属性,需要我们手动添加。
...
Son.prototype = new Father();
Son.prototype.constructor = Son;
...
解决办法:使用‘盗用构造函数’ ----> 经典继承
function Father() {
this.colors = ['red','blue'];
}
function Son(name) {
// 继承Father
Father.call(this);
// 自己的属性
this.name = name;
}
let obj1 = new Son('ly');
let obj2 = new Son('jack');
obj1.colors.push('black');
console.log(obj1); // Son {colors: Array(3), name: 'ly'}
console.log(obj2); // Son {colors: Array(2), name: 'jack'}
缺陷:子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式,也就是方法只能定义在构造函数内,但是方法定义在构造函数内,会造成内存的浪费。
2、组合继承
== 原型链+经典继承
function Father(name) {
this.name = name;
this.colors = ['red','blue'];
}
Father.prototype.sayName = function() {
console.log(this.name)
}
function Son(name,age) {
// 继承Father
Father.call(this,name);
// 自己的属性
this.age = age;
}
//继承方法
Son.prototype = new Father();
Son.prototype.constructor = Son;
//自己的方法
Son.prototype.sayAge = function() {
console.log(this.age)
}
let user1 = new Son('ly',18);
user1.colors.push('black');
console.log(user1.colors); // ['red', 'blue', 'black']
user1.sayName(); // ly
user1.sayAge(); // 18
let user2 = new Son('jack',20);
console.log(user2.colors); // ['red', 'blue']
user2.sayName(); // jack
user2.sayAge(); // 20
3、原型继承
使用原型链继承
使用 Son.prototype.__ proto __ = Father.prototype 表示改变构造函数的原型指向.
// 初始化对象,在prototype原型上定义show方法
function User() {}
User.prototype.show = function() {
console.log("user-metheds-show");
};
function Age() {}
// 默认方法的prototype里面的__proto__指向Object.prototype
console.log(Age.prototype.__proto__ == Object.prototype); // true
// 改变Age的prototype的父级原型指向
/*
改变原型的父级指向称之为继承
好处是保留了自己本身的原型,同时又继承了其他原型
*/
Age.prototype.__proto__ = User.prototype;
console.dir(Age);
// 这个是候往Age的原型上添加方法不会影响User的原型
Age.prototype.ageshow = function() {
console.log("Age-metheds-show");
};
// 实例化两个构造函数
let u = new User();
let a = new Age();
// u可以正常使用User原型中的show方法
u.show(); // user-metheds-show
// u.ageshow(); 此行代码报错,因为User没有继承Age的原型,所以不存在ageshow方法
/*
由于原型继承了User的原型,所以用show方法没有问题
同时自己的原型上的方法没有受到影响是,所以也可以用ageshow方法
*/
a.show(); // user-metheds-show
a.ageshow(); // Age-metheds-show
没有改变Age构造函数的原型(prototype)。
使用Object.create()继承
- Object.create 是重新创建一个原型,在实例化之后对函数进行 Object.create 操作不会改变实例化变量的原型指向
// 使用Object.create()也可以继承其他方法的原型
function Info() {}
let i = new Info();
// Info.prototype = Object.create(User.prototype); //Uncaught TypeError: i.show is not a function
Info.prototype.__proto__ = User.prototype; // 改变原型指向,
i.show(); // user-metheds-show
-
使用Object.create()继承原型前就实例化的构造函数,会导致实例化指向的原型还是继承之前的原型
- 继承之前原型中不包含show方法,所以i.show()会报错
-
但是使用Info.prototype.proto = User.prototype之前实例化不会报错
- 因为Info.prototype.proto = User.prototype是改变原型的指向, Object.create是重新创建一个原型
缺陷
- 影响一:constructor 归属问题
// 声明User构造函数
function User() {}
// 给User的原型添加show方法
User.prototype.show = function() {
console.log("User show");
};
// 声明Info构造函数
function Info() {}
// 使用Object.create()方法继承User的原型
Info.prototype = Object.create(User.prototype);
// 再打印User的结构
console.log(User.prototype); // {show: ƒ, constructor: ƒ}
// 这个时候打印Info的结构
console.log(Info.prototype); // {}
上面代码中 Info 的 prototype 原型中的 constructor 没有了,就是因为使用 Object.create 过程中吧 Info 的 constructor 弄丢了,此时去实例化两个构造函数,分别打印出实例化变量的 constructor,发现打印的结果都是 User
// 在实例化上面两个构造函数
let u = new User();
let i = new Info();
// u 打印的结果为User
console.log(u.constructor); // User
// i 正常情况下应该打印Info,但是是由于Object.create继承时没有指定constructor,导致没有正确打印
console.log(i.constructor); // User
通过手动指定 Info 的 constructor 为 Info
// 分析情况后给Info添加constructor = Info
Info.prototype.constructor = Info;
// 再次打印现在正确返回
console.log(i.constructor); // Info
- 影响二:constructor 可被遍历问题
// 使用 for in 遍历 i
/*
由于for in 的特点可以遍历到原型中的数据
但是我们发现遍历i的时候吧constructor也给遍历了出来
为了不遍历constructor,可以在设置constructor的时候吧它设置为不可遍历
*/
for (const key in i) {
console.log(key);
// constructor
// show
}
// 先查看一下Info的属性
/*
分析发现Info.prototype.constructor的enumerable属性为true
意思为可被遍历
那么可以在设置这个值得时候直接设置为不可遍历
*/
console.log(
JSON.stringify(Object.getOwnPropertyDescriptors(Info.prototype), null, 2)
);
// 修改如下
Object.defineProperty(Info.prototype, "constructor", {
// 设置值
value: Info,
// 设置不可遍历
enumerable: false,
});
// 再次for in
/*
结果不在打印constructor,设置成功
*/
for (const key in i) {
console.log(key); // show
}
4、方法重写和父级属性访问
// 定义基础函数
function User() {}
User.prototype.show = function() {
console.log("User-show");
};
User.prototype.site = function() {
return "User-site";
};
// 定义Info继承User
function Info() {}
Info.prototype.__proto__ = User.prototype;
// 实例化Info
let i = new Info();
// 这个时候调用show方法来自User的原型
i.show(); // User-show
// 在Info中重写show方法
Info.prototype.show = function() {
//使用 User.prototype.site() 调用继承的函数中的方法
console.log(User.prototype.site() + "Info-show"); // User-siteInfo-show
};
// 重写完成后调用show方法变成了Info原型中的方法
i.show(); // User-siteInfo-show
推荐
使用原型继承的Son.prototype.__ proto __ = Father.prototype加上经典继承(盗用构造函数);
// 声明User构造函数,接收name和age
function User(name, age) {
this.name = name;
this.age = age;
}
// 在原型上添加show方法
User.prototype.show = function() {
console.log(`${this.name},${this.age}`);
};
function Admin(...args) {
// 引用构造函数User,通过apply改变User的this指向
// 实现初始化属性在User当中完成
User.apply(this, args);
}
Admin.prototype.__proto__ = User.prototype;
let a = new Admin("李四", 18);
a.show(); // 李四,18
// 引用父类构造函数,复用了初始化的代码
function Info(...args) {
User.apply(this, args);
}
Info.prototype.__proto__ = User.prototype;
let i = new Info("张三", 19);
i.show(); // 张三,19
总结
关于JS的继承,其实就是某个构造函数,改变它原型的原型链,让其指向另一个构造函数的原型,以此达到当在本函数的原型上找不到方法或者属性时,可以沿着原型链向另一个构造函数的原型上查找。以此达到继承的目的。