06|面向对象的程序设计

2019-11-30  本文已影响0人  井润

01|理解对象

01|属性类型

对应的ECMAScript中有两种属性,分别为数据属性和访问器属性

  1. 数据属性

对应的配置都可以通过 Object.defineProperty来进行定义!

let person = {};
Object.defineProperty(person,'name',{
    writable:false,
    Value:'ProbeDream'
})

其实都是比较好理解的:

  1. 访问器属性

对应的访问器属性不包含数据值,它们包含一对儿 setter和getter函数!

对应的访问器有如下四个特性:

我们通过对应的例子讲述:

let book = {
    _year:2004,
    edition:1
}
Object.defineProperty(book,'year',{
    get(){
        return this._year;
    },set(newvalue){
        if(newvalue>2004){
            this._year = newvalue;
            this.edition += newvalue - 2004;
        }
    }
});
book.year = 2005;
console.log(book.edition);//2

其中对应的下划线表示一种常用的记号,表示只能通过对象方法访问的属性!

02|定义多个属性

其中在ES5中又提出了一个新的API Object.defineProperties方法,用该方法可以通过描述符一次定义多个属性,这个方法接收两个对象参数:

let book = {};
Object.defineProperties(book,{
    _year:{value:2004},
    edition:{value:1},
    year:{
        get(){
    return this._year;
        },set(newvalue){
            if(newvalue>2004){
                this._year = newvalue;
                this.edition += newvalue - 2004;
             }
        }
    }
})
03|读取属性的特性

我们需要通过一个API 出自于 ES5中的Object.getOwnPropertyDescriptor,可以取得给定属性的描述符,其中传入两个参数:

let descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value,descriptor.configurable);

02|创建对象

其中使用Object构造函数或者对象字面量,确实能够很快的创建对象,但是会出现很多重复的代码,我们推荐使用工厂模式:

01|工厂模式
function createPersonFactory(name,age,job){
    let p = new Object();
    p.name = name;
    p.age = age;
    p.job = job;
    p.sayName = function(){
        console.log(this.name);
    };
    return p;
}
const p1 = createPersonFactory("p1",19,"Software Engineer");
const p2 = createPersonFactory("p2",20,"Software Engineer");

于是有了构造器模式!

02|构造器模式
function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log(this.name);
    };
}
const p1 = new Person("p1",19,"Software Engineer");
const p2 = new Person("p2",20,"Software Engineer");

最后的p1和p2都保存着不同的Person实例,这两个实例都有一个constructor(构造器/函数)指向Person

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

对应的使用构造函数属性最初是用来表示对象类型的,但是用来监测对象类型最靠谱的还是使用 instanceof 操作符来得更加可靠一点!

console.log(p1 instanceof Person,p2 instanceof Person);//true true
  1. 将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它的方式不同,对应的构造函数也是函数,不存在定义构造函数的特殊语法!

//通过构造函数来使用
let Person = new Person("ProbeDream",22,'softWare Engineer');
Person.sayName();//ProbeDream

//作为普通函数的使用
Person('Probe',19,'Frontend Engineer');//添加到window上
window.sayName();//Probe

//在另一个对象的作用域中调用
let o = new Object();
Person.call(o,'Julia',18,'Hr');
o.sayName();//Julia
  1. 构造函数的问题

虽然说这样创建没有什么问题,但是比较麻烦的就是,每个方法都需要在不同的实力上重新创建一遍,对应的以上的代码中,都有对应的一个sayName的方法,其实对应的方法都是 Function不同的实例对象! 因此定义的函数其实也就是实例化的Function对象,因此可以这样定义构造函数!

function Person(name,age,obj){
    this.name = name;
    this.age = age;
    this.obj = obj;
    this.sayName = new Function('console.log(this.name)');//与声明函数在逻辑上是等价的!
}
console.log(p1.sayname === p2.sayName);//false 

这样一来对应的创建两个完成同样任务的Function实例的确没有必要,况且this对象在,根本不用在执行代码前就把函数绑定到特定对象上面! 可以通过把函数定义转移到构造函数外部解决这个问题

function Person(name,age,obj){
    this.name = name;
    this.age = age;
    this.obj = obj;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name);
}

虽然这样一来对应的不同的Person的实例共享了在全局作用域定义的函数,但是全局作用域内定义的函数,只能够被某个对象调用,让全局作用域名不副实,如果说对想要定义多个方法的话,就需要在全局作用域内定义很多全局函数 自定义的引用类型没有任何的封装性可言了! 因此我们介绍到了原型模式!

03|原型模式

我们创建的函数都有一个prototype(原型)属性,该属性是一个指针,指向一个对象,该对象的用途是包含可以由特定类型所有实例共享的属性和方法!

于是,我们不必在构造函数中定义对象实例的信息,而是将该信息直接添加到圆形对象中

function Person(){}
Person.prototype.name = 'ProbeDream';
Person.prototype.age = 22;
Person.prototype.job = 'software Engineer';
Person.prototype.sayName = function(){console.log(this.name);}

let p1 = new Person();
p1.sayName();//ProbeDream

let p2 = new Person();
p2.sayName();//ProbeDream

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

之所以对应的不同Person实例的sayName相等,是因为都指向同一个原型对象!

  1. 理解原型对象

我们创建了一个新的函数,都会根据一组特定的规则为该函数创建一个prototype的属性,该属性指向函数的原型对象! 所有的原型对象都会自动获得一个constructor属性,该属性包含一个prototype属性所在的函数的指针,例如说Person.prototype.constructor 指向Person,我们可以通过构造函数,继续为原型对象添加其他属性和方法!

对应的一个属性__proto__是存在于实例和构造函数的原型对象之间,并非存在于实例和构造函数之间

对应的p1,p2都包含了一个内部属性,该属性仅仅指向了Person.constructor,他们与构造函数没有直接的关系,但是他们可以调用对应的sayName是因为通过查找对象属性的过程实现的!

虽然所有的视线中都无法访问到对应的prototype,但是可以通过isPrototypeOf方法来确定对象间是否存在这种关系,我们使用Prototype指向调用了isPrototypeOf方法的对象Person.prototype那么该方法就返回true!

console.log(Person.prototype.isPrototypeOf(p1),Person.prototype.isPrototypeOf(p2));//true true
console.log(Object.getPrototypeOf(p1));//Person.prototype

对应的创建出来的实例,首先是看实例对象本身是否由该属性,如果没有的话再去原型链上查找!

通过以下例子就可以理解:

function Person(){}
Person.prototype.name = 'probedream';
Person.prototype.age = 29;
Person.prototype.job = 'Frontend Engineer';
Person.prototype.sayName = function(){
    console.log(this.name);
}
let p1 = new Person();
p1.name == "Julia";
let p2 = new Person();
console.log(p1.name,p2.name);//Julia  probedream
delete p1.name;
console.log(p1.name);//probedream
  1. 原型与in操作符

对应的in操作符一般分为两种情况下使用:

对应的判断属性只存在于原型中应该如何编写代码?

function hasPrototypeProperty(object,name){
    //先判断不在实例中 
    return !object.hasOwnProperty(name) && (name in object);
}

如果想获得所有实例属性,可以通过Object.getOwnPropertyNames() 方法!

  1. 更简单的原型语法
function Person(){}
Person.prototype = {
    //可以理解为 constructor:Object 
    name:"ProbeDream",
    age:22,
    job:"FrontEnd Engineer",
    sayName(){console.log(this.name);}
}
let p1 = new Person();
console.log(p1 instanceof Person,p1.constructor === Object);//true true 

虽然这样通过new操作符创建出来的对象的结果相同,但是有问题的是 constructor属性不再指向Person了! ,每创建一个函数都会同事创建它的prototype对象,并且该对象会自动获得constructor属性! 而我们这里的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新的对象的constructor属性(Object) 以上的代码虽然通过instanceof可以返回为true的结果值,但是对应的Constructor却无法确定对应的对象的类型了!

但是如果说想要Constructor对应的话,可以在 Person.prototype赋值的时候加上 constructor:Person

但是会导致Enumerable为true,原生的Constructor属性是不可以枚举的,因此我们可以通过Object.defineProperty设置Enumerable为false!

  1. 原型的动态性
let p1 = new Person();
Person.prototype.sayHi = function(){console.log('Hi!')}
p1.sayHi();//Hi!

以上代码运行没有问题,虽然说是新方法的添加是在实例化之后添加的,但是因为 实力与原型之间的松散连接的关系 我们调用sayHi方法的时候惠贤从实例上搜索,找不到的话再从原型上去查找!

但是有些时候,比如说重写整个原型对象的话,构造函数会为实例添加一个指向原型之间的[[Prototype]]指针,而把原型修改为另外一个对象等于切断了构造函数与最初原型之间的联系!

function Person(){}
let p1 = new Person();
Person.prototype = {
    constructor:Person,
    name:"ProbeDream",
    age:22,
    job:"FrontEnd Engineer",
    sayName(){console.log(this.name);}
}
p1.sayName();//error
  1. 原生对象的原型

原型模式不仅仅仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的!

对应的所有的原生引用类型,都在其构造类型的原型上定义了方法,例如说 Array.prototype可以找到sort方法! 对应的String.prototype可以找到对应的substring方法!

  1. 原型对象的问题

其中原型对象中最大的问题就是,共享本性所导致的,所有的属性都是被很多实例所共享的 如果说对于引用类型的值来说,问题就比较突出了!

function Person(){}
Person.prototype = {
    name:"ProbeDream",
    age:22,
    job:'Front End Engineer',
    friends:['Julia','Jerry'],
    sayName(){
        console.log(this.name);
    }
}
let p1 = new Person();
let p2 = new Person();
p1.friends.push('tom');
console.log(p1.friends === p2.friends);//不同的实例共享同一个数组

但是其中一点比较总要的是:实力要有属于自己全部的属性的! 而这种所谓的原型模式恰恰违反了这一原则!

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

这种模式也是我们常常见到的:

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['julia','jerry'];
}
Person.prototype = {
    constructor:Person,
    sayName(){console.log(this.name);}
}
//关闭构造器可遍历的特性!
Object.defineProperty(Person.prototype,'constructor',{
    enumerable:false,value:Person
})

这种方式可以说是认同度最高的一种创建定义类型的方法!

05|动态原型模式

动态原型就是致力于解决独立的构造函数和原型的问题! 将所有的信息都封装在了构造函数中,而通过在构造函数中初始化对应的原型!(在必要的情况下)

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    if(typeof this.sayName !== 'function'){
        Person.prototype.sayName = function(){
            console.log(this.name);
        }
    }
}

这段代码总的情况下来看还是比较容易理解的,只有在对应的sayName不存在的情况下才会添加到原型中!

对应的对于原型做的修改在对应的实例中完全可以得到体现!

但是需要注意的点就是:不能够使用对象字面量的形式重写原型,会导致切断现有实例和新原型之间的联系!

06|寄生构造函数模式

主要的原则就是:使用一个函数封装创建对象的代码,之后返回该对象!

function Person(name,age,job){
    let p = new Object();
    p.name = name;
    p.age = age;
    p.job = job;
    p.sayName = function(){
        console.log(this.name);
    }
    reutrn p;
}

不推荐这种模式创建对象!

07|稳妥的构造模式
function Person(name,age,job){
    let p = new Object();
    //私有变量和函数的定义!
    p.sayName = function(){
        console.log(name);
    }
    reutrn p;
}

对应的与寄生模式相似,但是与构造器和构造器原型之间并没有关系,使用instanceof操作符并没有意义!

03|继承

继承是OO语言(面向对象语言)中为人津津乐道的概念,很多OO语言都支持两种方式实现继承,接口继承和实现继承

ECMAScript中没有对应的函数签名因此无法实现接口继承!

01|原型链

对应的构造函数,原型,实例之间的关系

如果说让原型对象等于另一个类型的实例,结果会如何呢?

原型对象包含指向另外一个原型对象的指针,另一个原型中包含另外一个指向构造函数的指针! 这种层层递进的关系构成了实力与原型之间的链条,这就是所谓的原型链!

对应的实现原型链有一种基本模式,代码实现如下所示:

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subproperty = false;
}
SubType.property = new SuperType();//实现继承

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}
let p = new SubType();
console.log(p.getSuperValue());

其中对应的继承是通过,SuperType创建的实例赋值给SubType.prototype实现的! 实质上是重写原型对象,代之以一个新类型的实例

其实代码中比较好理解的是:

instance指向SubType原型对应的SubType指向SuperType原型!

而我们通过 instance.getSuperType 调用方法其实是经历了三个过程:

  1. 搜索实例
  2. 搜索SubType.prototype
  3. 搜索SUperType.prototype 找到该方法!
  1. 确定原型和实例的关系

第一种方式是通过 instanceof 操作符,只要用该操作符就可以测试实例与原型链中出现过的构造函数! 结果会返回true

console.log(p instanceof Object );
console.log(p instanceof SuperType);
console.log(p instanceof SubType);

第二种方式是通过isPrototypeOf方法,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型! 因此对应的方法也会返回true!

console.log(Object.prototype.isPrototypeOf(p));
console.log(SuperType.prototype.isPrototypeOf(p));
console.log(SubType.prototype.isPrototypeOf(p));
  1. 谨慎的定义方法

子类型有些时候需要重写炒类中的某个方法,但是需要注意的是,给原型添加方法一定是在替换原型语句(继承)之后

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subproperty = false;
}
SubType.property = new SuperType();//实现继承

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}
//重写超类型中的方法!
SubType.prototype.getSuperValue = function(){
    return false;
}
let p = new SubType();
console.log(p.getSuperValue());

如果说使用对象字面量的形式添加方法,会导致替换原型(继承)的代码无效! 因此不推荐使用 对象字面量的形式重写超类型的方法或者原型的方法!

  1. 原型链所出现的问题

原型链虽然说比较强大,可以用来实现继承,但是如果说原型链包含应用类型值的原型的话,会被所有实例所共享! 在通过原型实现继承的时候,原型实际上会变成另外一个类型的实例,对应的实例的属性也会变成现在的原型属性了!

function SuperType(){
    this.colors = ['red','blue','green'];
}
function SubType(){}
SubType.prototype = new SuperType();//实现继承
let  p1 = new SubType();
p1.colors.push('grey');
console.log(p1.colors);//["red", "blue", "green", "grey"]
let p2 = new SubType();
console.log(p2.colors);//["red", "blue", "green", "grey"]
02|借用构造函数

为了解决原型中包含引用类型值所带来的问题的过程中,开发人员使用一种叫做 借用构造函数的技术 其实总的来讲还是比较好理解的,就是 子类型构造函数内部调用父类型构造函数!

function SuperType(){
    this.colors = ['blue','red','yellow'];
}
function SubType(){
    //继承了SuperType
    SuperType.call(this);
}
let p1 = new SubType();
p1.colors.push('grey');
console.log(p1.colors);/["blue", "red", "yellow", "grey"]

let p2 = new SubType();
p2.colors.push('black');
console.log(p2.colors);//["blue", "red", "yellow", "black"]

其实通过这种方式就已经很好的解决了 圆形中包含引用类型带来的共享问题了! 每个实例中都只包含自己特有的colors属性副本!

与此在借用构造函数的时候还可以 传递参数!

借用构造函数出现的问题,方法都造构造函数中定义的话,函数的复用就无从谈起!

超类型中定义的方法对于子类而言的话是不可见的,结果所有的类型都只能够使用构造函数模式! 借用构造函数的模式也是很少使用的!

03|组合继承

对应的组合继承指的就是说,将原型链和借用构造函数的奇数组合到一块!

function SuperType(name){
    this.name = name;
    this.colors = ['red','blue','yellow'];
}
SuperType.prototype.sayName = function(){
    console.log(this.name);
}
function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

其实对应的组合继承避免了原型联合借用构造函数的缺陷!融合他们的优点,与此同时instanceof和isPrototypeOf能够用于识别基于组合继承创建的对象!

之后又对原型式继承和寄生式继承以及寄生组合式继承进行了简要的讲解! 因为也是不常用的例子,这里就不进行阐述了!

上一篇 下一篇

猜你喜欢

热点阅读