如何在js中使用面向对象思想来编程

为什么要用面向对象思想编程?

大家想想,我们在搭建静态页面编写DOM元素样式的时候,是不是用了CSS类名来抽象出一类的样式?那在写JS的时候,有时候为了不让代码重复,是不是也是习惯性地抽离公共方法来复用代码?其实使用面向对象思想编程的好处也同以上,就是为了在一堆有许多共同功能的“元素”中抽离“共同点”,从而达到一劳永逸的作用。面向对象编程的好处主要有:

  • 代码冗余度底
  • 代码复用性高
  • 高内聚低耦合

使用场景,举个栗子:

假如有一个需求,要动态创建100个一样样式和功能的div,大家一般拿到这个需求,可能就直接按部就班打上了:

for(var i = 0; i < 100; i++) {
    var div = document.createElement('div');
    documet.appendChild(div);
    div.style.width = '100px';
    div.style.height = '100px';
    div.style.border = '1px solid #000';
}
复制代码

还好这个需求是100个“一样的”,要是让你创建100个大小一样,颜色不一样的div,并且每个div点击下去,都会变颜色,每个div鼠标移动上去都会变大一倍等等等等复杂的需求,你要怎么做?你可能会开始编写一个好用的函数,传入参数,制造定制化的div,return出来,这也确实是一个好办法。如果使用“面向对象”来做,要怎么搞?看完这篇文章,你也许就懂了。

面向对象,首先要有对象

ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”

我们经常使用字面量的方法创建对象:

var person = {
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    sayName: function(){
        alert(this.name); // 'Nicholas'
    }
};
复制代码

-----------------------------分割符------------------------------

知识延伸,可跳过:

如果要删除对象中的某个属性可以用delete操作符 delete person.name,使用for-in循环这个对象,可以返回每个属性,可以对每个属性进行修改:person.name = 'cc',访问person.name时,返回'cc'。对象之所以拥有这些功能,是因为ECMA对于对象的规定。如果你想打破这默认的规定,比如:你想保护这个属性,让它不可写,不可删,可以使用ECMAScript 5 的 Object.defineProperty() 方法。

 Object.defineProperty(person, "name", {
    configurable: false, // 不可用delete删除
    enumerable: false, // 不可在for-in中返回
    writable: false, // 不可修改
    value: "Nicholas", // 读、写的位置
    get: function(){ // 读name属性的时候调用
        return this.name + 'aaa';
    },
    set: function(newValue){ // 写name属性的时候调用
        if (newValue === 'cc') {
            this.name = newValue;
            this.age += 1;
        }
    }
});
复制代码

ps: getter和setter函数不是必须的,也无需成对出现,但是只有getter的话,属性将不可写入,只有setter的话,属性将不可读(非严格模式返回undefined),所以还是成对儿吧~此外,定义多个属性ES5还提供了一个Object.defineProperties()方法, Object.getOwnPropertyDescriptor()这个方法可以读取这些配置。

-----------------------------分割符------------------------------

回归正题,用上面的方法创建对象的时候,每创建一个对象,就得写以上一堆代码,这些代码又是相似的,对于同一类的对象(属性相同),这样做确实很蠢,所以总是要偷懒的~ 包装一下呗:

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
复制代码

这便是设计模式中的 工厂模式,这虽然解决了创建多个相似对象的问题,但是还是难以将“同类”的概念抽离出来,用工厂模式创建的对象,构造函数都是Object,即:

person1.constructor === Object // true 
person1 instanceof Object // true
复制代码

为了更加明确创建一个类型为“人类”的对象,便有了构造函数模式,设计一个“人类”构造器,由它构造出来的“实例”就是一个个的人:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
复制代码

相比较工厂模式创建对象,构造函数模式:

  • 没有显式地创建对象
  • 直接将属性和方法赋给了 this 对象
  • 没有 return 语句
  • 换成了new 操作符调用它

那为什么用构造函数可以达到工厂模式一样创建对象的效果呢?因为new操作符起到了至关重要的作用,用new操作符经历了:

  • (1) 创建一个新对象;
  • (2) 将构造函数的作用域赋给新对象(也就是把 this 指向这个新对象);
  • (3) 执行构造函数中的代码(为这个新对象添加属性);
  • (4) 返回新对象。

new 操作符帮你隐式做到了工厂模式的功能,而且,每个用new 操作符实例化出来的实例:

person1.constructor === Person // true 
person1 instanceof Person // true
person2.constructor === Person // true 
person3 instanceof Person // true
复制代码

这样,是不是更加明确了person1 和 person2 这俩货就是“Person”的实例,就是“人类”,而不是模糊的“Object”。

目前为止,相对于开篇的字面量一个个去创建对象有了很大的进步,但是呢,凡是总有但是,构造函数模式还有缺陷,大家有没有发现,每次使用调用Person构造函数的时候,sayName方法都要在每个实例上重新创建一遍,这也太蠢,不仅仅浪费了内存,代码上还多余,如何解决?达到一劳永逸的效果,这时候原型模式就来了。

还有一种办法,就是把公共的函数抽出来写在全局下,但是这样做,全局函可以在任何地方访问到,就无法变成Person的自有函数,而且有很多这样的函数的话会污染全局作用域。

什么是原型?它是函数的一个属性,函数在创建的时候就会有prototype(原型)属性,这个属性就是该构造函数创建出来的实例的原型对象。啥意思呢,就以上代码来说:person1的原型对象就是Person.prototypeprototype属性上都有哪些东西呢?打印出来就知道了嘛,在chome浏览器控制台打印如下:

可以看到,默认就有 constructor__proto__这俩货, constructor是一个函数,可以看到它其实就是 fn函数,红皮书上说: 所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针

fn.prototype.constructor === fn // true
fn.constructor === Function // true 因为fn.__proto__.constructor === Function 为true,它其实是在原型链上的constructor属性
复制代码

还有一个__proto__属性([[Prototype]]),这个就很重要了,它就是原型模式的原理,它是一个指针,指向构造函数的原型对象,也就是说,图片上的fn.prototype.__proto__指向了fn的构造函数的原型对象,而fn的构造函数是Function,Function的原型对象是Object.prototype,所以,fn.prototype.__proto__ === Object.prototype为true,这里再拓展一下,原型链的顶层是null,Object.prototype.__proto__ === null为true。这样描述好像很绕和很抽象,拿Person 与 person1、person2的关系,可以有这样的图表示:

通俗说,person1和person2都是Personnew出来的,所以,person1和person2可以访问到Personprototype上的所有属性,如何访问到呢?就是通过person1.__proto__person2.__proto__,他们指向Personprototype

person1.__proto__ === Person.prototype // true
person2.__proto__ === Person.prototype // true
复制代码
// 原型模式
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
复制代码

so, person1.__proto__.name === 'Nicholas' // true,省略掉中间的__proto__,也可以访问得到a,person1.name === 'Nicholas' // true,利用构造函数原型属性,在原型属性上添加属性和方法,就可以让该构造函数的实例们共享这些属性和方法了。js在访问一个对象的某个属性是,先从该对象自身搜索,自身搜索结果为undefined时,再从自身的__proto__属性上找,再为undefined时,再从自身的__proto__属性的__proto__属性上找,一直这样下去,直到null位置,这写就构成了原型链。总结一句话:当找某个对象的属性时,先找自身,再通过原型链一层层往上找

所有属性都挂载在原型上也不行,这样实例没有差异性,除非每个实例都添加自身属性,从而覆盖它原型链上的属性,组合使用构造函数模式和原型模式就可以解决这样的问题,也是在 ECMAScript中使用最广泛、认同度最高的一种创建自 定义类型的方法

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
复制代码

这样就既实现了实例的差异性,又实现了代码的复用。

以上创建对象的方法,讲了五种:字面量创建、工厂模式、构造函数模式、原型模式、构造函数与原型的组合模式, 除了字面量创建是直接创建出一个对象,其余的几种,都是通过调用函数(实例化)来创建对象(实例)的,无论通过普通函数也好,通过构造函数也好(普通函数前面加个new来调用就是构造函数啦),我的理解是,除了字面量创建,其他的几种方法都是抽象出一种“类”,利用“类”实例化出各个对象的面向对象的编程思想

那我们创建创建的一个个的“类”(构造函数),可以互相调用,来复用代码么?可以,这就是继承

js 通过原型链实现“类”(构造函数)之间的继承

在上面的介绍中,我们知道实例与构造函数的关系:构造函数(构造函数A)实例化一个实例后,实例(实例a)就会拥有构造函数prototype属性上的所有东西,通过自己的__proto__指针指向这些东西。那,同样的,如果这个构造函数(构造函数A)的prototype属性也是另一个构造函数(构造函数B)的实例,A的prototype属性也会利用自己的__proto__指针把Bprototype上的所有东西捞过来,这可以干嘛呢?这就可以让实例a拥有Bprototype上的所有东西了,如何拥有?先通过自己的__proto__读构造函数A的prototype,构造函数A的prototype再通过它的__proto__读构造函数B的prototype,再返回传递给实例a。这一层层向上读取属性,就是原型链(实例与原型的链条)。说来说去,还是有点晕啊,来段代码:

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//继承了 SuperType,这样SubType.prototype上就有property这个属性和getSuperValue这个方法,传递给SubType的实例instance
// SubType.prototype.property -> true
// SubType.prototype.getSuperValue -> function(){return this.property;}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
复制代码

调用instance.getSuperValue() 会经历三个搜索步骤:

  • 1)搜索实例;
  • 2)搜索 SubType.prototype ;
  • 3)搜索 SuperType.prototype。 原型链继承的原理就是:利用原型让一个引用类型继承另一个引用类型的属性和方法,如何实现“利用原型让一个引用类型继承另一个引用类型的属性和方法”?就是让一个引用类型的原型属性成为另一个引用类型的实例,示例代码中:SubType的原型不仅具有作为一个 SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型。最终实现的结果就是: instance 指向 SubType的原型, SubType 的原型又指向SuperType的原型

ps: 所有引用类型默认都继承了 Object ,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。 啥意思?就是任何一个函数fn.prototype.__proto__ === Object.prototypetrue,js内部就是通过原型继承来实现的,让fn.prototype = new Object(),这也正是所有自定义类型都会继承 toString() 、valueOf() 等默认方法的根本原因。哈哈,其实没有可以去写原型链继承,在开发时,不经意间就用上了继承。 来图说明下以上代码的原型链:

如何判断一个实例是否是某个构造函数的实例,但是由于原型链的关系,我们可以说 instance 是 Object 、 SuperType 或 SubType 中任何一个类型的实例。 instanceofisPrototypeOf 都可以用来判断。原型链继承其实有一个毛病,大家可以看到,原型链继承是在原型上放想要继承的对象的实例,那全部属性和方法都是放在 prototype原型上,则这些属性和方法又会被它自己的实例们所共享,如果有一个属性是引用类型,有一个实例去改变了这个属性,那所有实例获得到的属性都是被改变后的了;还有一个问题,都放在原型上的话,所有实例们所读取的值都一样一样的,做不了差异性。那怎么办呢,我们在想,能不能把需要有差异性的属性在自身属性中继承,而不是放在原型中去继承?借用 构造函数和原型链的组合继承 可以做到。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(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(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
复制代码

如上代码,我们可以利用call或者apply来调用想要继承的构造函数,把this指给自身,这样,继承者自身就拥有了被继承者的自身的属性,相比之前,将属性“正大光明”的继承在了自身属性中,而不是在prototype中“偷偷”继承。其他还是一样写。以上的继承方式是JS中最常用的方式。不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数,也就是说,以上例子:有两组 namecolors 属性:一组在实例上,一组在 SubType 原型中。所以如果要改进,就需要把两次调用SuperType构造函数变成一次调用:

function inheritPrototype(subType, superType){
    var prototype = Object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}
function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
    alert(this.age);
};
复制代码

以上就是寄生组合式继承,这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf() 。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

ECMAScript6 中,对于“类”有了新的语法糖,实质还是基于原型链的原理:

class PersonClass {
    // 等价于 PersonType 构造器
    constructor(name) {
        this.name = name;
    }
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
    // 等价于 PersonType.create 
    static create(name) { // 静态成员
        return new PersonClass(name);
    }
}
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
复制代码

使用是要注意:

  • 类声明不会被提升,与let声明相似
  • 类声明中的所有代码自动运行在严格严格模式下
  • 类的所所有方法都不可枚举(不能不用for in 遍历到)
  • 类中的方法不能用new调用(内部都没有 [[Construct]])

ES6继承:

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    getArea() {
        return this.length * this.width;
    }
}
class Square extends Rectangle {
    constructor(length) {
        // 与 Rectangle.call(this, length, length) 相同
        super(length, length);
    }
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
复制代码

注意:继承了其他类的类被称为派生类( derived classes )。如果派生类指定了构造器,就需要 使用 super() ,否则会造成错误。若你选择不使用构造器, super() 方法会被自动调用, 并会使用创建新实例时提供的所有参数。继承可以把基类的静态方法直接继承下来!

以上就是这篇文章的全部了,参考《javascript高级程序设计》《深入理解ES6》

转载于:https://juejin.im/post/5c525534e51d4569c508a83f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值