理一理js中让人抓狂的对象 原型 继承

其实要总结这几个概念已经很久了,只是之前一直都觉得自己还不算完全掌握,而且知识点还不够系统,所以一直拖着,但是最近又重新看了几篇文章,自己也测试了一下,觉得开始有些清晰了,所以想在这里给自己做个总结吧,也希望在学的你们能够在这里学到一点东西。不要急躁,慢慢看,一边看一边做测试,这也是我最近的感悟。看了不一定会,要真正自己动手去测试一下。

什么是对象?

我的理解就是这是一个存储灌,你可以在里面存储任何东西,这些东西就是我们之前学的各种js里面的数据类型,然后给每一个名字贴上一个名字,方便我们以后找到。

例子:

//这个myFirstObject里面有两个属性,分别是firstName和 favoriteAuthor
var myFirstObject = {firstName: "Richard", favoriteAuthor: "Conrad"};

如何定义一个对象?

  • 对象字面量
  • 构造函数创建
  • 原型模式创建
对象字面量创建对象

这是最原始的方法,但是也不利于后面的多个对象的创建。

//这是一个mango对象,这个对象里面有color shape sweetness属性以及一个​howSweetAmI的方法
​var mango = {
color: "yellow",
shape: "round",
sweetness: 8,
​
​howSweetAmI: function () {
console.log("Hmm Hmm Good");
}
}
缺点:这种方法虽然简单明了,但是试想一下,如果我们要定义各种各样的水果对象,每一个水果都有color shape sweetnees的属性,我们都要一个个定义是不是会有点麻烦呢?

那看看下面这种构造函数的创建方法

考虑用构造函数的创建方法

构造函数创建方法,就是定义一个构造函数,然后在里面设置属性和方法值,然后再用new去实例化对象,所有实例化的对象都会有构造函数里面的属性和方法。

//在这里定义一个构造函数,在构造函数里面定义属性和方法,注意这里需要用this,后面就可以通过new来实例化对象,使用new的时候,就会将this指向这个实例化的对象。

function Fruit (theColor, theSweetness, theFruitName, theNativeToLand) {
​    this.type = "水果"
    this.color = theColor;
    this.sweetness = theSweetness;
    this.fruitName = theFruitName;
    this.nativeToLand = theNativeToLand;
​
    this.showName = function () {
        console.log("This is a " + this.fruitName);
    }
​
    this.nativeTo = function () {
    this.nativeToLand.forEach(function (eachCountry)  {
       console.log("Grown in:" + eachCountry);
        });
    }

}

接下来,我们就可以直接用new的方法来创建各种各样的水果对象了。

//创建一个芒果的对象。
var mangoFruit = new Fruit ("Yellow", 8, "Mango", ["South America", "Central America", "West Africa"]);
mangoFruit.showName(); // This is a Mango.​
mangoFruit.nativeTo();
​//Grown in:South America​
​// Grown in:Central America​
​// Grown in:West Africa​
​
//创建一个pineappleFruit的对象。
​var pineappleFruit = new Fruit ("Brown", 5, "Pineapple", ["United States"]);
pineappleFruit.showName(); // This is a Pineapple.

是不是很方便,可以把构造函数想象成一个大工厂,然后你只要使用new的方法去调用这个工厂,就相当于告诉这个工厂给我生产一个东西出来,那么这个工厂就会用所有自己有的设备,把它所有的东西能生产的都生产出来。所以只要在这个工厂上的设备能生产出来的都会被生产。

再来思考一个问题,这些实例化对象之间是不是其实都是有相似性的,就是你可以提炼出其中相同的属性和方法。像上面那个例子,所有水果的type属性和showName方法是不是都是一样的呢?那我们是不是可以用原型来写?

什么是原型?prototype

js中每一个函数都会有自己的一个原型对象,这个原型对象叫做prototype.而所有通过这个构造函数实例化的对象都会指向这个原型。其实你可以设想一下,构造函数是工厂的话,原型其实是不是可以是仓库,所有实例化的对象就可以从仓库里面拿东西。所以我们可以把所有对象公用的属性和方法给放在prototype下面,这样就可以避免属性和方法的重复定义。下面用一个例子和图来说明一下。

//这里我们使用原型来创建对象,所有对象共用的属性和方法就放在prototype上。
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

// 通过原型模式来添加所有实例共享的方法
// sayName() 方法将会被Person的所有实例共享,而避免了重复创建
Person.prototype.sayName = function () {
  console.log(this.name);
};

var person1 = new Person('Weiwei', 27, 'Student');
var person2 = new Person('Lily', 25, 'Doctor');
person1.sayName(); // Weiwei
person2.sayName(); // Lily

实例化的对象中的name age job属性是从构造函数那得到的,而实例化的对象的原型指向了构造函数的原型对象,所以也会有sayName方法。

image.png

//注意,这里是输出true,所以其实person1和person2的sayName方法都是同一个,来自同一个地址。

console.log(person1.sayName === person2.sayName); // true

小小的总结一下:

对象有三种不同的创建方式,对象字面量,构造函数,结合原型来创建,最有效的也就是第三种创建方式了,避免相同属性和方法的重复创建,所以可以将对象公用 的属性和方法定义在prototype上。

!!!!注意!!!!

如果使用原型继承的话,如果有多个对象和属性要同时一起定义的话,需要注意将原型prototype的constructor属性重新赋值,是不是听不懂了,别急,先看第一个例子,再看我们后面改进的。

例子1

//这是我们定义水果的属性和方法
function Fruit () {
​
}
​//一个一个使用Fruit.prototype来一一定义各个属性和方法。
Fruit.prototype.color = "Yellow";
Fruit.prototype.sweetness = 7;
Fruit.prototype.fruitName = "Generic Fruit";
Fruit.prototype.nativeToLand = "USA";
​
Fruit.prototype.showName = function () {
console.log("This is a " + this.fruitName);
}
​
Fruit.prototype.nativeTo = function () {
            console.log("Grown in:" + this.nativeToLand);
}

上面的方法虽然也是可行的,但是如果属性和方法太多的话,是不是太低效了。

更简单的原型创建方法:

function Fruit () {
​
}
​//一个一个使用Fruit.prototype来一一定义各个属性和方法。
Fruit.prototype= {
//这里一定要将prototype的constructor属性重新指向Fruit。因为我们这样相当于是重写了prototype的值。
constructor: Fruit,
color = "Yellow";
sweetness = 7;
fruitName = "Generic Fruit";
showName = function () {
console.log("This is a " + this.fruitName);
}
nativeTo = function () {
            console.log("Grown in:" + this.nativeToLand);
}
}

上面的例子看懂了吗?就是每一个构造函数的prototype属性都会自带有一个constructor属性,这个constructor属性又指向了构造函数,所以我们像上面那样定义的时候,也要将这个constructor属性给重新指向构造函数。(可以重新看一下上面我给出的那个图)

如何读取对象的属性:

// We have been using dot notation so far in the examples above, here is another example again:​
​var book = {title: "Ways to Go", pages: 280, bookMark1:"Page 20"};
​
​// To access the properties of the book object with dot notation, you do this:​
console.log ( book.title); // Ways to Go​
console.log ( book.pages); // 280


//当然,也可以用方括号来写:
console.log ( book["title"]); //Ways to Go​
console.log ( book["pages"]); // 280​

如何实现对象的继承:

  • 原型继承
  • 构造函数继承
  • 原型和构造函数继承
  • 创建空对象方法

原型继承:

  • 构造函数都有一个指向原型对象的指针
  • 原型对象都有一个指向构造函数的constructor
  • 实例化对象都有一个指向原型的[[prototype]]属性
function Father () {
  this.fatherValue = true;
}

Father.prototype.getFatherValue = function () {
  console.log(this.fatherValue);
};

function Child () {
  this.childValue = false;
}

// 实现继承:继承自Father
Child.prototype = new Father();

Child.prototype.getChildValue = function () {
  console.log(this.childValue);
};

var instance = new Child();
instance.getFatherValue(); // true
instance.getChildValue();  // false

上面的关键点就是用```Child.prototype = new Father();


![image.png](http://upload-images.jianshu.io/upload_images/5763769-c4014978c0314834.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

可以看一下这一个原型链的一个搜索的过程:

var instance = new Child();
instance.getFatherValue(); // true
instance.getChildValue(); // false

当我们查找```instance.getFatherValue(); ```的时候,是如何一个查找的过程呢?

- 先看一下instance 实例上有没有,没有则继续
- Chile prototype上查找有没有,也没有该方法,则继续向上查找
- 向上查找的是Father prototype的属性和方法,查找到了,则输出。

>这种原型继承的方法,其实就相当于延长了Child的原型链,因为其原型现在又可以再向上查找到Father的原型,相当于延长原型链之后可以继续再向上去查找到Father原型上的属性和方法。

#####思考一下:这其实也给了我们一个提示,如果实例,原型上有相同的方法的话,我们一般读取该属性的时候,也是直接读取到了实例上的属性和方法,除非实例本身没有,才会继续往上查找。

####缺点:
这个方法其实也是有缺点的,因为Child的实例化对象的一些属性和方法都是在该原型链上查找的,所以一些引用值得修改也会影响到所有实例化对象的属性,先看个例子。

function father(name,age) {
this.name = name
this.age = age
this.friends = ["lili","koko"]
}
father.prototype.sayname = function () {
console.log(this.name)
}
function children(school) {
this.school = school
}
children.prototype = new father()
children.prototype.sayname = function () {
console.log("我就是不说自己的名字")
}
var instance = new children("幼儿园")
var instance2 = new children("幼儿园")
//这里我们修改了instance的friends的值
instance.friends.push("yoyo")
//我们输出children的两个实例对象试一下,看看两个的属性值的区别
console.log(instance)
console.log(instance2)



![instance的输出.png](http://upload-images.jianshu.io/upload_images/5763769-2bbc0a638ee61a39.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![instance2的输出.png](http://upload-images.jianshu.io/upload_images/5763769-b2e3d6d0c8f39176.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

其实从上面两个图也可以发现,一旦修改了一个实例对象上的一个引用值,其他实例化对象的属性值也跟着变化了。因为这里的friends是引用类型的数据,所有的实例都会共享这个属性值,一旦修改其他也跟着修改了。

####构造函数继承


function Animal(){
    this.species = "动物";
  }
Animal.prototype.say = function(){console.log("hahaha")}
 function Cat(name,color){
//这里使用的是构造函数的继承,调用Animal构造函数,再用apply将this指向Cat本身
    Animal.apply(this, arguments);
    this.name = name;
    this.color = color;
  }
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物
//这样的话Cat的实例化对象就都有Animal的属性了。


>//Cat这个实例化对象就有Animal的属性,但是不会继承来自于Animal原型上的方法。

![image.png](http://upload-images.jianshu.io/upload_images/5763769-49c23d31a71c5e79.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

>构造函数的好处是可以在调用的时候输入参数,```Animal.apply(this, arguments);
```这里可以重新将Cat的参数赋值给Animal中的构造函数。但是这样其实还是有不好之处就是每次新生成一个实例化对象的时候,就会调用一次构造函数。除此之外,Cat并不能继承来自于Animal原型上的方法,这不能实现方法上的复用。

所以,我们可以考虑结合原型方法和构造函数方法。

刚刚是不是说到,只使用原型方法的话,继承父类的所有属性和方法,但是所有实例没有自己的属性,可能会因为一个实例的属性的更改而影响到其他实例;而构造函数的方法只能实现构造函数内的属性方法继承,不能实现父类原型上的继承;;

那就结合这两种方法来实现以下;

// 父类构造函数
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}

// 父类方法
Person.prototype.sayName = function () {
console.log(this.name);
};

// --------------

// 子类构造函数
function Student (name, age, job, school) {
// 继承父类的所有实例属性(获得父类构造函数中的属性)
Person.call(this, name, age, job);
this.school = school; // 添加新的子类属性
}

// 继承父类的原型方法(获得父类原型链上的属性和方法)
Student.prototype = new Person();

// 新增的子类方法
Student.prototype.saySchool = function () {
console.log(this.school);
};

var person1 = new Person('Weiwei', 27, 'Student');
var student1 = new Student('Lily', 25, 'Doctor', "Southeast University");

console.log(person1.sayName === student1.sayName); // true

person1.sayName(); // Weiwei
student1.sayName(); // Lily
student1.saySchool(); // Southeast University



![image.png](http://upload-images.jianshu.io/upload_images/5763769-508d69653dfb5c9f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

这个就是比较好的继承方法,将父类的属性继承过来,所有的实例都有自己的属性,同时将原型上的方法也继承过来,实现所有实例都有公共的属性和方法。当然,细心的你也许已经发现了,就是这个Student子类的原型上除了有saySchool方法之外,还有父类构造函数内的那些name job age属性,那是因为我们是使用```Student.prototype = new Person();```来实现继承的,所以该原型实际上就是Person的实例;

所以其实这个方法虽然是好,但是也会出现这样一个情况,属性的覆盖,原型上还有对应父类的属性。这也不是我们最初想要的结果。

所以,我们又引入了另外一个方法

####利用中间空对象的方法继承。
>什么意思呢?我们上面的结合原型和构造函数的方法之所以会出现原型上还有相同的属性的问题是因为,我们用```Student.prototype = new Person();```来实现继承,相当于把Student.prototype重新赋值成Person的实例了,我们就肯定会有Person 构造函数上的属性和原型上的方法。那么我们要的最理想的状态就是用```Student.prototype = new Person();```的时候,Person的构造函数上没有属性,但是这显然不够理智,那么我们就可以引入一个中间的空对象,来实现继承。
啊啊啊,还是看例子吧。

//如果这样子的话,是不是很完美,Child的原型是F的一个实例,而F的构造函数我们是设置成空的。
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();

>所以我们可以用这样的方式来封装起来以后可以使用‘

//这个就是Child继承Parent的方法。
function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
  }


我们再来写个例子吧;

// 父类构造函数
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}

// 父类方法
Person.prototype.sayName = function () {
console.log(this.name);
};

// --------------

// 子类构造函数
function Student (name, age, job, school) {
// 继承父类的所有实例属性(获得父类构造函数中的属性)
Person.call(this, name, age, job);
this.school = school; // 添加新的子类属性
}

function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
  }
extend( Student,Person); //调用该方法,实现继承父类原型链上的属性和方法;

// 新增的子类方法
Student.prototype.saySchool = function () {
console.log(this.school);
};

var person1 = new Person('Weiwei', 27, 'Student');
var student1 = new Student('Lily', 25, 'Doctor', "Southeast University");

console.log(person1.sayName === student1.sayName); // true

person1.sayName(); // Weiwei
student1.sayName(); // Lily
student1.saySchool(); // Southeast University
console.log(student1)


![image.png](http://upload-images.jianshu.io/upload_images/5763769-e762216f5426ad1e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

>这样继承是不是好多了,至少跟前面的例子相比,我们的原型链上不会再继承来自父类上的属性;



>后面还有方法会继续总结的,今天先写到这里好了,感觉自己写的过程真的会发现很不一样,也算是了解多了一些。


参考链接:
http://javascriptissexy.com/javascript-objects-in-detail/#
http://javascriptissexy.com/javascript-prototype-in-plain-detailed-language/#
http://javascriptissexy.com/oop-in-javascript-what-you-need-to-know/#
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance.html

推荐阅读更多精彩内容