js面向对象演化过程

参考连接:https://blog.csdn.net/weixin_33739541/article/details/91419021    自己再过一遍,方便理解和记忆,不喜勿喷

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

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

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

使用场景,举个栗子:

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

        for(let i = 0;i < 100;i++){
            var div = document.createElement("div");
            document.body.appendChild(div);
            div.style.width = "100px";
            div.style.height = "100px";
            div.style.border = "1px solid #000";
            div.innerHTML = i + 1;
            div.style.textAlign = "center";
            div.style.lineHeight = "100px";
        }

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

面向对象,首先要有对象

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

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

        var person = {
            name:"tom",
            age:29,
            job:"coding",
            sayName:function(){
                alert(this.name)
            }
        }

知识延伸,可跳过:

如果要删除对象中的某个属性可以用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, // 不可修改  //不能与get/set属性共存
            value: "tom", // 读、写的位置 //不能与get/set属性共存
            get: function(){ // 读name属性的时候调用
                console.log("get");
                return this.value;
            },
            set: function(newValue){ // 写name属性的时候调用
                console.log("set");
                if (newValue === ‘cc‘) {
                    this.value = newValue;
                    this.age += 1;
                }
            }
        })
        // delete person.name;  //执行删除时会报错
        for(let key in person){//没有枚举出name属性
            console.log(key);
        }
        person.name = ‘cc‘;//报错
        console.log(person);

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("tom",18,‘coding‘);
        var person2 = createPerson("bob",19,‘log‘);

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

        console.log(person1.constructor === Object);//true
        console.log(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 
person2 instanceof Person // true

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

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

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

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

js面向对象演化过程

 

 

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

        Person.prototype.constructor === Person // true
        Person.constructor === Function // true 因为Person.__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都是Person new出来的,所以,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(){
        console.log(this.name);
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
console.log(person1.friends); //"Shelby,Count,Van"
console.log(person2.friends); //"Shelby,Count"
console.log(person1.friends === person2.friends); //false
console.log(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();
console.log(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.prototype 为true,js内部就是通过原型继承来实现的,让fn.prototype = new Object(),这也正是所有自定义类型都会继承 toString() 、valueOf() 等默认方法的根本原因。哈哈,其实没有可以去写原型链继承,在开发时,不经意间就用上了继承。

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

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

如上代码,我们可以利用call或者apply来调用想要继承的构造函数,把this指给自身,这样,继承者自身就拥有了被继承者的自身的属性,相比之前,将属性“正大光明”的继承在了自身属性中,而不是在prototype中“偷偷”继承。其他还是一样写。以上的继承方式是JS中最常用的方式。不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数,也就是说,以上例子:有两组 name 和colors 属性:一组在实例上,一组在 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(){
    console.log(this.name);
};
function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
    console.log(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() 方法会被自动调用, 并会使用创建新实例时提供的所有参数。继承可以把基类的静态方法直接继承下来!

完!

 

 

js面向对象演化过程

上一篇:Ajax表单提交方式


下一篇:移动端 响应式、自适应、适配 实现方法分析(和其他基础知识拓展)