美文网首页前端开发那些事让前端飞Web前端之路
学会了面向对象,还怕没有对象?

学会了面向对象,还怕没有对象?

作者: 视觉派Pie | 来源:发表于2019-09-27 07:37 被阅读0次

    面向对象是一种编程思想,我们通过类(构造函数)和对象实现的面向对象编程,满足下述三个特定:封装、继承和多态。

    封装

    封装创建对象的函数

    封装即把实现一个功能的代码封装到一个函数中,以后实现这个功能,只需要执行该函数即可。实现低耦合,高内聚。

    现在我们把属性和方法封装成一个对象:

            //创建一个对象
            var person = new Object();
            //添加属性和方法
            person.name = "钢铁侠";
            person.sex = "男";
            person.showName = function(){
                alert("我的名字叫" + this.name);//我的名字叫钢铁侠
            }
            person.showSex = function(){
                alert("我的性别是" + this.sex);//我的性别是男
            }
            person.showName();
            person.showSex();
    

    如果我们想创建一个不同性别不同姓名的对象,就需要再写一遍上述代码:

            //创建一个对象
            var person2 = new Object();
            //添加属性和方法
            person2.name = "猩红女巫";
            person2.sex = "女";
            person2.showName = function(){
                alert("我的名字叫" + this.name);//我的名字叫猩红女巫
            }
            person2.showSex = function(){
                alert("我的性别是" + this.sex);//我的性别是女
            }
            person2.showName();
            person2.showSex();
    

    如果我们想要创建多个对象的话,写起来就非常麻烦,所以要去封装创建对象的函数解决代码重复的问题。

            function createPerson(name, sex){
                var person = new Object();
                person.name = name;
                person.sex = sex;
                person.showName = function(){
                    alert("我叫" + this.name);
                }
                person.showSex = function(){
                    alert("我是" + this.sex + "的");
                }
                return person;
            }
    

    然后生成实例对象,就等于是在调用函数:

            var p1 = createPerson("钢铁侠", "男");
            p1.showName();//我叫钢铁侠
            p1.showSex();//我是男的
    
            var p2 = createPerson("猩红女巫", "女");
            p2.showName();//我叫猩红女巫
            p2.showSex();//我是女的
    

    上述过程可以类比为开工厂生产酸奶:第一步:需要原料;第二步:加工酸奶;第三步:出厂售卖;我们通过var声明空对象的这一步就相当于第一步原料,添加属性和函数就相当于第二步加工,通过return返回对象就相当于第三步出厂。这种符合上述1、2、3步骤的函数叫做工厂函数,这种设计函数的思路,叫做工厂设计模式。

    通过new调用函数

    官方函数创建对象的方法是通过new的方法,当我们不使用new创建对象的时候,函数内部的this会指向窗口。

            function show(){
                alert(this);//[object Window]
            }
            show();
    

    所以当我们在函数内部给this.name赋值为xxxx时,可以通过window.name输出xxxx,因为如果这个函数没有主人的话它的主人就是window对象。

            function show(){
                alert(this);//[object Window]
                this.name = "xxxx";
            }
            show();
            alert(window.name);//xxxx
    

    但是如果这个函数通过new运算符去调用,那么这个函数中的this,就会指向新创建的对象。

            function show(){
                alert(this);//[object Object]
            }
            var obj = new show();
    

    当我们通过new运算符去调用函数的时候,它首部和尾部会自动的生成以下两步:1、原料操作:强制改变this指向this = new Object(); 3、出厂操作:将this返回return this;

            function show(){
                // this = new Object();
                alert(this);//[object Object]
                this.name = "xxxx";
                // return this;
            }
            var obj = new show();
            alert(obj.name);//xxxx
    

    所以现在我们改造一下之前创建的函数,调用的时候全部都通过new去调用,并且将函数中的person改成this。

            function createPerson(name, sex){
                this.name = name;
                this.sex = sex;
                this.showName = function(){
                    alert("我叫" + this.name);
                }
                this.showSex = function(){
                    alert("我是" + this.sex + "的");
                }
            }
            var p1 = new createPerson("钢铁侠", "男");
            p1.showName();//我叫钢铁侠
            p1.showSex();//我是男的
    
            var p2 = new createPerson("猩红女巫", "女");
            p2.showName();//我叫猩红女巫
            p2.showSex();//我是女的
    

    构造函数

    我们把这种可以创建对象的函数,叫做构造函数。(功能就是用来构造对象)

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

    为了和别的函数,进行区分,我们把构造函数首字母大写。官方的构造函数:Array、Object、Date。

    我们通过typeof可以看到官方通过new创建的Object、Array、Date本质上都是function函数。而且所有被该函数,创建的对象,对象的方法都是一套,arr1.push === arr2.push返回值是true。

            var arr = new Array();
            var obj = new Object();
            var d = new Date();
            alert(typeof Array);//类型 function函数
    

    但是通过调用函数生成的对象方法,彼此之间没有联系,不能反映出它们是同一个原型对象的实例。alert(p1.showName === p2.showName);返回值为false。

            var arr1 = new Array(10, 20, 30);
            var arr2 = new Array(40, 50, 60);
            alert(arr1.push === arr2.push); //true
    

    我们声明两个数组

            var arr1 = [10, 20, 30, 40, 50];
            var arr2 = [60, 70, 80, 90, 100];
    

    给数组添加求和的函数

            arr1.sum = function(){
                var res = 0;
                for(var i = 0; i < this.length; i++){
                    res += this[i];
                }
                return res;
            }
    

    调用arr1.sum可以输出arr1的和为150,但是调用arr2.sum会系统报错,提示arr1.sum不是一个函数。因为arr1和arr2是单独的两个对象,给arr1添加一个方法,arr2并不会拥有这个方法。所以我们之前通过new调用函数生成对象后,他们的方法是相互独立的。

    alert(arr1.sum == arr2.sum);//false
    

    每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。

    原型prototype

    prototype对象的引入:所有实例对象需要共享的属性和方法,都放在这个对象中,那些不需要共享的属性和方法,就放在构造函数中。以此来模拟类。

    所以想让arr2也拥有求和函数就需要再重新写一个arr2.sum,这样就会造成浪费,我们想让对象共用一个方法,这时候就需要引入原型prototype。在JS中一切皆对象,函数也是对象。 每一个被创建的函数,都有一个官方内置的属性,叫做prototype(原型)对象 ,我们输出一下show.protoype,得到结果[object Object]

            function show(){
            }
            alert(show.protoype);//[object Object]
    

    所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。

    如果,我们想要让该函数创建出来的对象,公用一套函数,那么我们应该将这套函数,添加该函数的prototype原型。 所以我们如果想让两个数组都拥有求和的方法,就需要将这个方法添加在Array的原型上。

            Array.prototype.sum = function(){
                var res = 0;
                for(var i = 0; i < this.length; i++){
                    res += this[i];
                }
                return res;
            }
    

    现在arr1和arr2都可以使用这个函数,并且arr1.sum == arr2.sum,他们使用的这个函数都是原型上的同一个方法。

            alert(arr1.sum());//150
            alert(arr2.sum());//400
            alert(arr1.sum == arr2.sum);//true
    

    我们可以通过混合法,让用户自定义构造函数,封装一个可以创建对象的函数,并且调用的是同一个方法。

            function Person(name, sex){
                //this = new Object();
                this.name = name;
                this.sex = sex;
                //return this;
            }
    
            //函数,必须,添加在这个函数的prototype原型
            Person.prototype.showName = function(){
                alert("我叫" + this.name);
            }
            Person.prototype.showSex = function(){
                alert("我是" + this.sex + "的");
            }
            var p1 = new Person("钢铁侠", '男');
            var p2 = new Person("猩红女巫", "女");
    
            p1.showName();//我叫钢铁侠
            p1.showSex();//我是男的
            p2.showName();//我叫猩红女巫
            p2.showSex();//我是女的
    
            alert(p1.showName == p2.showName); //true
    

    面向对象编程案例

    现在我们要测试100辆不同品牌的汽车,记录他们在道路上行驶的性能指数。

    创建一个可以构造各式各样车的构造函数

            function Car(type, name, speed){
                this.type = type;
                this.name = name;
                this.speed = speed;
            }
    

    在Car的原型上添加功能:让车跑在路上,计算时速。

            Car.prototype.run = function(road){
                alert(`一辆${this.type}品牌的${this.name}系列,时速为${this.speed}km/h的车,跑在长度为${road.length}km的${road.name},最终的成绩是${road.length / this.speed}小时`);
            }
    

    创建一个可以构造各式各样马路的构造函数

            function Road(name, length){
                this.name = name;
                this.length = length;
            }
    

    添加第一个测试用例car1:

            var kuahaidaqiao = new Road("跨海大桥", 1000);
            var car1 = new Car("大众", "PASSAT", 100);
            car1.run(kuahaidaqiao);//一辆大众品牌的PASSAT系列,时速为100km/h的车,跑在长度为1000km的跨海大桥,最终的成绩是10小时
    

    这就是面向对象编程,只写一遍代码,之后再要创建对象,只需要调用封装好的函数。类和对象是面向对象编程的两个语法,是面向对象实现的基础,但是在JS中没有类的概念,一切皆对象,所有的实例都是由Object构造函数构造出来的。所以当我们有类的需求的时候,我们自创了一个构造函数,来替代类的存在,所以构造函数的本质就是类。

    Prototype模式的验证方法

    为了配合prototype属性,Javascript定义了一些关键字,帮助我们使用它。

    1. instanceof

      格式:对象 instanceof 构造函数

      功能:判断这个对象是否是后面这个构造函数构造的。如果是,返回true;否则,返回false。

           alert(car1 instanceof Car);//true
           alert(car1 instanceof Road);//false
           alert(car1 instanceof Object);//true
      
    2. isPrototypeOf()

      格式:构造函数.prototype.isPrototypeOf(对象)

      功能:判断某个proptotype对象是否拥有某个实例,如果是,返回true;否则,返回false。

           alert(Car.prototype.isPrototypeOf(car1)); //true
           alert(Road.prototype.isPrototypeOf(kuahaidaqiao)); //true
      
    3. hasOwnProperty()

      格式:对象.hasOwnProperty("属性")

      功能:实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。

           Car.prototype.color = "white";
           alert(car1.hasOwnProperty("name")) //true
           alert(car1.hasOwnProperty("color")) //false
      
    4. in运算符

      格式:"属性"in对象

      功能:in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。

           alert("type" in car1); // true
           alert("length" in car1); // false
      

      in运算符还可以用来遍历某个对象的所有属性,包括添加在它身上的方法。

      for(var prop in car1) { alert("car1["+prop+"]="+car1[prop]); } 
      

    ECMA6语法糖 class类

    ES6提供了简单的定义类的语法糖class

    构造函数的写法:

            function Iphone(size, color){
                this.size = size;
                this.color = color;
            }
    
            Iphone.prototype.show = function(){
                alert(`您选择了一部${this.color}颜色的,内存大小是${this.size}GB的手机`);
            }
    
            var iphone1 = new Iphone(64, "玫瑰金");
            iphone1.show();//您选择了一部玫瑰金颜色的,内存大小是64GB的手机
    

    类的写法:

            class Iphone{
                constructor(size, color){
                    this.size = size;
                    this.color = color;
                }
                show(){
                    alert(`您选择了一部${this.color}颜色的,内存大小是${this.size}GB的手机`);
                }
            }
    
            var iphone2 = new Iphone(256, "黑色");
            iphone2.show();//您选择了一部黑色颜色的,内存大小是256GB的手机
    

    继承

    由于所有的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象一样。这就是Javascript继承机制的设计思想。

    继承一方面是为了实现面向对象,另一方面为了帮助大家更高效的编写代码,可以让一个构造函数继承另一个构造函数中的属性和方法。

    首先我们定义一个People类

            function Person(name, sex){
                this.name = name;
                this.sex = sex;
            }
            Person.prototype.showName = function(){
                alert("我叫" + this.name);
            }
            Person.prototype.showSex = function(){
                alert("我是" + this.sex);
            }
    

    现在我们要在Person类的基础上创建一个Worker类,拥有Person类的全部属性和方法,同时添加它自己的属性job,我们可以通过以下几种方法实现继承。

            function Person(name, sex){
                this.name = name;
                this.sex = sex;
            }
            Person.prototype.showName = function(){
                alert("我叫" + this.name);
            }
            Person.prototype.showSex = function(){
                alert("我是" + this.sex);
            }
    

    call/apply

    第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,这种继承Person的方式叫做构造函数的伪装,因为Person对象原本应该只为new的Person对象服务,但是它现在还可以被Worker对象使用。

            function Worker(name, sex, job){
                //继承Person的属性
                Person.call(this, name, sex); 
                this.job = job;//添加自己的属性
            }
            var w1 = new Worker("小明", "男", "程序员");
    

    call和apply的区别在于参数形式不同,call(obj, pra, pra)后面是单个参数。apply(obj, [args])后面是数组,作用都是强制改变this的指向。

            function Worker(name, sex, job){
                //继承Person的属性
                //构造函数的伪装
                Person.apply(this, arguments);
                this.job = job;
            }
    

    现在Worker想继承Person上的方法,Person方法都放在prototype中,prototype本质是对象,存储的是引用数据类型,所以我们不能直接将父级的方法赋值给子,继承只能是单向的,子继承父,但是不能影响父。

    每一个构造函数身上都会有一个prototype原型,我们可以将Person身上的prototype遍历,在遍历的过程中将Person身上的函数取出,放入Worker的prototype中,这样它们就不会互相影响了。

            for(var i in Person.prototype){
                Worker.prototype[i] = Person.prototype[i];
            }
    

    同时通过prototype来拓展自己的方法

            Worker.prototype.showJob = function(){
                alert("我是干" + this.job + "工作的");
            }
    

    直接调用父级的构造函数继承

    第二种方法更常见,使用prototype属性。如果"Worker"的prototype对象,指向一个Person的实例,那么所有"Worker"的实例,就能继承Person了。

    Worker.prototype = new Person();
    

    每一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有"Worker.prototype = new Person();"这一行,Worker.prototype.constructor是指向Worker的;加了这一行以后,Worker.prototype.constructor指向Person。

    alert(Worker.prototype.constructor == Person) //true
    

    更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。因此,在运行"Worker.prototype = new Person();"这一行之后,w1.constructor也指向Person。

            var w1 = new Worker("小明", "男", "程序员");
            alert(w1.constructor == Worker.prototype.constructor) //true
            alert(w1.constructor == Person.prototype.constructor) //true
            alert(w1.constructor == Person) //true
    

    这显然会导致继承链的紊乱(w1明明是用构造函数Worker生成的),因此我们必须手动纠正,将Worker.prototype对象的constructor值改为Worker。这是很重要的一点,编程时务必要遵守。

            Worker.prototype.constructor = Worker
            alert(w1.constructor == Worker.prototype.constructor) //true
            alert(w1.constructor == Person.prototype.constructor) //false
            alert(w1.constructor == Person) //false
    

    Object.create()拷贝继承

    上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象。Object.create()类似于数组中的concat方法,可以创建一个新对象。这样就可以将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。

            //拷贝原有对象,创建新对象
            Worker.prototype = Object.create(Person.prototype);
    

    多态

    创建一个父亲的构造函数

        function Father(name, sex, age){
            this.name = name;
            this.sex = sex;
            this.age = age;
        }
        Father.prototype.sing = function(){
            alert(`父亲的民歌唱得非常好`);
        }
    

    再创建一个儿子构造函数,继承父亲的属性和方法

        function Son(name, sex, age, degree){
            //构造函数的伪装
            Father.call(this,name, sex, age);
    
            //拓展自己的属性
            this.degree = degree;
    
        }
    
        //继承方法
        //原型链
        for(var i in Father.prototype[i]){
            Son.prototype[i] = Father.prototype[i];
        }
    

    通过new创建一个儿子对象,执行son1.sing()调用父亲原型上的方法。

        var son1 = new Son("小明", "男", 20, "本科");
        son1.sing();//父亲的民歌唱得非常好
    

    现在我们重写父级继承的函数

        Son.prototype.sing = function(){
            alert(`唱摇滚`);
        }
    

    再次调用son1.sing(),执行的是son1自己添加的sing方法。

        var son1 = new Son("小明", "男", 20, "本科");
        son1.sing();/唱摇滚
    

    对于父亲自身的sing方法没有影响,在子级重写的方法只在子级生效。

        var f2 = new Father("大明", "男", 40);
        f2.sing();//父亲的民歌唱得非常好
    

    现在我们再来看继承和多态的概念,其实它们都是继承某一部分,是同一件事情的两个侧重。继承侧重于从父级继承到属性和方法,而多态侧重于自己拓展的属性和方法,也就是重写的内容。简单来说凡是跟父级一样的部分叫继承,不一样的部分叫多态。

    注意:虽然Son继承了Father的属性和方法,但是通过Son构造函数new出来的对象不属于Father。

        alert(son1 instanceof Son) //true
        alert(f2 instanceof Father) //true
        alert(son1 instanceof Father) //false
        alert(son1 instanceof Object) //true
    

    同样的案例使用ECMA6 class类的方法来实现继承和多态,对比原来的方法更简单,更形象。

            class Father{
                constructor(name, sex, age){
                    this.name = name;
                    this.sex = sex;
                    this.age = age;
                }
    
                //声明方法
                sing(){
                    alert("会唱民歌");
                }
            }
    
            /*
                继承Father创建一个子类Son
                通过extends继承
            */
        
            class Son extends Father{
                constructor(name, sex, age, degree){
                    super(name, sex, age);
                }
                sing(){
                    alert("唱摇滚");
                }
            }
    
            var son1 = new Son("小明", "男", 30, "本科");
            alert(son1.name);//小明
            son1.sing();//唱摇滚
    
            var f1 = new Father("大明", "男", 40);
            alert(f1.name);//大明
            f1.sing();//会唱民歌
    

    原型链

    在javascript中,每个对象都有一个指向它的原型(prototype)对象的内部链接。每个原型对象又有自己的原型,直到某个对象的原型为null为止,组成这条链的最后一环。

    在构造函数有一个prototype的属性,通过构造函数构造出来的对象有_ _proto _ _属性,指向构造该对象的构造函数的原型,它还有一个名字叫魔术变量。

    也就是说通过Son构造函数构造出来的对象son1的_ _proto _ _完全等于Son的原型prototype。

    alert(son1.__proto__ === Son.prototype);//true
    alert(f2.__proto__ === Father.prototype);//true
    

    这也就解释了为什么通过同一个构造函数构造出来的对象,使用的都是同一套函数,因为通过该构造函数构造出来所有对象的_ _proto _ _就指向该构造函数的原型。

    相关文章

      网友评论

        本文标题:学会了面向对象,还怕没有对象?

        本文链接:https://www.haomeiwen.com/subject/zoxbsctx.html