ES5的面向对象
这是一个老生常谈的问题。JS的面向对象与大多数的强类型语言(C++/Java)不尽相同。通过类可以创建任意多个具有相同属性的方法和对象。但是JS中没有类的概念,所以它的对象与基于类的对象有所不同。
JS中的对象
- 首先我们先理解什么是对象。有一句话叫做:JS的一切都是对象。这句话还是有那么点道理。JS中算上ES6的Symbol就有7中类型(Number , null ,undefined String boolean Symbol 和对象),其中除了object以外都是原始类型。很多情况下原型类型可以通过对应的类转为对象。
然后我们看一下对象的创建
// 构造函数的创建方法
var obj = new Object();
// 对象字面量的创建
var person = {
name : 'kobe',
age : 40
}
复制代码
但是如果我们创建很多对象的时候就会产生大量的代码冗余。这是工厂模式横空出世
工厂模式
function factoryPerson(name,age){
var obj = new Object()
obj.name = name;
obj.age = age;
return obj;
}
var kobe = factoryPerson('kobe','40');
var durant = factoryPerson('durant',31);
复制代码
这样虽然能无数次的调用工厂函数,但是我们却不知这个工厂是代表什么工厂(工厂的类型不知道 是干什么的) 这时构造函数模式又横空出世
构造函数模式
function Person(name ,age){
this.name = name;
this.age = age;
}
var kobe = new Person('kobe','40');
var durant = new Person('durant',31);
复制代码
构造函数模式和工厂模式最大的区别就是多了一个new 关键字。那这个new 到底发生了什么?
- 创建了一个对象
- 将构造函数的作用域赋值给新的对象
- 执行构造函数中的代码
- 返回新对象
上面的例子中两个对象都有一个constructor属性,并且该属性会指向Person
kobe.constructor === Person // true
durant.constructor === Person // true
复制代码
创建了自定义的构造函数(Person) 它的实例就是一种特定的标识
但是构造函数也是存在问题的
function Person(name,age){
this.name = name;
this.age =age ;
this.sayName = function(){
console.log("Name"+this.name)
}
}
var kobe = new Person('kobe','40');
var durant = new Person('durant',31);
console.log(kobe.sayName === durant.sayName) // false
复制代码
以上标明了不同实例上的 同名函数式不相等的
这样就会出现创建一个多余的function实例出来
这样就出现了原型模式
我们创建的每个函数都有prototype属性,这个属性是一个指针,指向一个对象(由特定类型的所有实例所共享的属性和方法)。
使用原型对象的好处就是可以让所有对象共享它所包含的属性和方法
function Person(){}
Person.prototype.name = 'kobe'
Person.prototype.age = 40
Person.prototype.sayName = function(){
console.log(this.name)
}
复制代码
这样的话 Person里面的属性全部设置为实例共享状态。所有实例都会拥有同样的属性。
理解原型对象
- 无论什么时候创建一个函数。该函数都会创建一个prototype属性。然而这个属性就会指向原型对象(共享的对象)
- 在默认的情况下(也有特殊情况)所有原型对象都会有一个constructor属性,这个属性是指向prototype所在函数的指针
- 通过constructor属性我们可以继续为原型对象添加属性和方法
- Person.prototype.constructor = Person
- 在创建自定义的构造函数之后(原型对象默认只会取得constructor属性)
- 当构造函数创建一个新的实例时,实例中包含一个指针,是指向构造函数的原型对象(proto)
大概可以这样理解 当创建一个构造函数的时候,该构造函数上有prototype对象,该原型对象上有两个属性(1.constructor,2.proto),其中constructor指向该构造函数,_proto_会指向构造函数的原型对象
判断一个对象上的属性是原型属性还是继承属性
function has(object ,name){
return !object.hasOwnProperty(name)&&(name in object)
}
复制代码
混成模式
function Person(name,age){
this.name = name;
this.age = age;
}
// 相当于复写了原型对象, 所以要加上constructor 不然会切断实例和原型之间的联系
Person.prototype = {
constructor : Person,
sayName:function(){
console.log(this.name)
}
}
复制代码
可以说这是定义引用类型的一种默认模式
面向对象有一个很重要的特性就是继承
JS的继承跟其他(Java/C++)不同。JS的继承是通过原型链的方式进行继承的
什么是原型链?
原型链的基本思想就是让一个引用类型继承另外一个引用类型的属性和方法。每一个构造函数都有一个原型对象(自定义构造函数的一个prototype属性,因为它是一个对象,所以称为原型对象),原型对象有包含一个指针指向构造函数(原型对象上有一个constructor属性,是一个指针指向构造函数Person.prototype.constructor === Person),实例也有一个指向原型对象的内部指针(实例中有一个_proto_指向构造函数的原型对象)
假如我们让原型对象等于另外一个类型的实例,此时原型对象将包含指向另外一个原型的指针,这就是原型链
// 人类
function People(name , age){
this.name = "学生";
this.age = age;
}
People.prototype.sayName = function(){
console.log(this.name)
}
// 学生类继承人类
function Student(grade){
this.grade = grade;
}
Student.prototype = new People()
var student1 = new Student()
student1.sayName()
复制代码
- 实现了一个简单的学生类继承人类。本质上是重写了原型对象。学生类没有使用默认的原型对象,而是换了人的原型对象。这样新的原型对象有一个内部指针的是指向人的原型对象
- 所有引用类型默认都继承Object,这个继承也是通过原型链实现的
原型链虽然可以实现继承,但是也还是会有弊端。
引用数据类型的原型属性是会被实例所共享的。
function Father(){
this.colors = ['red','blue','green']
}
function Son(){
}
Son.prototype = new Father()
var son1 = new Son();
son1.colors.push('black');// [ 'red', 'blue', 'green', 'black' ]
console.log(son1.colors)
var son2 = new Son();
console.log(son2.colors)// [ 'red', 'blue', 'green', 'black' ]
复制代码
- 因为父类的属性是个引用数据类型(数组),son1添加了一个"black"属性。son2也共享了这个状态也有black
- 因为原型链继承父类以后,子类的原型对象成为两个父类的一个实例。所以拥有跟父类一样的colors属性。(相当于创建了一个Son.prototype.colors属性一样),但是因为引用类型会共享状态,所以子类都会共享了这个状态
出现了这个问题以后 就有(借助构造函数的方法)
function Father(){
this.colors = ['red','blue','green']
}
function Son(){
Father.call(this)
}
var son1 = new Son();
son1.colors.push('black')
var son2 = new Son();
console.log(son1.colors) //[ 'red', 'blue', 'green', 'black' ]
console.log(son2.colors) //[ 'red', 'blue', 'green']
复制代码
- 可以明显的看出来,两个子类son都没有共享Son的属性。这是因为在新创建子类的时候,子类的构造函数调用了父类的构造函数,使得每一次的创建实例的时候都会拥有一份父类定义所有对象初始化代码。
- 借用构造函数的方法还可以 传递参数(Father.call(this,'name'))
- 子类还可以定义自己的属性和方法
借助构造函数的方法
function People(name ,age){
this.name = name;
this.age = age;
}
function Student(name ,age,grade){
People.call(this,name,age)
this.grade = grade;
}
var student1 = new Student('张三',20,'大三')
console.log(student1)
复制代码
对于面向对象的理解
- 创建对象有三种模式
- 工厂模式(使用简单的函数创建对象,进而返回该对象。但是每次都要执行一遍代码。使得冗余)
- 构造函数模式(自定义类型,代码复用,但是方法每次都要新建)
- 原型模式(在原型中属性和方法是可以共享的,但是原型属性如果共享了,导致引用类型也共享了,不能每个子实例拥有自己的方法)
- 继承的方法
- 原型式继承
- 寄生式继承
- 寄生组合式