面向对象

147 阅读12分钟

6.面向对象

面向对象的语言,都有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。

ECMAScrit中没有类

创建对象

工厂模式

//工厂模式
function CreatePerson (name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    }

    return o;
}

var p1 = CreatePerson('hlf',29,'code');
var p2 = CreatePerson('hvg',22,'write');

p1.sayName()
p2.sayName()

构造函数模式

和工厂模式比:

  1. 没有显示的创建对象
  2. 直接将属性和方法赋值给this对象
  3. 没有return语句

类名大写字母开头,主要是为了区分其他函数。构造函数本身也是函数,只不过可以用来创建对象

要创建CreatePerson的新实例,必须使用new操作符。 通过new调用构造函数实际会经历一下4步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给信对象
  3. 执行构造函数中的代码
  4. 返回新对象
function CreatePerson(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;

    this.sayName = function(){
        console.log(this.name)
    }
}

var p1 = new CreatePerson('hlf',30,'coding')
var p2 = new CreatePerson('xxx',20,'jump')

p1.sayName() // 'hlf'
p2.sayName() // 'xxx'

console.log(p1 === p2); //false
console.log(p1.constructor === p1.constructor) //true

p1和p2分别是通过new CreatePerson()创建的不同的实例。

这两个实例都有一个constructor(构造函数)属性,该属性指向他们的构造函数CreatePerson

instanceof 检测对象类型

console.log(p1 instanceof Object); //true
console.log(p1 instanceof CreatePerson); //true
console.log(p2 instanceof Object); //true
console.log(p2 instanceof CreatePerson); //true

p1和p2之所以同时是Object的实例,是因为所有对象均继承自Object

1.将构造函数当做函数

构造函数和普通函数的唯一区别就是调用的方式不同。

构造函数也是函数,任何函数,只要通过new调用,那么它就可以作为构造函数。 而任何函数不通过new 来调用,就是普通函数。

// 作为构造函数
var person = new Person('hlf',29,'coding')
person.sayName(); //'hlf'

//作为普通函数
Person('hlf',29,'coding'); //this指向window
window.sayName() // 'hlf'

//在另一个对象中调用
var o = new Object();
Person.call(o,'xxx',18,'hello')
o.sayName(); //'xxx'

构造函数的典型用法就是使用new来创建一个新对象。

2.构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

p1和p2都有一个名为sayName()的方法,但是两个方法不是同一个Function的实例。

function Person(){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Functionfunction('alert(this.name)')
}

alert(p1.sayName == p2.sayName) //false

每个Person实例都包含了一个不同的Function实例。 因为不同实例上的同名函数是不相等的。

原型模式

每个函数都有一个prototype(原型)属性,这个属性一个指针对象,这个对象包含了所有实例共享的属性和方法。

prototype就是通过调用构造函数而创建的那个对象实例的原型对象。

原型的好处就是可以所有实例共享它所包含的属性和方法。

function Person(){

}

Person.prototype.name = 'hu';
Person.prototype.age = 29;
Person.prototype.job = 'coding';
Person.prototype.sayName = function(){
    console.log(this.name);
}

var p1 = new Person();
p1.sayName(); //'hu'

var p2 = new Person();
p2.sayName(); // 'hu'

console.log(p1.sayName === p2.sayName); //true

1.理解原型对象

只要创建了一个函数,就是自动创建一个prototype(原型)属性,这个属性指向的是原型对象。 默认情况下,所有原型对象会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。

Person.prototype.constructor 指向的Person, 而通过这个构造函数,我们可以继续为原型对象添加其他属性和方法

创建了一个自定义构造函数后,其原型对象默认会有constructor属性;

当调用构造函数创建的第一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象__proto__。

__proto__这个链接存在于实例和构造函数的原型对象之间,而不是存在于实例和构造函数之间。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
}

Person.protptype.sayName = function(){
    console.log(this.name);
}

var p1 = new Person('hlf',29,'coding');
p1.sayName();

console.log(p1.__proto__ === p1.constructor.prototype);

Person.prototype指向原型对象 Person.prototype.constructor又指回了Person

Person.prototype原型对象中除了包含constructor之外,还包括后来添加的其他公共属性。

Person的每个实例-p1,p2都包含一个内部属性__proto__属性,该属性执行了Person.prototype;

换句话说他们和构造函数没有直接关系,虽然这两个实例不包含属性和方法,但是却可以调用p1.sayName(),这是通过原型链查找来实现的

isPrototypeOf :检测一个对象是否是另一个对象的原型。或者说一个对象是否被包含在另一个对象的原型链中

console.log(Person.prototype.isPrototypeOf(p1)); //ture
console.log(Person.prototype.isPrototypeOf(p2)); //ture

这里用原型对象的isPrototypeOf()方法测试了p1和p2。 他们内部都有一个纸箱Person.prototype的指针,因此都返回了true

Object.getPrototypeOf :返回实例__proto__的值

console.log(Object.getPrototypeOf(p1) === Person.prototype); //true
console.log(Object.getPrototypeOf(p1).name);//'hlf'

Object.getPrototypeOf返回的对象就是这个对象的原型

原型的查找机制:先从构造函数本身找属性和方法,如果自身没有就会从构造函数的原型对象中查找。

function Person(){

}
Person.prototype.name = 'hlf';
Person.prototype.age = 29;
Person.prototype.job = 'coding'
Person.prototype.sayName = function(){
    console.log(this.name);
}

var p1 = new Person();
var p2 = new Person();

p1.name = 'hulongfei';

console.log(p1.name); //'hulongfei'
console.log(p2.name)//'hlf'

delete p1.name;
console.log(p1.name); //'hlf'

给实例p1添加了name属性,会优先查找实例自身的存在的属性,找到了就不回去原型上查找 delete删除了实例p1的name属性,实例本身没找到name属性,就是沿着原型链查找原型身上的name属性

hasOwnProperty() :检测一个属性是存在实例中,还是原型中,如果是实例自身的属性就返回true,如果是原型上的就返回false

function Person(){

}
Person.prototype.name = 'hlf';
Person.prototype.age = 29;
Person.prototype.job = 'coding'
Person.prototype.sayName = function(){
    console.log(this.name);
}

var p1 = new Person();
var p2 = new Person();

p1.name = 'hulongfei';

console.log(p1.hasOwnProperty('name')); //true //来自实例
console.log(p1.hasOwnProperty('age')); //false //来自原型

delete p1.name;
console.log(p1.hasOwnProperty('name')); //false 删掉实例自身的name,会去原型上找

通过hasOwnProperty(),什么时候访问的实例属性,什么时候访问的时候原型属性就一清二楚了

2.原型与in操作符

有两种方式使用in:单独使用和在for-in循环中使用

in操作符:检测属性是否属于这个对象。自身找不到会从原型上找
console.log(name in p1);//true  来自实例
console.log(name in p2); //true 来自原型

console.log(p1.hasOwnProperty('name')); //true 自身实例上有
console.log(p2.hasOwnProperty('name')); //false 自身实例上没有

//检测一个属性是否是原型上的属性
function hasPrototypeProperty(obj,name){
    return !obj.hasOwnProperty(name) && (name in obj)
}

console.log(hasPrototypeProperty(p1,'name')) // false
console.log(hasPrototypeProperty(p2,'name')) //true

for-in 循环时,返回所有能通过对象访问的可以枚举的属性,既包括实例中的属性,也包括原型中的属性

Object.keys():获取对象所有可以枚举的实例属性,接收一个对象,返回属性数组

var keys = Object.keys(Person.prototype); //['name','age','job','sayName']

var p1 = new Person();
p1.name = 'hlf';
p1.age = 20;
var p1keys = Object.keys(p1);
console.log(p1keys);//['name','age']

// Object.getOwnPrototypeNames():得到所有实例属性,无论是否可枚举
var keys = Object.getOwnPropertyNames(Person.prototype)
console.log(keys); //'constructor,name,job,sayName'

3.更简单的原型语法

function Person(){

}

Person.prototype = {
    name:'hlf',
    age:20,
    job:'coding',
    sayName:function(){
        console.log(this.name);
    }
}

var p1 = new Person();

console.log(p1 instaceof Object); //true
console.log(p1 instanceof Person); //true

consoloe.log(p1.constructor == Person); //false
console.log(p1.constructor == Object); //true

我们将Person.prototype重新创建了一个新对象,这个里面的constructor属性不再指向Person 了。

重新指回constructor
Person.prototype = {
    constructor:Person,
    name:'hlf',
    age:20,
    sayName(){
        console.log(this.name);
    }
}

这种方式重设constructor会导致他的可枚举属性变成true,原生默认是false
可以用Object.defineProperty
Object.defineProperty(Person.prototype,'constrcutor',{
    enumerble:false,
    value:Person
})

4.原型的动态性

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系,他们引用的仍然是最初的原型

5.原生对象的原型

原型对象上存在一些公有的属性和方法,不要修改原生对象的原型。 如果缺少某个方法,就在原生对象的原型中添加这个方法,容易导致命名冲突,也可以会意外的重写原生方法

6.原型对象的问题

省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下,都取得相同的属性值。 原型模式的最大问题是由其共享的本性所导致的。

挂载在原型上的属性,当实例修改原型上公共的属性时,所有的依赖这个属性的实例,都会得到修改,因为共享了一个原型属性。但是,实例一般都要有自己的属性和方法,这个问题就是我们很少见到有人单独使用原型模式的原因所在。

组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性 原型模式用于定义方法和共享的属性 每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['xx1','xx2']
}
Person.prototype = {
    constructor:Person,
    sayName(){
        console.log(this.name);
    }
}

var p1 = new Person('hlf',29,'coding')
var p2 = new Person('hulongfei',30,'coding1')

p1.friends.push('xx3')

console.log(p1.friends);// 'xx1,xx2,xx3'
console.log(p2.friends);// 'xx1,xx2'

console.log(p1.friends === p2.friends);//false
console.log(p1.sayName === p2.sayName); //true

实例属性都是在构造函数中定义的,所有实例共享的属性constructor和方法sayName()则在原型中定义。修改p1的属性并不影响到p2,因为他们分别引用了不同的数组。

这个构造函数与原型混合的模式,是目前使用最广泛,认同度最高的创建自定类型的方法。

继承

实现继承主要靠原型链来实现的

原型链

原型链是实现继承的主要方法 基本思想:就是利用原型让一个引用类继承另一个引用类型的属性和方法。

构造函数,原型和实例的关系 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

假如我们让原型对象的等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这个就是原型链的基本概念

    function Animal(name,age){
        this.name = name;
        this.age = age;
    }

    Animal.prototype.sayName=function(){
        console.log(this.name);
    }

    function Person(job){
        this.job = job;
    }

    // 继承了Animal
    Person.prototype = new Animal('hulongfei','30');
    Person.prototype.sayHello = function(){
        console.log('hello');
    }

    var p1 = new Person('coding');

    console.log(p1.__proto__); //Animal
    console.log(Person.prototype.constructor); //animal
    console.log(Animal.prototype.constructor); //animal

    console.log(p1.name); //'hulongfei'
    console.log(p1.age); //29
    p1.sayName() //'hulongfei'
    p1.sayHello() //'hello'

1.别忘记默认的原型

所有引用类型的默认都继承了Object.这个继承也是通过原型链实现的。 所有函数的默认原型都是Object的实例。因此,默认原型都会包含一个内部指针, 指向Object.prototype.这也正事所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。

2.确定原型和实例的关系

instanceof:检测实例与原型链中出现过的构造函数,结果返回true

    console.log(p1 instanceof Object); //true
    console.log(p1 instanceof Animal); //true
    console.log(p1 instanceof Person); //true

isPrototypeOf():只要原型链中出现过的原型,都可以说是原型链所派生的实例的原型。

console.log(Object.prototype.isPrototypeOf(p1)); //true
console.log(Animal.prototype.isPrototypeOf(p1)); //true
console.log(Person.prototype.isPrototypeOf(p1)); //true

3.谨慎地定义方法

4.原型链的问题

function SuperType(){
    this.color = ['red','blue','green']
}

function subType();

subType.prototype = new SuperType();

var instance1 = new Subtype();
instance1.colors.push('black');
console.log(instance1.colors);//'red,blue,green,black'

var instance2 = new SubType()
console.log(instance2.color); //'red,blue,green,black'

当通过原型链继承了SuperType之后,所有的SubType的实例都会共享这一个colors. 第二个问题就是创建子类型的实例时,不能像超类型的构造函数中传递参数。

组合继承

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

    function Animal(name){
        this.name = name;
        this.colors = ['red','blue','green']
    }

    Animal.prototype.sayName = function(){
        console.log(this.name);
    }

    function Person(name,age){
        //继承属性
        Animal.call(this,name);
        this.age = age;
    }
    //继承方法
    Person.prototype = new Animal();
    Person.prototype.constructor = Person;
    Person.prototype.sayAge = function(){
        console.log(this.age);
    }

    var p1 = new Person('hulongfei',30)
    p1.colors.push('pink')
    console.log(p1.colors); //["red", "blue", "green", "pink"]
    p1.sayName() //'hulongfei'
    p1.sayAge() //30

    var p2 = new Person('xxx',18)
    console.log(p2.colors); //['red','vlue','green']
    p2.sayName() //'xxx'
    p2.sayAge() //18

组合继承成为js中最常用的继承模式 组合继承最大的问题在于无论什么情况下,都会调用两次超类型的构造函数

寄生组合式继承

function object(o){
    function F(){};
    F.prototype = o;
    return new F()
}

function inhertPrototype (subType,superType){
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

function Animal(name,age){
    this.name = name;
    this.age = age;
    this.colors=['red','blue','green']
}
Animal.prototype.sayHello = function(){
    console.log('hello');
}

function Person(name,age){
    Animal.call(this,name); //继承父类属性
    this.age = age
}

inhertPrototype(Person,Animal)

Person.prototype.sayName = function(){
    console.log(this.name);
}

var p1 = new Person('hulongfei',20);
p1.colors.push('pink')
console.log(p1.colors);
p1.sayHello()
p1.sayName()

var p2 = new Person('xxx',17)
console.log(p2.colors); //'red,blue,green'
console.log(p2.age); //17
p2.sayName() //'xxx'

至调用了一次Animal构造函数,并且避免了在Person.prototype上面创建多余的属性。 而且原型链还能保持不变。还可以正常使用instanceof 和 isPrototypeOf()

总结

创建对象:

  • 工厂模式:使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。
  • 构造函数模式: 可以创建自定义引用类型,可以像创建内置对象实例一样使用new; 缺点:他的每个成员都无法得到复用,包括函数。不能共享
  • 原型模式:使用构造函数prototype设置共享的属性和方法。
  • 组合使用构造函数模式和原型模式时:使用构造函数定义实例属性,使用原型定义共享的属性和方法

继承:

  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承:实现基于类型继承的最有效方式