写在前面:
Javascript是一种基于对象的语言,但是在对于class的态度上其实一直表现的不冷不热,直到ES6才引入了class概念,而class概念就是把"属性"(property)和"方法"(method),封装成一个对象。
一, 生成实例对象的原始模式
如果我们要以对象的形式,声明一个猫,应该怎么做?
let Cat = {
name: "",
color: ""
}
然后,我们根据这个原型对象的规格,生成两个对象实例:
let cat1 = {}
cat1.name = "阿珍"
cat1.color = "白色"
let cat2 = {}
cat2.name = "阿强"
cat2.color = "黄色"
这样我们就创建好了, 但是问题是 我们创建的实例跟原型没有任何关系,而且如果实例多了,就很麻烦。
二, 原始模式的改进
为了解决这么麻烦的问题。。我们直接写个函数啊,是不是傻!
function Cat(name,color) {
return {
name:name,
color:color
}
}
let cat1 = Cat("阿珍", "白色")
let cat2 = Cat("阿强", "黄色")
这样解决了创建麻烦的问题,但是依然有问题。作为同一个对象原型的实例,阿珍和阿强没有任何关系。。
3, 构造模式
为了解决这个问题,js引入了构造函数。
构造函数引入了this,对构造函数用new运算符,就可以生成实例,并且this的的变量会绑定在实例对象上
function Cat(name,color) {
this.name = name
this.color = color
}
let cat1 = new Cat("阿珍", "白色")
let cat2 = new Cat("阿强", "黄色")
构造函数的实例有一个constructor属性,指向他们的构造函数。同时构造函数还有一个方法来判断实例和原型的关系
cat1.constructor // Cat
cat1 instanceof Cat // true
这样看起来是解决了之前提到的所有问题 但是新问题又来了。内存的优化问题。
如果我们在Cat中添加新的方法和属性
function Cat(name,color) {
this.name = name
this.color = color
this.type = "猫科动物"
this.eat = ()=>{
alert("吃个毛")
}
}
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃个毛
由于在原型中心添加的方法是固定的写法,所以所有新的实例都有一模一样的属性,这样的话就会导致内占用更多的内存。
那么这么来想,既然是一样的东西,有没有办法让他们指向内存的同一块区域呢? 就是说不管我调用cat1.type还是调用cat2.type都是指向同一个变量呢?
console.log(cat1.eat == cat2.eat) // false
四, Prototype
Javascript规定,每个构造函数都有一个prototype属性,这个属性是一个对象,而这个对象会被这个构造函数的所有实例继承。
这就意味着,我们可以把那些不会改变的,或者默认的属性写在prototype上,然后再生成实例
function Cat(name,color) {
this.name = name
this.color = color
}
Cat.prototype.type = "猫科动物"
Cat.prototypr.eat = () => {
alert ("吃个毛")
}
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃个毛
console.log(cat1.eat == cat2.eat) // true
五, Prototype的辅助方法
1. isPrototypeOf()
用于某个prototype和实例之间的关系
alert(Cat.prototype.isPrototypeOf(cat1)); //true
alert(Cat.prototype.isPrototypeOf(cat2)); //true
然而并不知道该怎么用。。。。
2. hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。
console.log(cat1.hasOwnProperty("name"), cat1.hasOwnProperty("type"))
// true false
3. in运算符
in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。
(也可以用来遍历属性)
alert("name" in cat1); // true
alert("type" in cat1); // true
6, 构造函数的继承
1 使用call/apply
这种方法最简单
function Animal() {
this.species = "动物"
}
function Cat(name, coloe) {
Animal.apply(this)
this.name = name
this.color = color
}
2 使用prototype
把Cat的prototype指向一个animal实例,就可以继承Animal所有的属性和方法了
但是要注意prototype中的constructor指向
function Animal() {
this.species = "动物"
}
function Cat(name, coloe) {
this.name = name
this.color = color
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
Cat.prototype.type = "猫科动物"
Cat.prototype.eat = ()=>{
alert("吃个毛")
}
这里注意一步
Cat.prototypr.constructor = Cat
因为我们用的这个方法,Cat.prototype的constructor变成了Animal,这样会导致原型链紊乱(cat1明明是用Cat构造的),所以需要手动校正。
一定要注意,我们每次这样操作的时候都要注意原型链是否正常
3 是用prototype直接继承
之前说过,一般的不变的量都是写在prototype里的,所以我们直接让被继承的把值赋给要继承的构造函数的prototype
function Animal() {
}
Animal.prototype.species = "动物"
function Cat(name, coloe) {
this.name = name
this.color = color
}
Cat.prototype = Animal.prototype;
Cat.prototype.type = "猫科动物"
Cat.prototype.eat = ()=>{
alert("吃个毛")
}
这个的优势就很大了,但是问题在于,现在Cat.prototype和 Animal.prototype现在指向同一个对象。任何对Cat.prototype的改动都会影响Animal.prototype
4 空对象中介
直接继承不行,那么到底有没有好一点的办法啊!我们回头看2
2其实没有什么很大的缺陷,那是因为2中的Cat直接继承了实例,既然继承实例会省去很多麻烦,那我们找一个空对象来接Animal的prototype然后再让cat来继承实例不就好了嘛
function Animal() {
}
Animal.prototype.species = "动物"
function Cat(name, coloe) {
this.name = name
this.color = color
}
function F() {}
F.prototype = Animal.prototype;
Cat.prototype = new F;
Cat.prototype.constructor = Cat;
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = ()=>{
alert("吃个毛")
}
F是空对象,占用内存并不多,而且修改Cat.prototype不会影响到animal
这个真是贼好用,封装个方法吧(根据大神思路封装)
function extend(Child,Parent) {
let F = function(){}
F.prototype = Parent.prototype
Child.prototype = new F
Child.prototype.constructor = Child
Child.uber = Parent.prototype
}
最后一行大神说是为了做一个父级的备份 uber是德语上层的意思
5 深拷贝
没什么说的,Animal中所有的属性放到prototype中,然后遍历拷贝
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
7, 非构造函数的继承
首先 啥是非构造函数的继承
比如说,我现在声明一个对象 叫做 “中国人”
var Chinese = {
nation: "中国人"
}
我再声明一个职业叫做 “医生”
var Doctor = {
career: "医生"
}
我现在想让 医生 继承 中国人 但是他俩又只是普通对象,这怎么办
1 object()方法
发明JSON格式的大佬提出的方法
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
是构造函数就直接继承,不是构造函数就写个构造函数让他继承
var Doctor = object(Chinese);
Doctor.career = "医生"
这样就实现了刚才想要的效果了
2 浅拷贝
浅拷贝是指只拷贝堆内存的地址
还是拿刚才的案例举例,这次用浅拷贝的方法解决
function extendcopy(p) {
let o = {}
for (var i in p) {
o[i] = p[i]
}
return o
}
var Chinese = {
nation: "中国"
}
var Doctor = extendcopy(Chinese)
Doctor.career = "医生"
这样就写完了,但是这样有个问题,那就是如果我改一下国家的数据结构
var Chinese = {
nation: "中国",
city: [
"上海","广州","深圳","北京"
]
}
然后我们再按照上面的方法进行运算
结果得出
Doctor == {
career : "医生",
nation: "中国",
city: [
"上海","广州","深圳","北京"
]
}
现在我们修改Doctor
Doctor.city.push("青岛")
Chinese == {
nation: "中国",
city: [
"上海","广州","深圳","北京","青岛"
]
}
然后我们发现Chinese中的city也变了,这就是这个方法的缺陷,也是浅拷贝的缺陷,因为浅拷贝只拷贝指针
3 深拷贝
所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用"浅拷贝"就行了。
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
8, ES6的面向对象
ES6中多了一个class声明,但是本质上这就是个语法糖,让class结构看起来更清晰,其实就是跟构造函数是一样的。
function Cat(name, color) {
this.name = name
this.color = color
}
Cat.prototype.eat = ()=>{
alert("eat")
}
class Cat {
constructor(name, color) {
this.name = name
this.color = color
}
eat() {
alert("eat")
}
}
这两个写法本质上讲是一样的。声明实例的时候也是使用new
不同点在于
1 每个class都必须有一个constructor方法,每次声明实例的时候都会运行
2 没有变量提升
3 class中的constructor方法中的 super()方法,是父类的constructor方法,所以说如果父级的constructor有参数,实例中的super()也应该传对应的参数。